Merge "HeifWriter / AvifWriter: fix Java doc" into androidx-main
diff --git a/.github/actions/build-single-project/action.yml b/.github/actions/build-single-project/action.yml
index ecedb70..a9f207f 100644
--- a/.github/actions/build-single-project/action.yml
+++ b/.github/actions/build-single-project/action.yml
@@ -36,7 +36,7 @@
shell: bash
run: |
set -x
- NDK_VERSION=$(grep "ndkVersion" settings.gradle | awk -F "=" '{gsub(/"| /, ""); print $2}')
+ NDK_VERSION=$(grep "ndkVersion" buildSrc/ndk.gradle | awk -F "=" '{gsub(/"| /, ""); print $2}')
echo "yes" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "ndk;$NDK_VERSION"
- name: "Install Android SDK Build-Tools"
shell: bash
diff --git a/activity/activity-compose-lint/build.gradle b/activity/activity-compose-lint/build.gradle
index 42e9863..f60b53f 100644
--- a/activity/activity-compose-lint/build.gradle
+++ b/activity/activity-compose-lint/build.gradle
@@ -34,13 +34,13 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
compileOnly(libs.intellijCore)
compileOnly(libs.uast)
compileOnly(libs.intellijKotlinCompiler)
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testRuntimeOnly(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 1ec1fad..bd7ba5b 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
implementation(libs.kotlinCoroutinesCore)
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
- api(projectOrArtifact(":activity:activity-ktx"))
+ api(project(":activity:activity-ktx"))
api("androidx.compose.ui:ui:1.0.1")
api("androidx.core:core-ktx:1.13.0")
api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
@@ -46,8 +46,8 @@
androidTestImplementation("androidx.annotation:annotation:1.8.1")
androidTestImplementation("androidx.compose.foundation:foundation-layout:1.6.0")
- androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
- androidTestImplementation projectOrArtifact(":compose:material:material")
+ androidTestImplementation project(":compose:ui:ui-test-junit4")
+ androidTestImplementation project(":compose:material:material")
androidTestRuntimeOnly project(":compose:test-utils")
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:runtime:runtime"))
@@ -57,7 +57,7 @@
androidTestImplementation(project(":compose:ui:ui-text"))
androidTestImplementation(project(":lifecycle:lifecycle-common"))
androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime-testing")
+ androidTestImplementation project(":lifecycle:lifecycle-runtime-testing")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.2")
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
@@ -65,8 +65,8 @@
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
- lintChecks(projectOrArtifact(":activity:activity-compose-lint"))
- lintPublish(projectOrArtifact(":activity:activity-compose-lint"))
+ lintChecks(project(":activity:activity-compose-lint"))
+ lintPublish(project(":activity:activity-compose-lint"))
}
androidx {
@@ -75,7 +75,7 @@
inceptionYear = "2020"
description = "Compose integration with Activity"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":activity:activity-compose:activity-compose-samples"))
+ samples(project(":activity:activity-compose:activity-compose-samples"))
}
android {
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 27063bd..361a6fa 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -38,8 +38,8 @@
api("androidx.compose.foundation:foundation-layout:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
implementation "androidx.compose.foundation:foundation:1.0.1"
- implementation projectOrArtifact(":activity:activity-compose")
- implementation projectOrArtifact(":activity:activity")
+ implementation project(":activity:activity-compose")
+ implementation project(":activity:activity")
implementation("androidx.compose.ui:ui-graphics:1.0.1")
implementation("androidx.compose.ui:ui-text:1.0.1")
implementation("androidx.compose.ui:ui:1.0.1")
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index e62b3c9..99a5da5 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -28,7 +28,7 @@
api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
api("androidx.savedstate:savedstate:1.2.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation("androidx.tracing:tracing:1.0.0")
implementation(libs.kotlinCoroutinesCore)
api(libs.kotlinStdlib)
diff --git a/activity/integration-tests/baselineprofile/build.gradle b/activity/integration-tests/baselineprofile/build.gradle
index b340454..1d63008 100644
--- a/activity/integration-tests/baselineprofile/build.gradle
+++ b/activity/integration-tests/baselineprofile/build.gradle
@@ -47,8 +47,8 @@
}
dependencies {
- implementation(projectOrArtifact(":benchmark:benchmark-junit4"))
- implementation(projectOrArtifact(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
implementation(libs.testRules)
implementation(libs.testExtJunit)
implementation(libs.testCore)
diff --git a/annotation/annotation/api/1.9.0-beta01.txt b/annotation/annotation/api/1.9.0-beta01.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/1.9.0-beta01.txt
@@ -0,0 +1,372 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+ method public abstract String suggest() default "";
+ property public abstract String suggest;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+ method public abstract int api() default -1;
+ method public abstract String codename() default "";
+ method public abstract int extension() default 0;
+ method public abstract int lambda() default -1;
+ method public abstract int parameter() default -1;
+ property public abstract int api;
+ property public abstract String codename;
+ property public abstract int extension;
+ property public abstract int lambda;
+ property public abstract int parameter;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+ method public abstract int api();
+ method public abstract String message() default "";
+ property public abstract int api;
+ property public abstract String message;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+ method public abstract int unit() default androidx.annotation.Dimension.PX;
+ property public abstract int unit;
+ field public static final androidx.annotation.Dimension.Companion Companion;
+ field public static final int DP = 0; // 0x0
+ field public static final int PX = 1; // 0x1
+ field public static final int SP = 2; // 0x2
+ }
+
+ public static final class Dimension.Companion {
+ field public static final int DP = 0; // 0x0
+ field public static final int PX = 1; // 0x1
+ field public static final int SP = 2; // 0x2
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+ method public abstract String message();
+ property public abstract String message;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+ method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+ method public abstract boolean fromInclusive() default true;
+ method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+ method public abstract boolean toInclusive() default true;
+ property public abstract double from;
+ property public abstract boolean fromInclusive;
+ property public abstract double to;
+ property public abstract boolean toInclusive;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+ method public abstract String value();
+ property public abstract String value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+ method @Deprecated public abstract int attributeId() default 0;
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+ method @Deprecated public abstract boolean hasAttributeId() default true;
+ method @Deprecated public abstract String name() default "";
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+ property @Deprecated public abstract int attributeId;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+ property @Deprecated public abstract boolean hasAttributeId;
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+ method @Deprecated public abstract String name();
+ method @Deprecated public abstract int value();
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract int value;
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+ method @Deprecated public abstract int mask() default 0;
+ method @Deprecated public abstract String name();
+ method @Deprecated public abstract int target();
+ property @Deprecated public abstract int mask;
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract int target;
+ }
+
+ @Deprecated public enum InspectableProperty.ValueType {
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+ method public abstract boolean flag() default false;
+ method public abstract boolean open() default false;
+ method public abstract int[] value();
+ property public abstract boolean flag;
+ property public abstract boolean open;
+ property public abstract int[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+ method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+ method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+ property public abstract long from;
+ property public abstract long to;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+ method public abstract boolean flag() default false;
+ method public abstract boolean open() default false;
+ method public abstract long[] value();
+ property public abstract boolean flag;
+ property public abstract boolean open;
+ property public abstract long[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+ }
+
+ @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+ method public abstract String expression();
+ method public abstract String[] imports();
+ property public abstract String expression;
+ property public abstract String[] imports;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+ method public abstract int api() default 1;
+ method public abstract int value() default 1;
+ property public abstract int api;
+ property public abstract int value;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+ method public abstract int extension();
+ method public abstract int version();
+ property public abstract int extension;
+ property public abstract int version;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+ method public abstract androidx.annotation.RequiresExtension[] value();
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+ method public abstract String enforcement();
+ method public abstract String name();
+ property public abstract String enforcement;
+ property public abstract String name;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+ method public abstract String[] allOf();
+ method public abstract String[] anyOf();
+ method public abstract boolean conditional() default false;
+ method public abstract String value() default "";
+ property public abstract String[] allOf;
+ property public abstract String[] anyOf;
+ property public abstract boolean conditional;
+ property public abstract String value;
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+ method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+ property public abstract androidx.annotation.RequiresPermission value;
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+ method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+ property public abstract androidx.annotation.RequiresPermission value;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+ method public abstract androidx.annotation.RestrictTo.Scope[] value();
+ property public abstract androidx.annotation.RestrictTo.Scope[] value;
+ }
+
+ public enum RestrictTo.Scope {
+ enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+ method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+ method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+ method public abstract long multiple() default 1;
+ method public abstract long value() default -1;
+ property public abstract long max;
+ property public abstract long min;
+ property public abstract long multiple;
+ property public abstract long value;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+ method public abstract boolean open() default false;
+ method public abstract String[] value();
+ property public abstract boolean open;
+ property public abstract String[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+ method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+ property public abstract int otherwise;
+ field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+ field public static final int NONE = 5; // 0x5
+ field public static final int PACKAGE_PRIVATE = 3; // 0x3
+ field public static final int PRIVATE = 2; // 0x2
+ field public static final int PROTECTED = 4; // 0x4
+ }
+
+ public static final class VisibleForTesting.Companion {
+ field public static final int NONE = 5; // 0x5
+ field public static final int PACKAGE_PRIVATE = 3; // 0x3
+ field public static final int PRIVATE = 2; // 0x2
+ field public static final int PROTECTED = 4; // 0x4
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+ }
+
+}
+
diff --git a/annotation/annotation/api/restricted_1.9.0-beta01.txt b/annotation/annotation/api/restricted_1.9.0-beta01.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/restricted_1.9.0-beta01.txt
@@ -0,0 +1,372 @@
+// Signature format: 4.0
+package androidx.annotation {
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnimatorRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AnyRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface AnyThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ArrayRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface AttrRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface BinderThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface BoolRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CallSuper {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface CheckResult {
+ method public abstract String suggest() default "";
+ property public abstract String suggest;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ChecksSdkIntAtLeast {
+ method public abstract int api() default -1;
+ method public abstract String codename() default "";
+ method public abstract int extension() default 0;
+ method public abstract int lambda() default -1;
+ method public abstract int parameter() default -1;
+ property public abstract int api;
+ property public abstract String codename;
+ property public abstract int extension;
+ property public abstract int lambda;
+ property public abstract int parameter;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorInt {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface ColorLong {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface ColorRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CONSTRUCTOR) public @interface ContentView {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface DeprecatedSinceApi {
+ method public abstract int api();
+ method public abstract String message() default "";
+ property public abstract int api;
+ property public abstract String message;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DimenRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Dimension {
+ method public abstract int unit() default androidx.annotation.Dimension.PX;
+ property public abstract int unit;
+ field public static final androidx.annotation.Dimension.Companion Companion;
+ field public static final int DP = 0; // 0x0
+ field public static final int PX = 1; // 0x1
+ field public static final int SP = 2; // 0x2
+ }
+
+ public static final class Dimension.Companion {
+ field public static final int DP = 0; // 0x0
+ field public static final int PX = 1; // 0x1
+ field public static final int SP = 2; // 0x2
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public @interface Discouraged {
+ method public abstract String message();
+ property public abstract String message;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface DisplayContext {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface DoNotInline {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface DrawableRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface EmptySuper {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface FloatRange {
+ method public abstract double from() default kotlin.jvm.internal.DoubleCompanionObject.NEGATIVE_INFINITY;
+ method public abstract boolean fromInclusive() default true;
+ method public abstract double to() default kotlin.jvm.internal.DoubleCompanionObject.POSITIVE_INFINITY;
+ method public abstract boolean toInclusive() default true;
+ property public abstract double from;
+ property public abstract boolean fromInclusive;
+ property public abstract double to;
+ property public abstract boolean toInclusive;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FontRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface FractionRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface GravityInt {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface GuardedBy {
+ method public abstract String value();
+ property public abstract String value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FIELD}) public @interface HalfFloat {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IdRes {
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InspectableProperty {
+ method @Deprecated public abstract int attributeId() default 0;
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping();
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping();
+ method @Deprecated public abstract boolean hasAttributeId() default true;
+ method @Deprecated public abstract String name() default "";
+ method @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType() default androidx.annotation.InspectableProperty.ValueType.INFERRED;
+ property @Deprecated public abstract int attributeId;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.EnumEntry[] enumMapping;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.FlagEntry[] flagMapping;
+ property @Deprecated public abstract boolean hasAttributeId;
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract androidx.annotation.InspectableProperty.ValueType valueType;
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.EnumEntry {
+ method @Deprecated public abstract String name();
+ method @Deprecated public abstract int value();
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract int value;
+ }
+
+ @Deprecated @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS}) public static @interface InspectableProperty.FlagEntry {
+ method @Deprecated public abstract int mask() default 0;
+ method @Deprecated public abstract String name();
+ method @Deprecated public abstract int target();
+ property @Deprecated public abstract int mask;
+ property @Deprecated public abstract String name;
+ property @Deprecated public abstract int target;
+ }
+
+ @Deprecated public enum InspectableProperty.ValueType {
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType COLOR;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType GRAVITY;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INFERRED;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_ENUM;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType INT_FLAG;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType NONE;
+ enum_constant @Deprecated public static final androidx.annotation.InspectableProperty.ValueType RESOURCE_ID;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface IntDef {
+ method public abstract boolean flag() default false;
+ method public abstract boolean open() default false;
+ method public abstract int[] value();
+ property public abstract boolean flag;
+ property public abstract boolean open;
+ property public abstract int[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface IntRange {
+ method public abstract long from() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+ method public abstract long to() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+ property public abstract long from;
+ property public abstract long to;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface IntegerRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface InterpolatorRes {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface Keep {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface LayoutRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface LongDef {
+ method public abstract boolean flag() default false;
+ method public abstract boolean open() default false;
+ method public abstract long[] value();
+ property public abstract boolean flag;
+ property public abstract boolean open;
+ property public abstract long[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface MainThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface MenuRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface NavigationRes {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface NonNull {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface NonUiContext {
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FILE}) public @interface Nullable {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface OpenForTesting {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface PluralsRes {
+ }
+
+ @Dimension(unit=androidx.annotation.Dimension.Companion.PX) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface Px {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+ method public abstract String expression();
+ method public abstract String[] imports();
+ property public abstract String expression;
+ property public abstract String[] imports;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresApi {
+ method public abstract int api() default 1;
+ method public abstract int value() default 1;
+ property public abstract int api;
+ property public abstract int value;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RequiresExtension {
+ method public abstract int extension();
+ method public abstract int version();
+ property public abstract int extension;
+ property public abstract int version;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public static @interface RequiresExtension.Container {
+ method public abstract androidx.annotation.RequiresExtension[] value();
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface RequiresFeature {
+ method public abstract String enforcement();
+ method public abstract String name();
+ property public abstract String enforcement;
+ property public abstract String name;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface RequiresPermission {
+ method public abstract String[] allOf();
+ method public abstract String[] anyOf();
+ method public abstract boolean conditional() default false;
+ method public abstract String value() default "";
+ property public abstract String[] allOf;
+ property public abstract String[] anyOf;
+ property public abstract boolean conditional;
+ property public abstract String value;
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Read {
+ method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+ property public abstract androidx.annotation.RequiresPermission value;
+ }
+
+ @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public static @interface RequiresPermission.Write {
+ method public abstract androidx.annotation.RequiresPermission value() default androidx.annotation.RequiresPermission();
+ property public abstract androidx.annotation.RequiresPermission value;
+ }
+
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FILE}) public @interface RestrictTo {
+ method public abstract androidx.annotation.RestrictTo.Scope[] value();
+ property public abstract androidx.annotation.RestrictTo.Scope[] value;
+ }
+
+ public enum RestrictTo.Scope {
+ enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES;
+ enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ReturnThis {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS}) public @interface Size {
+ method public abstract long max() default kotlin.jvm.internal.LongCompanionObject.MAX_VALUE;
+ method public abstract long min() default kotlin.jvm.internal.LongCompanionObject.MIN_VALUE;
+ method public abstract long multiple() default 1;
+ method public abstract long value() default -1;
+ property public abstract long max;
+ property public abstract long min;
+ property public abstract long multiple;
+ property public abstract long value;
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS) public @interface StringDef {
+ method public abstract boolean open() default false;
+ method public abstract String[] value();
+ property public abstract boolean open;
+ property public abstract String[] value;
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StringRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface StyleableRes {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface TransitionRes {
+ }
+
+ @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD}) public @interface UiContext {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface UiThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface VisibleForTesting {
+ method public abstract int otherwise() default androidx.annotation.VisibleForTesting.PRIVATE;
+ property public abstract int otherwise;
+ field public static final androidx.annotation.VisibleForTesting.Companion Companion;
+ field public static final int NONE = 5; // 0x5
+ field public static final int PACKAGE_PRIVATE = 3; // 0x3
+ field public static final int PRIVATE = 2; // 0x2
+ field public static final int PROTECTED = 4; // 0x4
+ }
+
+ public static final class VisibleForTesting.Companion {
+ field public static final int NONE = 5; // 0x5
+ field public static final int PACKAGE_PRIVATE = 3; // 0x3
+ field public static final int PRIVATE = 2; // 0x2
+ field public static final int PROTECTED = 4; // 0x4
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER}) public @interface WorkerThread {
+ }
+
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface XmlRes {
+ }
+
+}
+
diff --git a/annotation/annotation/bcv/native/current.txt b/annotation/annotation/bcv/native/current.txt
index 3703dab..f1fd8ff 100644
--- a/annotation/annotation/bcv/native/current.txt
+++ b/annotation/annotation/bcv/native/current.txt
@@ -1,5 +1,5 @@
// Klib ABI Dump
-// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64]
+// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 0b88e7c..043e852 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -22,6 +22,7 @@
mingwX64()
linux()
ios()
+ watchosDeviceArm64()
watchos()
tvos()
wasmJs()
@@ -42,7 +43,7 @@
wasmJsMain {
dependsOn(nonJvmMain)
dependencies {
- api(libs.kotlinStdlibJs)
+ implementation(libs.kotlinStdlibJs)
}
}
diff --git a/appcompat/appcompat-lint/integration-tests/build.gradle b/appcompat/appcompat-lint/integration-tests/build.gradle
index 4c2adc6..5153ca8 100644
--- a/appcompat/appcompat-lint/integration-tests/build.gradle
+++ b/appcompat/appcompat-lint/integration-tests/build.gradle
@@ -13,7 +13,7 @@
dependencies {
implementation(project(":appcompat:appcompat"))
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
api(libs.kotlinStdlib)
}
diff --git a/appcompat/appcompat-resources/build.gradle b/appcompat/appcompat-resources/build.gradle
index a34ebd8..19f68bf 100644
--- a/appcompat/appcompat-resources/build.gradle
+++ b/appcompat/appcompat-resources/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.appcompat", module: "appcompat-resources"
})
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index fd5e26f..855505c 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -32,7 +32,7 @@
api("androidx.drawerlayout:drawerlayout:1.0.0")
implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
api("androidx.savedstate:savedstate:1.2.1")
@@ -46,8 +46,8 @@
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1", {
// Needed to ensure that the same version of lifecycle-runtime-ktx
diff --git a/appcompat/integration-tests/receive-content-testapp/build.gradle b/appcompat/integration-tests/receive-content-testapp/build.gradle
index c33e1bf..3bb6c17 100644
--- a/appcompat/integration-tests/receive-content-testapp/build.gradle
+++ b/appcompat/integration-tests/receive-content-testapp/build.gradle
@@ -29,7 +29,7 @@
implementation(project(":appcompat:appcompat"))
implementation(libs.constraintLayout)
implementation(libs.guavaAndroid)
- implementation(projectOrArtifact(":recyclerview:recyclerview"))
+ implementation(project(":recyclerview:recyclerview"))
implementation(libs.material)
androidTestImplementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 0cc59d0..afc07be 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -291,6 +291,9 @@
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
}
+ @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppSearchApi {
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
@@ -842,6 +845,25 @@
}
+package androidx.appsearch.ast {
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public interface Node {
+ method public default java.util.List<androidx.appsearch.ast.Node!> getChildren();
+ }
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class TextNode implements androidx.appsearch.ast.Node {
+ ctor public TextNode(androidx.appsearch.ast.TextNode);
+ ctor public TextNode(String);
+ method public String getValue();
+ method public boolean isPrefix();
+ method public boolean isVerbatim();
+ method public void setPrefix(boolean);
+ method public void setValue(String);
+ method public void setVerbatim(boolean);
+ }
+
+}
+
package androidx.appsearch.exceptions {
public class AppSearchException extends java.lang.Exception {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 0cc59d0..afc07be 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -291,6 +291,9 @@
method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
}
+ @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAppSearchApi {
+ }
+
public interface Features {
method public int getMaxIndexedProperties();
method public boolean isFeatureSupported(String);
@@ -842,6 +845,25 @@
}
+package androidx.appsearch.ast {
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public interface Node {
+ method public default java.util.List<androidx.appsearch.ast.Node!> getChildren();
+ }
+
+ @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public final class TextNode implements androidx.appsearch.ast.Node {
+ ctor public TextNode(androidx.appsearch.ast.TextNode);
+ ctor public TextNode(String);
+ method public String getValue();
+ method public boolean isPrefix();
+ method public boolean isVerbatim();
+ method public void setPrefix(boolean);
+ method public void setValue(String);
+ method public void setVerbatim(boolean);
+ }
+
+}
+
package androidx.appsearch.exceptions {
public class AppSearchException extends java.lang.Exception {
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 2a7270f..79542c5 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -43,7 +43,7 @@
implementation("androidx.collection:collection:1.4.2")
implementation('androidx.concurrent:concurrent-futures:1.0.0')
- implementation("androidx.core:core:1.6.0")
+ implementation("androidx.core:core:1.9.0")
annotationProcessor project(':appsearch:appsearch-compiler')
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index 5b5ac39..8dd3688 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -752,6 +752,8 @@
.setOrder(SearchSpec.ORDER_ASCENDING)
.setRankingStrategy("this.documentScore()")
.addInformationalRankingExpressions("this.relevanceScore()")
+ .addInformationalRankingExpressions(
+ ImmutableSet.of("this.documentScore() * this.relevanceScore()", "1 + 1"))
.build();
assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
assertThat(searchSpec.getRankingStrategy())
@@ -759,7 +761,8 @@
assertThat(searchSpec.getAdvancedRankingExpression())
.isEqualTo("this.documentScore()");
assertThat(searchSpec.getInformationalRankingExpressions()).containsExactly(
- "this.relevanceScore()");
+ "this.relevanceScore()",
+ "this.documentScore() * this.relevanceScore()", "1 + 1").inOrder();
}
@Test
@@ -772,6 +775,8 @@
SearchSpec original = searchSpecBuilder.build();
SearchSpec rebuild = searchSpecBuilder
.addInformationalRankingExpressions("this.documentScore()")
+ .addInformationalRankingExpressions(
+ ImmutableSet.of("this.documentScore() * this.relevanceScore()", "1 + 1"))
.build();
// Rebuild won't effect the original object
@@ -779,7 +784,8 @@
.containsExactly("this.relevanceScore()");
assertThat(rebuild.getInformationalRankingExpressions())
- .containsExactly("this.relevanceScore()", "this.documentScore()").inOrder();
+ .containsExactly("this.relevanceScore()", "this.documentScore()",
+ "this.documentScore() * this.relevanceScore()", "1 + 1").inOrder();
}
@Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/TextNodeCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/TextNodeCtsTest.java
new file mode 100644
index 0000000..fce2056
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/ast/TextNodeCtsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.cts.ast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.ast.TextNode;
+
+import org.junit.Test;
+
+public class TextNodeCtsTest {
+ @Test
+ public void testConstructor_prefixVerbatimFalseByDefault() {
+ TextNode defaultTextNode = new TextNode("foo");
+
+ assertThat(defaultTextNode.isPrefix()).isFalse();
+ assertThat(defaultTextNode.isVerbatim()).isFalse();
+ }
+
+ @Test
+ public void testCopyConstructor_fieldsCorrectlyCopied() {
+ TextNode fooNode = new TextNode("foo");
+ fooNode.setPrefix(false);
+ fooNode.setVerbatim(true);
+
+ TextNode copyConstructedFooNode = new TextNode(fooNode);
+
+ assertThat(fooNode.getValue()).isEqualTo(copyConstructedFooNode.getValue());
+ assertThat(fooNode.isPrefix()).isEqualTo(copyConstructedFooNode.isPrefix());
+ assertThat(fooNode.isVerbatim()).isEqualTo(copyConstructedFooNode.isVerbatim());
+ }
+
+ @Test
+ public void testCopyConstructor_originalUnchanged() {
+ TextNode fooNode = new TextNode("foo");
+ fooNode.setPrefix(true);
+ fooNode.setVerbatim(true);
+ TextNode barNode = new TextNode(fooNode);
+ barNode.setValue("bar");
+ barNode.setPrefix(false);
+
+ // Check original is unchanged.
+ assertThat(fooNode.getValue()).isEqualTo("foo");
+ assertThat(fooNode.isPrefix()).isTrue();
+ assertThat(fooNode.isVerbatim()).isTrue();
+ // Check that the fields were modified.
+ assertThat(barNode.getValue()).isEqualTo("bar");
+ assertThat(barNode.isPrefix()).isFalse();
+ // Check that fields that weren't set are unmodified.
+ assertThat(barNode.isVerbatim()).isTrue();
+ }
+
+ @Test
+ public void testGetChildren_alwaysReturnEmptyList() {
+ TextNode fooNode = new TextNode("foo");
+ assertThat(fooNode.getChildren().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testConstructor_throwsIfStringNull() {
+ String nullString = null;
+ assertThrows(NullPointerException.class, () -> new TextNode(nullString));
+ }
+
+ @Test
+ public void testCopyConstructor_throwsIfStringNodeNull() {
+ TextNode nullTextNode = null;
+ assertThrows(NullPointerException.class, () -> new TextNode(nullTextNode));
+ }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
index d4856d7..0024d8d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -139,4 +139,11 @@
assertThat(Flags.FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX)
.isEqualTo("com.android.appsearch.flags.enable_enterprise_empty_batch_result_fix");
}
+
+ @Test
+ public void testFlagValue_enableAbstractSyntaxTree() {
+ assertThat(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+ .isEqualTo("com.android.appsearch.flags"
+ + ".enable_abstract_syntax_trees");
+ }
}
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java
similarity index 72%
rename from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
rename to appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java
index c273ad3..3595a7d 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ExperimentalAppSearchApi.java
@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package androidx.appsearch.app;
-package androidx.compose.foundation.layout
+import androidx.annotation.RequiresOptIn;
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+/** Indicates that an AppSearch api is unstable. */
+@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
+public @interface ExperimentalAppSearchApi {}
+
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 006d119..28e9fa2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -36,6 +36,7 @@
import androidx.appsearch.safeparcel.GenericDocumentParcel;
import androidx.appsearch.safeparcel.PropertyParcel;
import androidx.appsearch.util.IndentingStringBuilder;
+import androidx.core.os.ParcelCompat;
import androidx.core.util.Preconditions;
import java.lang.reflect.Array;
@@ -190,9 +191,11 @@
@NonNull
public static GenericDocument createFromParcel(@NonNull Parcel parcel) {
Objects.requireNonNull(parcel);
- return new GenericDocument(
- parcel.readParcelable(
- GenericDocumentParcel.class.getClassLoader(), GenericDocumentParcel.class));
+ GenericDocumentParcel documentParcel =
+ ParcelCompat.readParcelable(
+ parcel, GenericDocumentParcel.class.getClassLoader(),
+ GenericDocumentParcel.class);
+ return new GenericDocument(documentParcel);
}
/**
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 3cabaab..4cb1019 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -105,6 +105,9 @@
READ_ASSISTANT_APP_SEARCH_DATA,
ENTERPRISE_ACCESS,
MANAGED_PROFILE_CONTACTS_ACCESS,
+ EXECUTE_APP_FUNCTIONS,
+ EXECUTE_APP_FUNCTIONS_TRUSTED,
+ PACKAGE_USAGE_STATS,
})
@Retention(RetentionPolicy.SOURCE)
@RequiresFeature(
@@ -190,6 +193,46 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
+ /**
+ * The AppSearch enumeration corresponding to {@link
+ * android.Manifest.permission#EXECUTE_APP_FUNCTIONS} Android permission that can be used to
+ * guard AppSearch schema type visibility in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}.
+ *
+ * <p>This is internally used by AppFunctions API to store app functions runtime metadata so it
+ * is visible to packages holding {@link android.Manifest.permission#EXECUTE_APP_FUNCTIONS}
+ * permission (currently associated with system assistant apps).
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int EXECUTE_APP_FUNCTIONS = 9;
+
+ /**
+ * The AppSearch enumeration corresponding to {@link
+ * android.Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} Android permission that can be
+ * used to guard AppSearch schema type visibility in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}.
+ *
+ * <p>This is internally used by AppFunctions API to store app functions runtime metadata so it
+ * is visible to packages holding {@link
+ * android.Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission (currently associated
+ * with system packages in the {@link android.app.role.SYSTEM_UI_INTELLIGENCE} role).
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10;
+
+ /**
+ * The {@link android.Manifest.permission#PACKAGE_USAGE_STATS} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ *
+ * @exportToFramework:hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int PACKAGE_USAGE_STATS = 11;
+
private final Set<AppSearchSchema> mSchemas;
private final Set<String> mSchemasNotDisplayedBySystem;
private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
@@ -539,7 +582,7 @@
Preconditions.checkNotNull(permissions);
for (int permission : permissions) {
Preconditions.checkArgumentInRange(permission, READ_SMS,
- MANAGED_PROFILE_CONTACTS_ACCESS, "permission");
+ PACKAGE_USAGE_STATS, "permission");
}
resetIfBuilt();
Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/ast/Node.java b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/Node.java
new file mode 100644
index 0000000..876603a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/Node.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.ast;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This is the basic Abstract Syntax Tree (AST) class.
+ * All other classes extend from this class depending on the specific node.
+ *
+ * <p>This API may change in response to feedback and additional changes.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+@ExperimentalAppSearchApi
+public interface Node {
+ /**
+ * Get a list of the node's child {@link Node}s.
+ *
+ * <p>By default this method will return an empty list representing that the node has no
+ * child nodes.
+ *
+ * <p>If a node type extends this interface and has child nodes, then that class
+ * should override this implementation and return a list of nodes of size equal to the number
+ * of child nodes that node has.
+ *
+ * @return An empty list of {@link Node} representing the child nodes.
+ */
+ @NonNull
+ default List<Node> getChildren() {
+ return Collections.emptyList();
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/ast/TextNode.java b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/TextNode.java
new file mode 100644
index 0000000..80f9ab4
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/ast/TextNode.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.ast;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.ExperimentalAppSearchApi;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.core.util.Preconditions;
+
+/**
+ * {@link Node} that stores text.
+ *
+ * <p>Text may represent a string or number.
+ * For example in the query `hello AND "world peace" -cat price:49.99`
+ * <ul>
+ * <li> hello and cat are strings.
+ * <li> "world peace" is a verbatim string, i.e. a quoted string that can be represented by
+ * setting mVerbatim to true. Because it is a verbatim string, it will be treated as a
+ * single term "world peace" instead of terms "world" and "peace".
+ * <li> 49.99 is a number. {@link TextNode}s may represent integers or doubles and treat numbers
+ * as terms.
+ * <li> price is NOT a string but a property path as part of a {@link PropertyRestrictNode}.
+ * </ul>
+ *
+ * <p>The node will be segmented and normalized based on the flags set in the Node.
+ * For example, if the node containing the string "foo" has both mPrefix and mVerbatim set to true,
+ * then the resulting tree will be treated as the query `"foo"*`
+ * i.e. the prefix of the quoted string "foo".
+ *
+ * <p>{@link TextNode}s is guaranteed to not have child nodes.
+ *
+ * <p>This API may change in response to feedback and additional changes.
+ */
+@ExperimentalAppSearchApi
+@FlaggedApi(Flags.FLAG_ENABLE_ABSTRACT_SYNTAX_TREES)
+public final class TextNode implements Node{
+ private String mValue;
+ private boolean mPrefix = false;
+ private boolean mVerbatim = false;
+
+ /**
+ * Public constructor for {@link TextNode} representing text passed into the constructor as a
+ * string.
+ *
+ * <p>By default {@link #mPrefix} and {@link #mVerbatim} are both false. In other words the
+ * {@link TextNode} represents a term that is not the prefix of a potentially longer term that
+ * could be matched against and not a quoted string to be treated as a single term.
+ *
+ * @param value The text value that {@link TextNode} holds.
+ */
+ public TextNode(@NonNull String value) {
+ mValue = Preconditions.checkNotNull(value);
+ }
+
+ /**
+ * Copy constructor that takes in {@link TextNode}.
+ *
+ * @param original The {@link TextNode} to copy and return another {@link TextNode}.
+ */
+ public TextNode(@NonNull TextNode original) {
+ Preconditions.checkNotNull(original);
+ mValue = original.mValue;
+ mPrefix = original.mPrefix;
+ mVerbatim = original.mVerbatim;
+ }
+
+ /**
+ * Retrieve the string value that the TextNode holds.
+ *
+ * @return A string representing the text that the TextNode holds.
+ */
+ @NonNull
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Whether or not a TextNode represents a query term that will match indexed tokens when the
+ * query term is a prefix of the token.
+ *
+ * <p>For example, if the value of the TextNode is "foo" and mPrefix is set to true, then the
+ * TextNode represents the query `foo*`, and will match against tokens like "foo", "foot", and
+ * "football".
+ *
+ * <p>If mPrefix and mVerbatim are both true, then the TextNode represents the prefix of the
+ * quoted string. For example if the value of the TextNode is "foo bar" and both mPrefix and
+ * mVerbatim are set to true, then the TextNode represents the query `"foo bar"*`.
+ *
+ * @return True, if the TextNode represents a query term that will match indexed tokens when the
+ * query term is a prefix of the token.
+ *
+ * <p> False, if the TextNode represents a query term that will only match exact tokens in the
+ * index.
+ */
+ public boolean isPrefix() {
+ return mPrefix;
+ }
+
+ /**
+ * Whether or not a TextNode represents a quoted string.
+ *
+ * <p>For example, if the value of the TextNode is "foo bar" and mVerbatim is set to true, then
+ * the TextNode represents the query `"foo bar"`. "foo bar" will be treated as a single token
+ * and match documents that have a property marked as verbatim and exactly contain
+ * "foo bar".
+ *
+ * <p>If mVerbatim and mPrefix are both true, then the TextNode represents the prefix of the
+ * quoted string. For example if the value of the TextNode is "foo bar" and both mPrefix and
+ * mVerbatim are set to true, then the TextNode represents the query `"foo bar"*`.
+ *
+ * @return True, if the TextNode represents a quoted string. For example, if the value of
+ * TextNode is "foo bar", then the query represented is `"foo bar"`. This means "foo bar" will
+ * be treated as one term, matching documents that have a property marked as verbatim and
+ * contains exactly "foo bar".
+ *
+ * <p> False, if the TextNode does not represent a quoted string. For example, if the value of
+ * TextNode is "foo bar", then the query represented is `foo bar`. This means that "foo" and
+ * "bar" will be treated as separate terms instead of one term and implicitly ANDed, matching
+ * documents that contain both "foo" and "bar".
+ */
+ public boolean isVerbatim() {
+ return mVerbatim;
+ }
+
+ /**
+ * Set the text value that the {@link TextNode} holds.
+ *
+ * @param value The string that the {@link TextNode} will hold.
+ */
+ public void setValue(@NonNull String value) {
+ mValue = Preconditions.checkNotNull(value);
+ }
+
+ /**
+ * Set whether or not the {@link TextNode} represents a prefix. If true, the {@link TextNode}
+ * represents a prefix match for {@code value}.
+ *
+ * @param isPrefix Whether or not the {@link TextNode} represents a prefix. If true, it
+ * represents a query term that will match against indexed tokens when the query
+ * term is a prefix of token.
+ */
+ public void setPrefix(boolean isPrefix) {
+ mPrefix = isPrefix;
+ }
+
+ /**
+ * Set whether or not the {@link TextNode} represents a quoted string, i.e. verbatim. If true,
+ * the {@link TextNode} represents a quoted string.
+ *
+ * @param isVerbatim Whether or not the {@link TextNode} represents a quoted string. If true, it
+ * represents a quoted string.
+ */
+ public void setVerbatim(boolean isVerbatim) {
+ mVerbatim = isVerbatim;
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
index 93d8138..0816e9e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -150,6 +150,10 @@
public static final String FLAG_ENABLE_ENTERPRISE_EMPTY_BATCH_RESULT_FIX =
FLAG_PREFIX + "enable_enterprise_empty_batch_result_fix";
+ /** Enables abstract syntax trees to be built and used within AppSearch. */
+ public static final String FLAG_ENABLE_ABSTRACT_SYNTAX_TREES =
+ FLAG_PREFIX + "enable_abstract_syntax_trees";
+
// Whether the features should be enabled.
//
// In Jetpack, those should always return true.
@@ -276,4 +280,9 @@
public static boolean enableEnterpriseEmptyBatchResultFix() {
return true;
}
+
+ /** Whether AppSearch can create and use abstract syntax trees. */
+ public static boolean enableAbstractSyntaxTrees() {
+ return true;
+ }
}
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 373cd50..f898b04 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -179,6 +179,9 @@
.replace(
'androidx.core.util.ObjectsCompat',
'java.util.Objects')
+ .replace(
+ 'import androidx.core.os.ParcelCompat',
+ 'import android.os.Parcel')
# Preconditions.checkNotNull is replaced with Objects.requireNonNull. We add both
# imports and let google-java-format sort out which one is unused.
.replace(
@@ -196,6 +199,10 @@
contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
contents = re.sub(r'@RequiresFeature\([^)]*\)', '', contents, flags=re.DOTALL)
+ contents = re.sub(
+ r'ParcelCompat\.readParcelable\(.*?([a-zA-Z.()]+),.*?([a-zA-Z.()]+),.*?([a-zA-Z.()]+)\)',
+ r'\1.readParcelable(\2)', contents, flags=re.DOTALL)
+
# Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
# to allow the same documentation to compile for both.
contents = re.sub(r'(#[a-zA-Z0-9_]+)Async}', r'\1}', contents)
diff --git a/arch/core/core-testing/build.gradle b/arch/core/core-testing/build.gradle
index dda5131..f5688ed 100644
--- a/arch/core/core-testing/build.gradle
+++ b/arch/core/core-testing/build.gradle
@@ -32,7 +32,7 @@
api(project(":arch:core:core-runtime"))
api("androidx.annotation:annotation:1.8.1")
api(libs.junit)
- api(libs.mockitoCore, excludes.bytebuddy)
+ api(libs.mockitoCore)
testImplementation(libs.junit)
diff --git a/autofill/autofill/api/1.3.0-beta01.txt b/autofill/autofill/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..0b0a188
--- /dev/null
+++ b/autofill/autofill/api/1.3.0-beta01.txt
@@ -0,0 +1,193 @@
+// Signature format: 4.0
+package androidx.autofill {
+
+ public final class HintConstants {
+ method public static String generateSmsOtpHintForCharacterPosition(int);
+ field public static final String AUTOFILL_HINT_2FA_APP_OTP = "2faAppOTPCode";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_DAY = "birthDateDay";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_FULL = "birthDateFull";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_MONTH = "birthDateMonth";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_YEAR = "birthDateYear";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE = "creditCardExpirationDate";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY = "creditCardExpirationDay";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = "creditCardExpirationMonth";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = "creditCardExpirationYear";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_NUMBER = "creditCardNumber";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
+ field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
+ field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
+ field public static final String AUTOFILL_HINT_GENDER = "gender";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+ field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
+ field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
+ field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
+ field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
+ field public static final String AUTOFILL_HINT_NOT_APPLICABLE = "notApplicable";
+ field public static final String AUTOFILL_HINT_PASSWORD = "password";
+ field public static final String AUTOFILL_HINT_PERSON_NAME = "personName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_FAMILY = "personFamilyName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_GIVEN = "personGivenName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_MIDDLE = "personMiddleName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL = "personMiddleInitial";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_PREFIX = "personNamePrefix";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_SUFFIX = "personNameSuffix";
+ field @Deprecated public static final String AUTOFILL_HINT_PHONE = "phone";
+ field public static final String AUTOFILL_HINT_PHONE_COUNTRY_CODE = "phoneCountryCode";
+ field public static final String AUTOFILL_HINT_PHONE_NATIONAL = "phoneNational";
+ field public static final String AUTOFILL_HINT_PHONE_NUMBER = "phoneNumber";
+ field public static final String AUTOFILL_HINT_PHONE_NUMBER_DEVICE = "phoneNumberDevice";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS = "postalAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_APT_NUMBER = "aptNumber";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY = "addressCountry";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_DEPENDENT_LOCALITY = "dependentLocality";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS = "extendedAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE = "extendedPostalCode";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY = "addressLocality";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_REGION = "addressRegion";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS = "streetAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_CODE = "postalCode";
+ field public static final String AUTOFILL_HINT_PROMO_CODE = "promoCode";
+ field public static final String AUTOFILL_HINT_SMS_OTP = "smsOTPCode";
+ field public static final String AUTOFILL_HINT_UPI_VPA = "upiVirtualPaymentAddress";
+ field public static final String AUTOFILL_HINT_USERNAME = "username";
+ field public static final String AUTOFILL_HINT_WIFI_PASSWORD = "wifiPassword";
+ }
+
+}
+
+package androidx.autofill.inline {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class Renderer {
+ method public static android.app.PendingIntent? getAttributionIntent(android.app.slice.Slice);
+ method public static android.os.Bundle getSupportedInlineUiVersionsAsBundle();
+ method public static android.view.View? render(android.content.Context, android.app.slice.Slice, android.os.Bundle);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class SuggestionHintConstants {
+ field public static final String SUGGESTION_HINT_CLIPBOARD_CONTENT = "clipboardContent";
+ field public static final String SUGGESTION_HINT_SMART_REPLY = "smartReply";
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class UiVersions {
+ method public static java.util.List<java.lang.String!> getVersions(android.os.Bundle);
+ method public static androidx.autofill.inline.UiVersions.StylesBuilder newStylesBuilder();
+ field public static final String INLINE_UI_VERSION_1 = "androidx.autofill.inline.ui.version:v1";
+ }
+
+ public static interface UiVersions.Content {
+ method public android.app.slice.Slice getSlice();
+ }
+
+ public static interface UiVersions.Style {
+ }
+
+ public static final class UiVersions.StylesBuilder {
+ method public androidx.autofill.inline.UiVersions.StylesBuilder addStyle(androidx.autofill.inline.UiVersions.Style);
+ method public android.os.Bundle build();
+ }
+
+}
+
+package androidx.autofill.inline.common {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class ImageViewStyle extends androidx.autofill.inline.common.ViewStyle {
+ }
+
+ public static final class ImageViewStyle.Builder {
+ ctor public ImageViewStyle.Builder();
+ method public androidx.autofill.inline.common.ImageViewStyle build();
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setMaxHeight(int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setMaxWidth(int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setPadding(int, int, int, int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setScaleType(android.widget.ImageView.ScaleType);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setTintList(android.content.res.ColorStateList);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class TextViewStyle extends androidx.autofill.inline.common.ViewStyle {
+ }
+
+ public static final class TextViewStyle.Builder {
+ ctor public TextViewStyle.Builder();
+ method public androidx.autofill.inline.common.TextViewStyle build();
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setPadding(int, int, int, int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextColor(@ColorInt int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextSize(float);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextSize(int, float);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTypeface(String, int);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public class ViewStyle {
+ }
+
+ public static final class ViewStyle.Builder {
+ ctor public ViewStyle.Builder();
+ method public androidx.autofill.inline.common.ViewStyle build();
+ method public androidx.autofill.inline.common.ViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setPadding(int, int, int, int);
+ }
+
+}
+
+package androidx.autofill.inline.v1 {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class InlineSuggestionUi {
+ method public static androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder newContentBuilder(android.app.PendingIntent);
+ method public static androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder newStyleBuilder();
+ }
+
+ public static final class InlineSuggestionUi.Content implements androidx.autofill.inline.UiVersions.Content {
+ method public android.app.PendingIntent? getAttributionIntent();
+ method public CharSequence? getContentDescription();
+ method public android.graphics.drawable.Icon? getEndIcon();
+ method public final android.app.slice.Slice getSlice();
+ method public android.graphics.drawable.Icon? getStartIcon();
+ method public CharSequence? getSubtitle();
+ method public CharSequence? getTitle();
+ }
+
+ public static final class InlineSuggestionUi.Content.Builder {
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content build();
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setContentDescription(CharSequence);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setEndIcon(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setHints(java.util.List<java.lang.String!>);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setStartIcon(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setSubtitle(CharSequence);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setTitle(CharSequence);
+ }
+
+ public static final class InlineSuggestionUi.Style implements androidx.autofill.inline.UiVersions.Style {
+ method public androidx.autofill.inline.common.ViewStyle? getChipStyle();
+ method public androidx.autofill.inline.common.ImageViewStyle? getEndIconStyle();
+ method public int getLayoutDirection();
+ method public androidx.autofill.inline.common.ImageViewStyle? getSingleIconChipIconStyle();
+ method public androidx.autofill.inline.common.ViewStyle? getSingleIconChipStyle();
+ method public androidx.autofill.inline.common.ImageViewStyle? getStartIconStyle();
+ method public androidx.autofill.inline.common.TextViewStyle? getSubtitleStyle();
+ method public androidx.autofill.inline.common.TextViewStyle? getTitleStyle();
+ }
+
+ public static final class InlineSuggestionUi.Style.Builder {
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style build();
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setChipStyle(androidx.autofill.inline.common.ViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setEndIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setLayoutDirection(int);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSingleIconChipIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSingleIconChipStyle(androidx.autofill.inline.common.ViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setStartIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSubtitleStyle(androidx.autofill.inline.common.TextViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setTitleStyle(androidx.autofill.inline.common.TextViewStyle);
+ }
+
+}
+
diff --git a/biometric/biometric-ktx/api/res-current.txt b/autofill/autofill/api/res-1.3.0-beta01.txt
similarity index 100%
copy from biometric/biometric-ktx/api/res-current.txt
copy to autofill/autofill/api/res-1.3.0-beta01.txt
diff --git a/autofill/autofill/api/restricted_1.3.0-beta01.txt b/autofill/autofill/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..2ce5394
--- /dev/null
+++ b/autofill/autofill/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,196 @@
+// Signature format: 4.0
+package androidx.autofill {
+
+ public final class HintConstants {
+ method public static String generateSmsOtpHintForCharacterPosition(int);
+ field public static final String AUTOFILL_HINT_2FA_APP_OTP = "2faAppOTPCode";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_DAY = "birthDateDay";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_FULL = "birthDateFull";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_MONTH = "birthDateMonth";
+ field public static final String AUTOFILL_HINT_BIRTH_DATE_YEAR = "birthDateYear";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE = "creditCardExpirationDate";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY = "creditCardExpirationDay";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = "creditCardExpirationMonth";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR = "creditCardExpirationYear";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_NUMBER = "creditCardNumber";
+ field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
+ field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
+ field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
+ field public static final String AUTOFILL_HINT_GENDER = "gender";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+ field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
+ field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
+ field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
+ field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
+ field public static final String AUTOFILL_HINT_NOT_APPLICABLE = "notApplicable";
+ field public static final String AUTOFILL_HINT_PASSWORD = "password";
+ field public static final String AUTOFILL_HINT_PERSON_NAME = "personName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_FAMILY = "personFamilyName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_GIVEN = "personGivenName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_MIDDLE = "personMiddleName";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL = "personMiddleInitial";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_PREFIX = "personNamePrefix";
+ field public static final String AUTOFILL_HINT_PERSON_NAME_SUFFIX = "personNameSuffix";
+ field @Deprecated public static final String AUTOFILL_HINT_PHONE = "phone";
+ field public static final String AUTOFILL_HINT_PHONE_COUNTRY_CODE = "phoneCountryCode";
+ field public static final String AUTOFILL_HINT_PHONE_NATIONAL = "phoneNational";
+ field public static final String AUTOFILL_HINT_PHONE_NUMBER = "phoneNumber";
+ field public static final String AUTOFILL_HINT_PHONE_NUMBER_DEVICE = "phoneNumberDevice";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS = "postalAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_APT_NUMBER = "aptNumber";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY = "addressCountry";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_DEPENDENT_LOCALITY = "dependentLocality";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS = "extendedAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE = "extendedPostalCode";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY = "addressLocality";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_REGION = "addressRegion";
+ field public static final String AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS = "streetAddress";
+ field public static final String AUTOFILL_HINT_POSTAL_CODE = "postalCode";
+ field public static final String AUTOFILL_HINT_PROMO_CODE = "promoCode";
+ field public static final String AUTOFILL_HINT_SMS_OTP = "smsOTPCode";
+ field public static final String AUTOFILL_HINT_UPI_VPA = "upiVirtualPaymentAddress";
+ field public static final String AUTOFILL_HINT_USERNAME = "username";
+ field public static final String AUTOFILL_HINT_WIFI_PASSWORD = "wifiPassword";
+ }
+
+}
+
+package androidx.autofill.inline {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class Renderer {
+ method public static android.app.PendingIntent? getAttributionIntent(android.app.slice.Slice);
+ method public static android.os.Bundle getSupportedInlineUiVersionsAsBundle();
+ method public static android.view.View? render(android.content.Context, android.app.slice.Slice, android.os.Bundle);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class SuggestionHintConstants {
+ field public static final String SUGGESTION_HINT_CLIPBOARD_CONTENT = "clipboardContent";
+ field public static final String SUGGESTION_HINT_SMART_REPLY = "smartReply";
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class UiVersions {
+ method public static java.util.List<java.lang.String!> getVersions(android.os.Bundle);
+ method public static androidx.autofill.inline.UiVersions.StylesBuilder newStylesBuilder();
+ field public static final String INLINE_UI_VERSION_1 = "androidx.autofill.inline.ui.version:v1";
+ }
+
+ public static interface UiVersions.Content {
+ method public android.app.slice.Slice getSlice();
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @StringDef({androidx.autofill.inline.UiVersions.INLINE_UI_VERSION_1}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface UiVersions.InlineUiVersion {
+ }
+
+ public static interface UiVersions.Style {
+ }
+
+ public static final class UiVersions.StylesBuilder {
+ method public androidx.autofill.inline.UiVersions.StylesBuilder addStyle(androidx.autofill.inline.UiVersions.Style);
+ method public android.os.Bundle build();
+ }
+
+}
+
+package androidx.autofill.inline.common {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class ImageViewStyle extends androidx.autofill.inline.common.ViewStyle {
+ }
+
+ public static final class ImageViewStyle.Builder {
+ ctor public ImageViewStyle.Builder();
+ method public androidx.autofill.inline.common.ImageViewStyle build();
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setMaxHeight(int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setMaxWidth(int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setPadding(int, int, int, int);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setScaleType(android.widget.ImageView.ScaleType);
+ method public androidx.autofill.inline.common.ImageViewStyle.Builder setTintList(android.content.res.ColorStateList);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class TextViewStyle extends androidx.autofill.inline.common.ViewStyle {
+ }
+
+ public static final class TextViewStyle.Builder {
+ ctor public TextViewStyle.Builder();
+ method public androidx.autofill.inline.common.TextViewStyle build();
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setPadding(int, int, int, int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextColor(@ColorInt int);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextSize(float);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTextSize(int, float);
+ method public androidx.autofill.inline.common.TextViewStyle.Builder setTypeface(String, int);
+ }
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public class ViewStyle {
+ }
+
+ public static final class ViewStyle.Builder {
+ ctor public ViewStyle.Builder();
+ method public androidx.autofill.inline.common.ViewStyle build();
+ method public androidx.autofill.inline.common.ViewStyle.Builder setBackground(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setBackgroundColor(@ColorInt int);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setLayoutMargin(int, int, int, int);
+ method public androidx.autofill.inline.common.ViewStyle.Builder setPadding(int, int, int, int);
+ }
+
+}
+
+package androidx.autofill.inline.v1 {
+
+ @RequiresApi(api=android.os.Build.VERSION_CODES.R) public final class InlineSuggestionUi {
+ method public static androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder newContentBuilder(android.app.PendingIntent);
+ method public static androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder newStyleBuilder();
+ }
+
+ public static final class InlineSuggestionUi.Content implements androidx.autofill.inline.UiVersions.Content {
+ method public android.app.PendingIntent? getAttributionIntent();
+ method public CharSequence? getContentDescription();
+ method public android.graphics.drawable.Icon? getEndIcon();
+ method public final android.app.slice.Slice getSlice();
+ method public android.graphics.drawable.Icon? getStartIcon();
+ method public CharSequence? getSubtitle();
+ method public CharSequence? getTitle();
+ }
+
+ public static final class InlineSuggestionUi.Content.Builder {
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content build();
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setContentDescription(CharSequence);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setEndIcon(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setHints(java.util.List<java.lang.String!>);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setStartIcon(android.graphics.drawable.Icon);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setSubtitle(CharSequence);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Content.Builder setTitle(CharSequence);
+ }
+
+ public static final class InlineSuggestionUi.Style implements androidx.autofill.inline.UiVersions.Style {
+ method public androidx.autofill.inline.common.ViewStyle? getChipStyle();
+ method public androidx.autofill.inline.common.ImageViewStyle? getEndIconStyle();
+ method public int getLayoutDirection();
+ method public androidx.autofill.inline.common.ImageViewStyle? getSingleIconChipIconStyle();
+ method public androidx.autofill.inline.common.ViewStyle? getSingleIconChipStyle();
+ method public androidx.autofill.inline.common.ImageViewStyle? getStartIconStyle();
+ method public androidx.autofill.inline.common.TextViewStyle? getSubtitleStyle();
+ method public androidx.autofill.inline.common.TextViewStyle? getTitleStyle();
+ }
+
+ public static final class InlineSuggestionUi.Style.Builder {
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style build();
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setChipStyle(androidx.autofill.inline.common.ViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setEndIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setLayoutDirection(int);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSingleIconChipIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSingleIconChipStyle(androidx.autofill.inline.common.ViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setStartIconStyle(androidx.autofill.inline.common.ImageViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setSubtitleStyle(androidx.autofill.inline.common.TextViewStyle);
+ method public androidx.autofill.inline.v1.InlineSuggestionUi.Style.Builder setTitleStyle(androidx.autofill.inline.common.TextViewStyle);
+ }
+
+}
+
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index 12612a1..028e2dc 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
* should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
*
* <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
- * hints.3
+ * hints.
*/
public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
diff --git a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
index 65975e0..388661e 100644
--- a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
+++ b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
@@ -2,78 +2,6 @@
<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" it in project.properties && project.properties[it].toString().toBoolean()"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_SKIP_GENERATION)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_DONT_DISABLE_RULES)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" project.properties.filterKeys { k ->"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/kotlin/androidx/baselineprofile/gradle/producer/tasks/CollectBaselineProfileTask.kt"/>
- </issue>
-
- <issue
id="InternalAgpApiUsage"
message="Avoid using internal Android Gradle Plugin APIs"
errorLine1="import com.android.build.gradle.internal.api.DefaultAndroidSourceDirectorySet"
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
index c71c8b3..7b4aecf 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPlugin.kt
@@ -231,7 +231,11 @@
newBuildTypePrefix = BUILD_TYPE_BASELINE_PROFILE_PREFIX,
filterBlock = {
// Create baseline profile build types only for non debuggable builds.
- !it.isDebuggable
+ // Note that it's possible to override benchmarkRelease and nonMinifiedRelease,
+ // so we also want to make sure we don't extended these again.
+ !it.isDebuggable &&
+ !it.name.startsWith(BUILD_TYPE_BASELINE_PROFILE_PREFIX) &&
+ !it.name.startsWith(BUILD_TYPE_BENCHMARK_PREFIX)
},
newConfigureBlock = { base, ext ->
@@ -278,7 +282,11 @@
newBuildTypePrefix = BUILD_TYPE_BASELINE_PROFILE_PREFIX,
filterBlock = {
// Create baseline profile build types only for non debuggable builds.
- !it.isDebuggable
+ // Note that it's possible to override benchmarkRelease and nonMinifiedRelease,
+ // so we also want to make sure we don't extended these again.
+ !it.isDebuggable &&
+ !it.name.startsWith(BUILD_TYPE_BASELINE_PROFILE_PREFIX) &&
+ !it.name.startsWith(BUILD_TYPE_BENCHMARK_PREFIX)
},
newConfigureBlock = { base, ext ->
@@ -330,8 +338,13 @@
extendedBuildTypeToOriginalBuildTypeMapping = benchmarkExtendedToOriginalTypeMap,
filterBlock = {
// Create benchmark type for non debuggable types, and without considering
- // baseline profiles build types.
- !it.isDebuggable && it.name !in baselineProfileExtendedToOriginalTypeMap
+ // baseline profiles build types. Note that it's possible to override
+ // benchmarkRelease and nonMinifiedRelease, so we also want to make sure we don't
+ // extended these again.
+ !it.isDebuggable &&
+ it.name !in baselineProfileExtendedToOriginalTypeMap &&
+ !it.name.startsWith(BUILD_TYPE_BASELINE_PROFILE_PREFIX) &&
+ !it.name.startsWith(BUILD_TYPE_BENCHMARK_PREFIX)
},
newConfigureBlock = { base, ext ->
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index 239eff6..aa184ec 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -79,16 +79,16 @@
private val baselineProfileExtension = BaselineProfileProducerExtension.register(project)
private val configurationManager = ConfigurationManager(project)
private val shouldSkipGeneration by lazy {
- project.properties.containsKey(PROP_SKIP_GENERATION)
+ project.providers.gradleProperty(PROP_SKIP_GENERATION).isPresent
}
private val forceOnlyConnectedDevices: Boolean by lazy {
- project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)
+ project.providers.gradleProperty(PROP_FORCE_ONLY_CONNECTED_DEVICES).isPresent
}
private val addEnabledRulesInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_DONT_DISABLE_RULES)
+ !project.providers.gradleProperty(PROP_DONT_DISABLE_RULES).isPresent
}
private val addTargetPackageNameInstrumentationArgument by lazy {
- !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)
+ !project.providers.gradleProperty(PROP_SEND_TARGET_PACKAGE_NAME).isPresent
}
// This maps all the extended build types to the original ones. Note that release does not
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/tasks/CollectBaselineProfileTask.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/tasks/CollectBaselineProfileTask.kt
index dd6adb1..27615f97 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/tasks/CollectBaselineProfileTask.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/tasks/CollectBaselineProfileTask.kt
@@ -84,9 +84,9 @@
// Sets the project testInstrumentationRunnerArguments
it.testInstrumentationRunnerArguments.set(
- project.properties.filterKeys { k ->
- k.startsWith(PROP_KEY_PREFIX_INSTRUMENTATION_RUNNER_ARG)
- }
+ project.providers.gradlePropertiesPrefixedBy(
+ PROP_KEY_PREFIX_INSTRUMENTATION_RUNNER_ARG
+ )
)
// Disables the task if requested
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index cbd7fa7..8638ad0 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -51,12 +51,17 @@
// Properties that can be specified by cmd line using -P<property_name> when invoking gradle.
val testMaxAgpVersion by lazy {
- project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->
- val parts = str.toString().split(".").map { it.toInt() }
+ project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str
+ ->
+ val parts = str.split(".").map { it.toInt() }
return@lazy AndroidPluginVersion(parts[0], parts[1], parts[2])
} ?: return@lazy null
}
+ val suppressWarnings: Boolean by lazy {
+ project.providers.gradleProperty("androidx.baselineprofile.suppresswarnings").isPresent
+ }
+
// Logger
protected val logger = BaselineProfilePluginLogger(project.logger)
@@ -99,6 +104,14 @@
private fun configureWithAndroidPlugin() {
+ fun setWarnings() {
+ if (suppressWarnings) {
+ logger.suppressAllWarnings()
+ } else {
+ getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
+ }
+ }
+
onBeforeFinalizeDsl()
testAndroidComponentExtension()?.let { testComponent ->
@@ -107,7 +120,7 @@
// This can be done only here, since warnings may depend on user configuration
// that is ready only after `finalizeDsl`.
- getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
+ setWarnings()
checkAgpVersion()
}
testComponent.beforeVariants { onTestBeforeVariants(it) }
@@ -123,7 +136,7 @@
// This can be done only here, since warnings may depend on user configuration
// that is ready only after `finalizeDsl`.
- getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
+ setWarnings()
checkAgpVersion()
}
applicationComponent.beforeVariants { onApplicationBeforeVariants(it) }
@@ -139,7 +152,7 @@
// This can be done only here, since warnings may depend on user configuration
// that is ready only after `finalizeDsl`.
- getWarnings()?.let { warnings -> logger.setWarnings(warnings) }
+ setWarnings()
checkAgpVersion()
}
libraryComponent.beforeVariants { onLibraryBeforeVariants(it) }
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfilePluginLogger.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfilePluginLogger.kt
index f41bbb1..daebf6e 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfilePluginLogger.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfilePluginLogger.kt
@@ -31,21 +31,28 @@
maxAgpVersion = false
}
+ private var suppressAllWarnings: Boolean = false
+
fun setWarnings(warnings: Warnings) {
this.warnings = warnings
}
+ fun suppressAllWarnings() {
+ suppressAllWarnings = true
+ }
+
fun debug(message: String) = logger.debug(message)
fun info(message: String) = logger.info(message)
fun warn(property: Warnings.() -> (Boolean), propertyName: String?, message: String) {
+ if (suppressAllWarnings) return
if (property(warnings)) {
logger.warn(message)
if (propertyName != null) {
logger.warn(
"""
-
+
This warning can be disabled setting the following property:
baselineProfile {
warnings {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPluginTest.kt
index f039ec8..eaf3624 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/apptarget/BaselineProfileAppTargetPluginTest.kt
@@ -22,6 +22,7 @@
import androidx.baselineprofile.gradle.utils.TestAgpVersion.TEST_AGP_VERSION_8_1_0
import androidx.baselineprofile.gradle.utils.build
import androidx.baselineprofile.gradle.utils.buildAndAssertThatOutput
+import androidx.baselineprofile.gradle.utils.containsOnly
import com.google.common.truth.Truth.assertThat
import java.io.File
import org.junit.Rule
@@ -44,7 +45,10 @@
"""
.trimIndent()
-private fun createBuildGradle(agpVersion: TestAgpVersion) =
+private fun createBuildGradle(
+ agpVersion: TestAgpVersion,
+ overrideExtendedBuildTypesForRelease: Boolean = false
+) =
"""
import static com.android.build.gradle.internal.ProguardFileType.EXPLICIT;
@@ -56,6 +60,21 @@
android {
namespace 'com.example.namespace'
buildTypes {
+
+ ${
+ if (overrideExtendedBuildTypesForRelease) """
+
+ benchmarkRelease {
+ initWith(release)
+ profileable true
+ }
+ nonMinifiedRelease {
+ initWith(release)
+ }
+
+ """.trimIndent() else ""
+ }
+
anotherRelease {
initWith(release)
minifyEnabled true
@@ -87,8 +106,15 @@
}
}
+ def printVariantsTaskProvider = tasks.register("printVariants", PrintTask) { t ->
+ t.text.set("")
+ }
+
androidComponents {
onVariants(selector()) { variant ->
+ printVariantsTaskProvider.configure { t ->
+ t.text.set(t.text.get() + "\n" + "print-variant:" + variant.name)
+ }
tasks.register(variant.name + "BuildProperties", PrintTask) { t ->
def buildType = android.buildTypes[variant.buildType]
def text = "minifyEnabled=" + buildType.minifyEnabled.toString() + "\n"
@@ -110,76 +136,6 @@
"""
.trimIndent()
-@RunWith(Parameterized::class)
-class BaselineProfileAppTargetPluginTest(agpVersion: TestAgpVersion) {
-
- @get:Rule
- val projectSetup = BaselineProfileProjectSetupRule(forceAgpVersion = agpVersion.versionString)
-
- private val buildGradle = createBuildGradle(agpVersion)
-
- companion object {
- @Parameterized.Parameters(name = "agpVersion={0}")
- @JvmStatic
- fun parameters() = TestAgpVersion.values()
- }
-
- @Test
- fun testSrcSetAreAddedToVariantsForApplications() {
- projectSetup.appTarget.setBuildGradle(buildGradle)
-
- data class TaskAndExpected(val taskName: String, val expectedDirs: List<String>)
-
- arrayOf(
- TaskAndExpected(
- taskName = "nonMinifiedAnotherReleaseJavaSources",
- expectedDirs =
- listOf(
- "src/main/java",
- "src/anotherRelease/java",
- "src/nonMinifiedAnotherRelease/java",
- )
- ),
- TaskAndExpected(
- taskName = "nonMinifiedReleaseJavaSources",
- expectedDirs =
- listOf(
- "src/main/java",
- "src/release/java",
- "src/nonMinifiedRelease/java",
- )
- ),
- TaskAndExpected(
- taskName = "nonMinifiedAnotherReleaseKotlinSources",
- expectedDirs =
- listOf(
- "src/main/kotlin",
- "src/anotherRelease/kotlin",
- "src/nonMinifiedAnotherRelease/kotlin",
- )
- ),
- TaskAndExpected(
- taskName = "nonMinifiedReleaseKotlinSources",
- expectedDirs =
- listOf(
- "src/main/kotlin",
- "src/release/kotlin",
- "src/nonMinifiedRelease/kotlin",
- )
- )
- )
- .forEach { t ->
-
- // Runs the task and assert
- projectSetup.appTarget.gradleRunner.buildAndAssertThatOutput(t.taskName) {
- t.expectedDirs
- .map { File(projectSetup.appTarget.rootDir, it) }
- .forEach { e -> contains(e.absolutePath) }
- }
- }
- }
-}
-
@RunWith(JUnit4::class)
class BaselineProfileAppTargetPluginTestWithAgp80 {
@@ -230,10 +186,25 @@
assertThat(logLine).isNotNull()
}
}
+
+ @Test
+ fun verifyUnitTestDisabled() {
+ projectSetup.appTarget.setBuildGradle(buildGradle)
+ projectSetup.appTarget.gradleRunner.buildAndAssertThatOutput("test", "--dry-run") {
+ contains(":testDebugUnitTest ")
+ contains(":testReleaseUnitTest ")
+ contains(":testAnotherReleaseUnitTest ")
+ doesNotContain(":testNonMinifiedReleaseUnitTest ")
+ doesNotContain(":testNonMinifiedAnotherReleaseUnitTest ")
+ doesNotContain(":testBenchmarkAnotherReleaseUnitTest ")
+ }
+ }
}
@RunWith(Parameterized::class)
-class BaselineProfileAppTargetPluginTestWithAgp81AndAbove(agpVersion: TestAgpVersion) {
+class BaselineProfileAppTargetPluginTestWithAgp81AndAbove(
+ private val agpVersion: TestAgpVersion,
+) {
companion object {
@Parameterized.Parameters(name = "agpVersion={0}")
@@ -247,6 +218,61 @@
private val buildGradle = createBuildGradle(agpVersion)
@Test
+ fun additionalBuildTypesShouldNotBeCreatedForExistingNonMinifiedAndBenchmarkBuildTypes() =
+ arrayOf(
+ true,
+ false,
+ )
+ .forEach { overrideExtendedBuildTypesForRelease ->
+ projectSetup.appTarget.setBuildGradle(
+ buildGradleContent =
+ createBuildGradle(
+ agpVersion = agpVersion,
+ overrideExtendedBuildTypesForRelease =
+ overrideExtendedBuildTypesForRelease,
+ )
+ )
+ projectSetup.appTarget.gradleRunner.build("printVariants") {
+ val variants =
+ it.lines()
+ .filter { l -> l.startsWith("print-variant:") }
+ .map { l -> l.substringAfter("print-variant:").trim() }
+ .toSet()
+ .toList()
+
+ assertThat(
+ variants.containsOnly(
+ "debug",
+ "release",
+ "benchmarkRelease",
+ "nonMinifiedRelease",
+ "anotherRelease",
+ "nonMinifiedAnotherRelease",
+ "benchmarkAnotherRelease",
+ "myCustomRelease",
+ "nonMinifiedMyCustomRelease",
+ "benchmarkMyCustomRelease",
+ )
+ )
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun verifyUnitTestDisabled() {
+ projectSetup.appTarget.setBuildGradle(buildGradle)
+ projectSetup.appTarget.gradleRunner.buildAndAssertThatOutput("test", "--dry-run") {
+ contains(":testDebugUnitTest ")
+ contains(":testReleaseUnitTest ")
+ contains(":testAnotherReleaseUnitTest ")
+ doesNotContain(":testNonMinifiedReleaseUnitTest ")
+ doesNotContain(":testBenchmarkReleaseUnitTest ")
+ doesNotContain(":testNonMinifiedAnotherReleaseUnitTest ")
+ doesNotContain(":testBenchmarkAnotherReleaseUnitTest ")
+ }
+ }
+
+ @Test
fun verifyNewBuildTypes() {
projectSetup.appTarget.setBuildGradle(buildGradle)
@@ -399,7 +425,7 @@
}
@RunWith(Parameterized::class)
-class BaselineProfileAppTargetPluginTestWithAgp80AndAbove(agpVersion: TestAgpVersion) {
+class BaselineProfileAppTargetPluginTestWithAgp80AndAbove(private val agpVersion: TestAgpVersion) {
companion object {
@Parameterized.Parameters(name = "agpVersion={0}")
@@ -413,16 +439,96 @@
private val buildGradle = createBuildGradle(agpVersion)
@Test
- fun verifyUnitTestDisabled() {
+ fun testSrcSetAreAddedToVariantsForApplications() {
projectSetup.appTarget.setBuildGradle(buildGradle)
- projectSetup.appTarget.gradleRunner.buildAndAssertThatOutput("test", "--dry-run") {
- contains(":testDebugUnitTest ")
- contains(":testReleaseUnitTest ")
- contains(":testAnotherReleaseUnitTest ")
- doesNotContain(":testNonMinifiedReleaseUnitTest ")
- doesNotContain(":testBenchmarkReleaseUnitTest ")
- doesNotContain(":testNonMinifiedAnotherReleaseUnitTest ")
- doesNotContain(":testBenchmarkAnotherReleaseUnitTest ")
- }
+
+ data class TaskAndExpected(val taskName: String, val expectedDirs: List<String>)
+
+ arrayOf(
+ TaskAndExpected(
+ taskName = "nonMinifiedAnotherReleaseJavaSources",
+ expectedDirs =
+ listOf(
+ "src/main/java",
+ "src/anotherRelease/java",
+ "src/nonMinifiedAnotherRelease/java",
+ )
+ ),
+ TaskAndExpected(
+ taskName = "nonMinifiedReleaseJavaSources",
+ expectedDirs =
+ listOf(
+ "src/main/java",
+ "src/release/java",
+ "src/nonMinifiedRelease/java",
+ )
+ ),
+ TaskAndExpected(
+ taskName = "nonMinifiedAnotherReleaseKotlinSources",
+ expectedDirs =
+ listOf(
+ "src/main/kotlin",
+ "src/anotherRelease/kotlin",
+ "src/nonMinifiedAnotherRelease/kotlin",
+ )
+ ),
+ TaskAndExpected(
+ taskName = "nonMinifiedReleaseKotlinSources",
+ expectedDirs =
+ listOf(
+ "src/main/kotlin",
+ "src/release/kotlin",
+ "src/nonMinifiedRelease/kotlin",
+ )
+ )
+ )
+ .forEach { t ->
+
+ // Runs the task and assert
+ projectSetup.appTarget.gradleRunner.buildAndAssertThatOutput(t.taskName) {
+ t.expectedDirs
+ .map { File(projectSetup.appTarget.rootDir, it) }
+ .forEach { e -> contains(e.absolutePath) }
+ }
+ }
}
+
+ @Test
+ fun additionalBuildTypesShouldNotBeCreatedForExistingNonMinifiedAndBenchmarkBuildTypes() =
+ arrayOf(
+ true,
+ false,
+ )
+ .forEach { overrideExtendedBuildTypesForRelease ->
+ projectSetup.appTarget.setBuildGradle(
+ buildGradleContent =
+ createBuildGradle(
+ agpVersion = agpVersion,
+ overrideExtendedBuildTypesForRelease =
+ overrideExtendedBuildTypesForRelease,
+ )
+ )
+
+ projectSetup.appTarget.gradleRunner.build("printVariants") {
+ val variants =
+ it.lines()
+ .filter { l -> l.startsWith("print-variant:") }
+ .map { l -> l.substringAfter("print-variant:").trim() }
+ .toSet()
+ .toList()
+
+ assertThat(
+ variants.containsOnly(
+ "debug",
+ "release",
+ "nonMinifiedRelease",
+ "anotherRelease",
+ "nonMinifiedAnotherRelease",
+ "myCustomRelease",
+ "nonMinifiedMyCustomRelease",
+ )
+ )
+ .isTrue()
+ }
+ }
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
index e4f5538..736c8a4 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
@@ -1501,6 +1501,46 @@
}
@Test
+ fun testSuppressWarningWithProperty() {
+ val requiredLines =
+ listOf(
+ "This version of the Baseline Profile Gradle Plugin was tested with versions below",
+ // We skip the lines in between because they may contain changing version numbers.
+ "baselineProfile {",
+ " warnings {",
+ " maxAgpVersion = false",
+ " }",
+ "}"
+ )
+
+ projectSetup.consumer.setup(androidPlugin = ANDROID_APPLICATION_PLUGIN)
+ projectSetup.producer.setupWithoutFlavors(
+ releaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ )
+
+ val gradleCmds =
+ arrayOf(
+ "generateBaselineProfile",
+ "-Pandroidx.benchmark.test.maxagpversion=1.0.0",
+ )
+
+ // Run with no suppress warnings property
+ projectSetup.consumer.gradleRunner.build(*gradleCmds) {
+ val notFound = it.lines().requireInOrder(*requiredLines.toTypedArray())
+ assertThat(notFound).isEmpty()
+ }
+
+ // Run with suppress warnings property
+ projectSetup.consumer.gradleRunner.build(
+ *gradleCmds,
+ "-Pandroidx.baselineprofile.suppresswarnings"
+ ) {
+ val notFound = it.lines().requireInOrder(*requiredLines.toTypedArray())
+ assertThat(notFound).isEqualTo(requiredLines)
+ }
+ }
+
+ @Test
fun testMergeArtAndStartupProfilesShouldDependOnProfileGeneration() {
projectSetup.producer.setupWithFreeAndPaidFlavors(
freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
index 5392918..ee1a2a1 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
@@ -95,6 +95,9 @@
return remaining
}
+internal fun List<String>.containsOnly(vararg strings: String): Boolean =
+ toSet().union(setOf(*strings)).size == this.size
+
fun camelCase(vararg strings: String): String {
if (strings.isEmpty()) return ""
return StringBuilder()
diff --git a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
index 9ce75c2..42b07bb 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
+++ b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="GradleProjectIsolation"
@@ -11,6 +11,15 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.projectDir, // frameworks/support"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithTypeWithoutConfigureEach"
message="Avoid passing a closure to withType, use withType().configureEach instead"
errorLine1=" project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {"
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 4f9ef1f..f8663aa 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -54,10 +54,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 3ffb560..b3181f7 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -69,7 +69,7 @@
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core:1.9.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation("androidx.tracing:tracing-ktx:1.1.0")
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index f98293b..3183dea 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -70,6 +70,9 @@
if (sdkInt in 31..33) {
" Please use profileinstaller `1.2.1`" +
" or newer for API 31-33 support"
+ } else if (sdkInt >= 34) {
+ " Please use profileinstaller `1.4.0`" +
+ " or newer for API 34+ support"
} else {
""
}
diff --git a/benchmark/gradle-plugin/lint-baseline.xml b/benchmark/gradle-plugin/lint-baseline.xml
index 4363bf5..8c91fdc 100644
--- a/benchmark/gradle-plugin/lint-baseline.xml
+++ b/benchmark/gradle-plugin/lint-baseline.xml
@@ -3,27 +3,36 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("lockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of findProperty"
- errorLine1=" project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: """
- errorLine2=" ~~~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" if (!project.rootProject.tasks.exists("unlockClocks")) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index a7bd8e3..8b2eb89 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -101,14 +101,19 @@
extension.buildTypes.named(testBuildType).configure { it.isDefault = true }
if (
- !project.rootProject.hasProperty("android.injected.invoked.from.ide") &&
+ !project.providers.gradleProperty("android.injected.invoked.from.ide").isPresent &&
!testInstrumentationArgs.containsKey("androidx.benchmark.output.enable")
) {
// NOTE: This argument is checked by ResultWriter to enable CI reports.
defaultConfig.testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] =
"true"
- if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
defaultConfig.testInstrumentationRunnerArguments["no-isolated-storage"] = "1"
}
}
@@ -119,7 +124,9 @@
project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {
it.adbPath.set(adbPathProvider)
it.coresArg.set(
- project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: ""
+ project.providers
+ .gradleProperty("androidx.benchmark.lockClocks.cores")
+ .orElse("")
)
}
}
@@ -159,7 +166,12 @@
project.layout.buildDirectory.dir(
"outputs/connected_android_test_additional_output"
)
- if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {
+ if (
+ !project.providers
+ .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+ .getOrElse("false")
+ .toBoolean()
+ ) {
// Only enable pulling benchmark data through this plugin on older versions of
// AGP that do not yet enable this flag.
project.tasks
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh b/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh
index 858a968..bcf9c44 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/disableJit.sh
@@ -27,8 +27,7 @@
else
echo "Could not find adb. Options are:"
echo " 1. Ensure adb is on your \$PATH"
- echo " 2. Use './gradlew lockClocks'"
- echo " 3. Manually adb push this script to your device, and run it there"
+ echo " 2. Manually adb push this script to your device, and run it there"
exit -1
fi
fi
@@ -38,6 +37,7 @@
# require root
if [[ `id` != "uid=0"* ]]; then
echo "Not running as root, cannot disable jit, aborting"
+ echo "Run 'adb root' and retry"
exit -1
fi
@@ -45,7 +45,19 @@
stop
start
+## Poll for boot animation to start...
+echo " Waiting for boot animation to start..."
+while [[ "`getprop init.svc.bootanim`" == "stopped" ]]; do
+ sleep 0.1; # frequent polling for boot anim to start, in case it's fast
+done
+
+## And then complete
+echo " Waiting for boot animation to stop..."
+while [[ "`getprop init.svc.bootanim`" == "running" ]]; do
+ sleep 0.5;
+done
+
DEVICE=`getprop ro.product.device`
-echo "JIT compilation has been disabled on $DEVICE!"
+echo "\nJIT compilation has been disabled on $DEVICE!"
echo "Performance will be terrible for almost everything! (except e.g. AOT benchmarks)"
echo "To reenable it (strongly recommended after benchmarking!!!), reboot or run resetDevice.sh"
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
index 593afb8..fe2169d 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
@@ -57,6 +57,7 @@
# require root
if [[ `id` != "uid=0"* ]]; then
echo "Not running as root, cannot lock clocks, aborting"
+ echo "Run 'adb root' and retry"
exit -1
fi
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh b/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh
index 060f075..8d11421 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/resetDevice.sh
@@ -27,8 +27,7 @@
else
echo "Could not find adb. Options are:"
echo " 1. Ensure adb is on your \$PATH"
- echo " 2. Use './gradlew lockClocks'"
- echo " 3. Manually adb push this script to your device, and run it there"
+ echo " 2. Manually adb push this script to your device, and run it there"
exit -1
fi
fi
diff --git a/biometric/biometric-ktx/OWNERS b/biometric/biometric-ktx/OWNERS
deleted file mode 100644
index 2d066b3..0000000
--- a/biometric/biometric-ktx/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-# Bug component: 483659
[email protected]
[email protected]
\ No newline at end of file
diff --git a/biometric/biometric-ktx/api/api_lint.ignore b/biometric/biometric-ktx/api/api_lint.ignore
deleted file mode 100644
index e9e002b..0000000
--- a/biometric/biometric-ktx/api/api_lint.ignore
+++ /dev/null
@@ -1,73 +0,0 @@
-// Baseline format: 1.0
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricAuthExtensionsKt#startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #2:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #2:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class2BiometricOrCredentialAuthExtensionsKt#startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #7:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricAuthExtensionsKt#authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #7:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `subtitle` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #5:
- Parameter `confirmationRequired` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.Class3BiometricOrCredentialAuthExtensionsKt#startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, CharSequence, boolean, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #6:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.CredentialAuthExtensionsKt#startCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.CredentialAuthExtensionsKt#startCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.CredentialAuthExtensionsKt#startCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #3:
- Parameter `description` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.biometric.auth.CredentialAuthExtensionsKt#startCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject, CharSequence, CharSequence, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback) parameter #4:
- Parameter `executor` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
diff --git a/biometric/biometric-ktx/api/current.txt b/biometric/biometric-ktx/api/current.txt
deleted file mode 100644
index ece90bf..0000000
--- a/biometric/biometric-ktx/api/current.txt
+++ /dev/null
@@ -1,57 +0,0 @@
-// Signature format: 4.0
-package androidx.biometric.auth {
-
- public final class AuthPromptErrorException extends java.lang.Exception {
- ctor public AuthPromptErrorException(int errorCode, CharSequence errorMessage);
- method public int getErrorCode();
- method public CharSequence getErrorMessage();
- property public final int errorCode;
- property public final CharSequence errorMessage;
- }
-
- public final class AuthPromptFailureException extends java.lang.Exception {
- ctor public AuthPromptFailureException();
- }
-
- public final class Class2BiometricAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class Class2BiometricOrCredentialAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class Class3BiometricAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- }
-
- public final class Class3BiometricOrCredentialAuthExtensionsKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class CredentialAuthExtensionsKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.CredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
-}
-
diff --git a/biometric/biometric-ktx/api/restricted_current.txt b/biometric/biometric-ktx/api/restricted_current.txt
deleted file mode 100644
index ece90bf..0000000
--- a/biometric/biometric-ktx/api/restricted_current.txt
+++ /dev/null
@@ -1,57 +0,0 @@
-// Signature format: 4.0
-package androidx.biometric.auth {
-
- public final class AuthPromptErrorException extends java.lang.Exception {
- ctor public AuthPromptErrorException(int errorCode, CharSequence errorMessage);
- method public int getErrorCode();
- method public CharSequence getErrorMessage();
- property public final int errorCode;
- property public final CharSequence errorMessage;
- }
-
- public final class AuthPromptFailureException extends java.lang.Exception {
- ctor public AuthPromptFailureException();
- }
-
- public final class Class2BiometricAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2Biometrics(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.Fragment, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class Class2BiometricOrCredentialAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static suspend Object? authenticateWithClass2BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static androidx.biometric.auth.AuthPrompt startClass2BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class Class3BiometricAuthExtensionsKt {
- method public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method public static androidx.biometric.auth.AuthPrompt authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method public static suspend Object? authenticateWithClass3Biometrics(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, CharSequence negativeButtonText, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- }
-
- public final class Class3BiometricOrCredentialAuthExtensionsKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithClass3BiometricsOrCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startClass3BiometricOrCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? subtitle, optional CharSequence? description, optional boolean confirmationRequired, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
- public final class CredentialAuthExtensionsKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticate(androidx.biometric.auth.CredentialAuthPrompt, androidx.biometric.auth.AuthPromptHost host, androidx.biometric.BiometricPrompt.CryptoObject? crypto, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithCredentials(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static suspend Object? authenticateWithCredentials(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, kotlin.coroutines.Continuation<? super androidx.biometric.BiometricPrompt.AuthenticationResult>);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startCredentialAuthentication(androidx.fragment.app.Fragment, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- method @RequiresApi(android.os.Build.VERSION_CODES.R) public static androidx.biometric.auth.AuthPrompt startCredentialAuthentication(androidx.fragment.app.FragmentActivity, androidx.biometric.BiometricPrompt.CryptoObject? crypto, CharSequence title, optional CharSequence? description, optional java.util.concurrent.Executor? executor, androidx.biometric.auth.AuthPromptCallback callback);
- }
-
-}
-
diff --git a/biometric/biometric-ktx/samples/build.gradle b/biometric/biometric-ktx/samples/build.gradle
deleted file mode 100644
index c750a94..0000000
--- a/biometric/biometric-ktx/samples/build.gradle
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("kotlin-android")
-}
-
-dependencies {
- compileOnly(project(":annotation:annotation-sampled"))
- implementation(project(":biometric:biometric-ktx"))
-}
-
-androidx {
- name = "Biometric Samples"
- type = LibraryType.SAMPLES
- inceptionYear = "2021"
- description = "Contains the sample code for the AndroidX Biometric library"
-}
-
-android {
- compileSdk 35
- namespace "androidx.biometric.samples"
-}
diff --git a/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt b/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt
deleted file mode 100644
index 2aeafe0..0000000
--- a/biometric/biometric-ktx/samples/src/main/java/androidx/biometric/samples/auth/CoroutineSamples.kt
+++ /dev/null
@@ -1,309 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.samples.auth
-
-import android.security.keystore.KeyGenParameterSpec
-import android.security.keystore.KeyProperties
-import androidx.annotation.Sampled
-import androidx.biometric.BiometricPrompt
-import androidx.biometric.auth.AuthPromptErrorException
-import androidx.biometric.auth.AuthPromptFailureException
-import androidx.biometric.auth.AuthPromptHost
-import androidx.biometric.auth.Class2BiometricAuthPrompt
-import androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt
-import androidx.biometric.auth.Class3BiometricAuthPrompt
-import androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt
-import androidx.biometric.auth.CredentialAuthPrompt
-import androidx.biometric.auth.authenticate
-import androidx.fragment.app.Fragment
-import java.nio.charset.Charset
-import java.security.KeyStore
-import javax.crypto.Cipher
-import javax.crypto.KeyGenerator
-import javax.crypto.SecretKey
-
-// Stubbed definitions for samples
-private const val KEYSTORE_INSTANCE = "AndroidKeyStore"
-private const val KEY_NAME = "mySecretKey"
-private const val title = ""
-private const val subtitle = ""
-private const val description = ""
-private const val negativeButtonText = ""
-
-private fun sendEncryptedPayload(payload: ByteArray?): ByteArray? = payload
-
-@Sampled
-suspend fun Fragment.class2BiometricAuth() {
- val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
-
- // Construct AuthPrompt with localized Strings to be displayed to UI.
- val authPrompt =
- Class2BiometricAuthPrompt.Builder(title, negativeButtonText)
- .apply {
- setSubtitle(subtitle)
- setDescription(description)
- setConfirmationRequired(true)
- }
- .build()
-
- try {
- val authResult = authPrompt.authenticate(AuthPromptHost(this))
-
- // Encrypt a payload using the result of crypto-based auth.
- val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
-
- // Use the encrypted payload somewhere interesting.
- sendEncryptedPayload(encryptedPayload)
- } catch (e: AuthPromptErrorException) {
- // Handle irrecoverable error during authentication.
- // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
- // androidx.biometric.BiometricPrompt.AuthenticationError.
- } catch (e: AuthPromptFailureException) {
- // Handle auth failure due biometric credentials being rejected.
- }
-}
-
-@Sampled
-suspend fun Fragment.class2BiometricOrCredentialAuth() {
- val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
-
- // Construct AuthPrompt with localized Strings to be displayed to UI.
- val authPrompt =
- Class2BiometricOrCredentialAuthPrompt.Builder(title)
- .apply {
- setSubtitle(subtitle)
- setDescription(description)
- setConfirmationRequired(true)
- }
- .build()
-
- try {
- val authResult = authPrompt.authenticate(AuthPromptHost(this))
-
- // Encrypt a payload using the result of crypto-based auth.
- val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
-
- // Use the encrypted payload somewhere interesting.
- sendEncryptedPayload(encryptedPayload)
- } catch (e: AuthPromptErrorException) {
- // Handle irrecoverable error during authentication.
- // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
- // androidx.biometric.BiometricPrompt.AuthenticationError.
- } catch (e: AuthPromptFailureException) {
- // Handle auth failure due biometric credentials being rejected.
- }
-}
-
-@Sampled
-@Suppress("NewApi", "ClassVerificationFailure")
-suspend fun Fragment.class3BiometricAuth() {
- // To use Class3 authentication, we need to create a CryptoObject.
- // First create a spec for the key to be generated.
- val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
- val keySpec =
- KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose)
- .apply {
- setBlockModes(KeyProperties.BLOCK_MODE_CBC)
- setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
- setUserAuthenticationRequired(true)
-
- // Require authentication for each use of the key.
- val timeout = 0
- // Set the key type according to the allowed auth types.
- val keyType =
- KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
- setUserAuthenticationParameters(timeout, keyType)
- }
- .build()
-
- // Generate and store the key in the Android keystore.
- KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
- init(keySpec)
- generateKey()
- }
-
- // Prepare the crypto object to use for authentication.
- val cipher =
- Cipher.getInstance(
- "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
- KeyProperties.ENCRYPTION_PADDING_PKCS7
- )
- .apply {
- val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
- init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
- }
-
- val cryptoObject = BiometricPrompt.CryptoObject(cipher)
- val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
-
- // Construct AuthPrompt with localized Strings to be displayed to UI.
- val authPrompt =
- Class3BiometricAuthPrompt.Builder(title, negativeButtonText)
- .apply {
- setSubtitle(subtitle)
- setDescription(description)
- setConfirmationRequired(true)
- }
- .build()
-
- try {
- val authResult = authPrompt.authenticate(AuthPromptHost(this), cryptoObject)
-
- // Encrypt a payload using the result of crypto-based auth.
- val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
-
- // Use the encrypted payload somewhere interesting.
- sendEncryptedPayload(encryptedPayload)
- } catch (e: AuthPromptErrorException) {
- // Handle irrecoverable error during authentication.
- // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
- // androidx.biometric.BiometricPrompt.AuthenticationError.
- } catch (e: AuthPromptFailureException) {
- // Handle auth failure due biometric credentials being rejected.
- }
-}
-
-@Sampled
-@Suppress("NewApi", "ClassVerificationFailure")
-suspend fun Fragment.class3BiometricOrCredentialAuth() {
- // To use Class3 authentication, we need to create a CryptoObject.
- // First create a spec for the key to be generated.
- val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
- val keySpec =
- KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose)
- .apply {
- setBlockModes(KeyProperties.BLOCK_MODE_CBC)
- setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
- setUserAuthenticationRequired(true)
-
- // Require authentication for each use of the key.
- val timeout = 0
- // Set the key type according to the allowed auth types.
- val keyType =
- KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
- setUserAuthenticationParameters(timeout, keyType)
- }
- .build()
-
- // Generate and store the key in the Android keystore.
- KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
- init(keySpec)
- generateKey()
- }
-
- // Prepare the crypto object to use for authentication.
- val cipher =
- Cipher.getInstance(
- "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
- KeyProperties.ENCRYPTION_PADDING_PKCS7
- )
- .apply {
- val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
- init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
- }
-
- val cryptoObject = BiometricPrompt.CryptoObject(cipher)
- val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
-
- // Construct AuthPrompt with localized Strings to be displayed to UI.
- val authPrompt =
- Class3BiometricOrCredentialAuthPrompt.Builder(title)
- .apply {
- setSubtitle(subtitle)
- setDescription(description)
- setConfirmationRequired(true)
- }
- .build()
-
- try {
- val authResult = authPrompt.authenticate(AuthPromptHost(this), cryptoObject)
-
- // Encrypt a payload using the result of crypto-based auth.
- val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
-
- // Use the encrypted payload somewhere interesting.
- sendEncryptedPayload(encryptedPayload)
- } catch (e: AuthPromptErrorException) {
- // Handle irrecoverable error during authentication.
- // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
- // androidx.biometric.BiometricPrompt.AuthenticationError.
- } catch (e: AuthPromptFailureException) {
- // Handle auth failure due biometric credentials being rejected.
- }
-}
-
-@Sampled
-@Suppress("NewApi", "ClassVerificationFailure")
-suspend fun Fragment.credentialAuth() {
- // To use credential authentication, we need to create a CryptoObject.
- // First create a spec for the key to be generated.
- val keyPurpose = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
- val keySpec =
- KeyGenParameterSpec.Builder(KEY_NAME, keyPurpose)
- .apply {
- setBlockModes(KeyProperties.BLOCK_MODE_CBC)
- setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
- setUserAuthenticationRequired(true)
-
- // Require authentication for each use of the key.
- val timeout = 0
- // Set the key type according to the allowed auth type.
- val keyType = KeyProperties.AUTH_DEVICE_CREDENTIAL
- setUserAuthenticationParameters(timeout, keyType)
- }
- .build()
-
- // Generate and store the key in the Android keystore.
- KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_INSTANCE).run {
- init(keySpec)
- generateKey()
- }
-
- // Prepare the crypto object to use for authentication.
- val cipher =
- Cipher.getInstance(
- "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/" +
- KeyProperties.ENCRYPTION_PADDING_PKCS7
- )
- .apply {
- val keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE).apply { load(null) }
- init(Cipher.ENCRYPT_MODE, keyStore.getKey(KEY_NAME, null) as SecretKey)
- }
-
- val cryptoObject = BiometricPrompt.CryptoObject(cipher)
- val payload = "A message to encrypt".toByteArray(Charset.defaultCharset())
-
- // Construct AuthPrompt with localized Strings to be displayed to UI.
- val authPrompt =
- CredentialAuthPrompt.Builder(title).apply { setDescription(description) }.build()
-
- try {
- val authResult = authPrompt.authenticate(AuthPromptHost(this), cryptoObject)
-
- // Encrypt a payload using the result of crypto-based auth.
- val encryptedPayload = authResult.cryptoObject?.cipher?.doFinal(payload)
-
- // Use the encrypted payload somewhere interesting.
- sendEncryptedPayload(encryptedPayload)
- } catch (e: AuthPromptErrorException) {
- // Handle irrecoverable error during authentication.
- // Possible values for AuthPromptErrorException.errorCode are listed in the @IntDef,
- // androidx.biometric.BiometricPrompt.AuthenticationError.
- } catch (e: AuthPromptFailureException) {
- // Handle auth failure due biometric credentials being rejected.
- }
-}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt
deleted file mode 100644
index c1daef7..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptErrorException.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth
-/**
- * Thrown when an unrecoverable error has been encountered and authentication has stopped.
- *
- * @param errorCode An integer ID associated with the error.
- * @param errorMessage A human-readable string that describes the error.
- */
-public class AuthPromptErrorException(
- public val errorCode: Int,
- public val errorMessage: CharSequence
-) : Exception(errorMessage.toString())
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt
deleted file mode 100644
index d105928..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricAuthExtensions.kt
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.biometric.auth
-
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import java.util.concurrent.Executor
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @sample androidx.biometric.samples.auth.class2BiometricAuth
- * @see Class2BiometricAuthPrompt.authenticate(AuthPromptHost, AuthPromptCallback)
- */
-public suspend fun Class2BiometricAuthPrompt.authenticate(
- host: AuthPromptHost,
-): AuthenticationResult {
- return suspendCancellableCoroutine { continuation ->
- val authPrompt =
- startAuthentication(host, Runnable::run, CoroutineAuthPromptCallback(continuation))
-
- continuation.invokeOnCancellation { authPrompt.cancelAuthentication() }
- }
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class2BiometricAuthPrompt
- */
-public fun FragmentActivity.startClass2BiometricAuthentication(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass2BiometricAuthenticationInternal(
- AuthPromptHost(this),
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class2BiometricAuthPrompt
- */
-public suspend fun FragmentActivity.authenticateWithClass2Biometrics(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass2BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return authPrompt.authenticate(AuthPromptHost(this))
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class2BiometricAuthPrompt
- */
-public fun Fragment.startClass2BiometricAuthentication(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass2BiometricAuthenticationInternal(
- AuthPromptHost(this),
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris).
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class2BiometricAuthPrompt
- */
-public suspend fun Fragment.authenticateWithClass2Biometrics(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass2BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return authPrompt.authenticate(AuthPromptHost(this))
-}
-
-/** Creates a [Class2BiometricAuthPrompt] with the given parameters and starts authentication. */
-private fun startClass2BiometricAuthenticationInternal(
- host: AuthPromptHost,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
- executor: Executor?,
- callback: AuthPromptCallback
-): AuthPrompt {
- val prompt =
- buildClass2BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired
- )
-
- return if (executor == null) {
- prompt.startAuthentication(host, callback)
- } else {
- prompt.startAuthentication(host, executor, callback)
- }
-}
-
-/** Creates a [Class2BiometricAuthPrompt] with the given parameters. */
-private fun buildClass2BiometricAuthPrompt(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
-): Class2BiometricAuthPrompt =
- Class2BiometricAuthPrompt.Builder(title, negativeButtonText)
- .apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }
- .build()
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt
deleted file mode 100644
index 996cc32..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthExtensions.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.biometric.auth
-
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import java.util.concurrent.Executor
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @sample androidx.biometric.samples.auth.class2BiometricOrCredentialAuth
- * @see Class2BiometricOrCredentialAuthPrompt.authenticate( AuthPromptHost, AuthPromptCallback )
- */
-public suspend fun Class2BiometricOrCredentialAuthPrompt.authenticate(
- host: AuthPromptHost,
-): AuthenticationResult {
- return suspendCancellableCoroutine { continuation ->
- val authPrompt =
- startAuthentication(host, Runnable::run, CoroutineAuthPromptCallback(continuation))
-
- continuation.invokeOnCancellation { authPrompt.cancelAuthentication() }
- }
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class2BiometricOrCredentialAuthPrompt
- */
-public fun FragmentActivity.startClass2BiometricOrCredentialAuthentication(
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass2BiometricOrCredentialAuthenticationInternal(
- AuthPromptHost(this),
- title,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class2BiometricOrCredentialAuthPrompt
- */
-public suspend fun FragmentActivity.authenticateWithClass2BiometricsOrCredentials(
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass2BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return authPrompt.authenticate(AuthPromptHost(this))
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class2BiometricOrCredentialAuthPrompt
- */
-public fun Fragment.startClass2BiometricOrCredentialAuthentication(
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass2BiometricOrCredentialAuthenticationInternal(
- AuthPromptHost(this),
- title,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 2** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * Note that **Class 3** biometrics are guaranteed to meet the requirements for **Class 2** and thus
- * will also be accepted.
- *
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class2BiometricOrCredentialAuthPrompt
- */
-public suspend fun Fragment.authenticateWithClass2BiometricsOrCredentials(
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass2BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return authPrompt.authenticate(AuthPromptHost(this))
-}
-
-/**
- * Creates a [Class2BiometricOrCredentialAuthPrompt] with the given parameters and starts
- * authentication.
- */
-private fun startClass2BiometricOrCredentialAuthenticationInternal(
- host: AuthPromptHost,
- title: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
- executor: Executor?,
- callback: AuthPromptCallback
-): AuthPrompt {
- val prompt =
- buildClass2BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return if (executor == null) {
- prompt.startAuthentication(host, callback)
- } else {
- prompt.startAuthentication(host, executor, callback)
- }
-}
-
-/** Creates a [Class2BiometricOrCredentialAuthPrompt] with the given parameters. */
-private fun buildClass2BiometricOrCredentialAuthPrompt(
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): Class2BiometricOrCredentialAuthPrompt =
- Class2BiometricOrCredentialAuthPrompt.Builder(title)
- .apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }
- .build()
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt
deleted file mode 100644
index a26b017..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricAuthExtensions.kt
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.biometric.auth
-
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.biometric.BiometricPrompt.CryptoObject
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import java.util.concurrent.Executor
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @sample androidx.biometric.samples.auth.class3BiometricAuth
- * @see Class3BiometricAuthPrompt.authenticate(AuthPromptHost, AuthPromptCallback)
- */
-public suspend fun Class3BiometricAuthPrompt.authenticate(
- host: AuthPromptHost,
- crypto: CryptoObject?,
-): AuthenticationResult {
- return suspendCancellableCoroutine { continuation ->
- val authPrompt =
- startAuthentication(
- host,
- crypto,
- Runnable::run,
- CoroutineAuthPromptCallback(continuation)
- )
-
- continuation.invokeOnCancellation { authPrompt.cancelAuthentication() }
- }
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class3BiometricAuthPrompt
- */
-public fun FragmentActivity.authenticateWithClass3Biometrics(
- crypto: CryptoObject?,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass3BiometricAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class3BiometricAuthPrompt
- */
-public suspend fun FragmentActivity.authenticateWithClass3Biometrics(
- crypto: CryptoObject?,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass3BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired
- )
-
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class3BiometricAuthPrompt
- */
-public fun Fragment.authenticateWithClass3Biometrics(
- crypto: CryptoObject?,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass3BiometricAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris).
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class3BiometricAuthPrompt
- */
-public suspend fun Fragment.authenticateWithClass3Biometrics(
- crypto: CryptoObject?,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass3BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired
- )
-
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/** Creates a [Class3BiometricAuthPrompt] with the given parameters and starts authentication. */
-private fun startClass3BiometricAuthenticationInternal(
- host: AuthPromptHost,
- crypto: CryptoObject?,
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
- executor: Executor?,
- callback: AuthPromptCallback
-): AuthPrompt {
- val prompt =
- buildClass3BiometricAuthPrompt(
- title,
- negativeButtonText,
- subtitle,
- description,
- confirmationRequired
- )
-
- return if (executor == null) {
- prompt.startAuthentication(host, crypto, callback)
- } else {
- prompt.startAuthentication(host, crypto, executor, callback)
- }
-}
-
-/** Creates a [Class3BiometricAuthPrompt] with the given parameters. */
-private fun buildClass3BiometricAuthPrompt(
- title: CharSequence,
- negativeButtonText: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
-): Class3BiometricAuthPrompt =
- Class3BiometricAuthPrompt.Builder(title, negativeButtonText)
- .apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }
- .build()
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt
deleted file mode 100644
index 036c350..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthExtensions.kt
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.biometric.auth
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.biometric.BiometricPrompt.CryptoObject
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import java.util.concurrent.Executor
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @sample androidx.biometric.samples.auth.class3BiometricOrCredentialAuth
- * @see Class3BiometricOrCredentialAuthPrompt.authenticate( AuthPromptHost, AuthPromptCallback )
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun Class3BiometricOrCredentialAuthPrompt.authenticate(
- host: AuthPromptHost,
- crypto: CryptoObject?,
-): AuthenticationResult {
- return suspendCancellableCoroutine { continuation ->
- val authPrompt =
- startAuthentication(
- host,
- crypto,
- Runnable::run,
- CoroutineAuthPromptCallback(continuation)
- )
-
- continuation.invokeOnCancellation { authPrompt.cancelAuthentication() }
- }
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class3BiometricOrCredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public fun FragmentActivity.startClass3BiometricOrCredentialAuthentication(
- crypto: CryptoObject?,
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass3BiometricOrCredentialAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class3BiometricOrCredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun FragmentActivity.authenticateWithClass3BiometricsOrCredentials(
- crypto: CryptoObject?,
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass3BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired
- )
-
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see Class3BiometricOrCredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public fun Fragment.startClass3BiometricOrCredentialAuthentication(
- crypto: CryptoObject?,
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startClass3BiometricOrCredentialAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- subtitle,
- description,
- confirmationRequired,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with a **Class 3** biometric (e.g. fingerprint, face, or iris)
- * or the screen lock credential (i.e. PIN, pattern, or password) for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param subtitle An optional subtitle to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param confirmationRequired Whether user confirmation should be required for passive biometrics.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see Class3BiometricOrCredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun Fragment.authenticateWithClass3BiometricsOrCredentials(
- crypto: CryptoObject?,
- title: CharSequence,
- subtitle: CharSequence? = null,
- description: CharSequence? = null,
- confirmationRequired: Boolean = true,
-): AuthenticationResult {
- val authPrompt =
- buildClass3BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired,
- )
-
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/**
- * Creates a [Class3BiometricOrCredentialAuthPrompt] with the given parameters and starts
- * authentication.
- */
-@RequiresApi(Build.VERSION_CODES.R)
-private fun startClass3BiometricOrCredentialAuthenticationInternal(
- host: AuthPromptHost,
- crypto: CryptoObject?,
- title: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
- executor: Executor?,
- callback: AuthPromptCallback
-): AuthPrompt {
- val prompt =
- buildClass3BiometricOrCredentialAuthPrompt(
- title,
- subtitle,
- description,
- confirmationRequired
- )
-
- return if (executor == null) {
- prompt.startAuthentication(host, crypto, callback)
- } else {
- prompt.startAuthentication(host, crypto, executor, callback)
- }
-}
-
-/** Creates a [Class3BiometricOrCredentialAuthPrompt] with the given parameters. */
-@RequiresApi(Build.VERSION_CODES.R)
-private fun buildClass3BiometricOrCredentialAuthPrompt(
- title: CharSequence,
- subtitle: CharSequence?,
- description: CharSequence?,
- confirmationRequired: Boolean,
-): Class3BiometricOrCredentialAuthPrompt =
- Class3BiometricOrCredentialAuthPrompt.Builder(title)
- .apply {
- subtitle?.let { setSubtitle(it) }
- description?.let { setDescription(it) }
- setConfirmationRequired(confirmationRequired)
- }
- .build()
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt
deleted file mode 100644
index 3d7e72e..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CoroutineAuthPromptCallback.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth
-
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.fragment.app.FragmentActivity
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.CancellableContinuation
-
-/** Implementation of [AuthPromptCallback] used to transform callback results for coroutine APIs. */
-internal class CoroutineAuthPromptCallback(
- private val continuation: CancellableContinuation<AuthenticationResult>
-) : AuthPromptCallback() {
- override fun onAuthenticationError(
- activity: FragmentActivity?,
- errorCode: Int,
- errString: CharSequence
- ) {
- continuation.resumeWithException(AuthPromptErrorException(errorCode, errString))
- }
-
- override fun onAuthenticationSucceeded(
- activity: FragmentActivity?,
- result: AuthenticationResult
- ) {
- continuation.resumeWith(Result.success(result))
- }
-
- override fun onAuthenticationFailed(activity: FragmentActivity?) {
- continuation.resumeWithException(AuthPromptFailureException())
- }
-}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CredentialAuthExtensions.kt b/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CredentialAuthExtensions.kt
deleted file mode 100644
index e5fdf5b..0000000
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CredentialAuthExtensions.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.biometric.auth
-
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.biometric.BiometricPrompt
-import androidx.biometric.BiometricPrompt.AuthenticationResult
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import java.util.concurrent.Executor
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @sample androidx.biometric.samples.auth.credentialAuth
- * @see CredentialAuthPrompt.authenticate( AuthPromptHost host, BiometricPrompt.CryptoObject,
- * AuthPromptCallback )
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun CredentialAuthPrompt.authenticate(
- host: AuthPromptHost,
- crypto: BiometricPrompt.CryptoObject?,
-): AuthenticationResult {
- return suspendCancellableCoroutine { continuation ->
- val authPrompt =
- startAuthentication(
- host,
- crypto,
- Runnable::run,
- CoroutineAuthPromptCallback(continuation)
- )
-
- continuation.invokeOnCancellation { authPrompt.cancelAuthentication() }
- }
-}
-
-/**
- * Prompts the user to authenticate with the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see CredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public fun FragmentActivity.startCredentialAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
- title: CharSequence,
- description: CharSequence? = null,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startCredentialAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- description,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see CredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun FragmentActivity.authenticateWithCredentials(
- crypto: BiometricPrompt.CryptoObject?,
- title: CharSequence,
- description: CharSequence? = null
-): AuthenticationResult {
- val authPrompt = buildCredentialAuthPrompt(title, description)
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/**
- * Prompts the user to authenticate with the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @param executor An executor for [callback] methods. If `null`, these will run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An [AuthPrompt] handle to the shown prompt.
- * @see CredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public fun Fragment.startCredentialAuthentication(
- crypto: BiometricPrompt.CryptoObject?,
- title: CharSequence,
- description: CharSequence? = null,
- executor: Executor? = null,
- callback: AuthPromptCallback
-): AuthPrompt {
- return startCredentialAuthenticationInternal(
- AuthPromptHost(this),
- crypto,
- title,
- description,
- executor,
- callback
- )
-}
-
-/**
- * Prompts the user to authenticate with the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param title The title to be displayed on the prompt.
- * @param description An optional description to be displayed on the prompt.
- * @return [AuthenticationResult] for a successful authentication.
- * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
- * authentication has stopped.
- * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
- * @see CredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public suspend fun Fragment.authenticateWithCredentials(
- crypto: BiometricPrompt.CryptoObject?,
- title: CharSequence,
- description: CharSequence? = null
-): AuthenticationResult {
- val authPrompt = buildCredentialAuthPrompt(title, description)
- return authPrompt.authenticate(AuthPromptHost(this), crypto)
-}
-
-/** Creates a [CredentialAuthPrompt] with the given parameters and starts authentication. */
-@RequiresApi(Build.VERSION_CODES.R)
-private fun startCredentialAuthenticationInternal(
- host: AuthPromptHost,
- crypto: BiometricPrompt.CryptoObject?,
- title: CharSequence,
- description: CharSequence?,
- executor: Executor?,
- callback: AuthPromptCallback
-): AuthPrompt {
- val prompt = buildCredentialAuthPrompt(title, description)
- return if (executor == null) {
- prompt.startAuthentication(host, crypto, callback)
- } else {
- prompt.startAuthentication(host, crypto, executor, callback)
- }
-}
-
-/** Creates a [CredentialAuthPrompt] with the given parameters. */
-@RequiresApi(Build.VERSION_CODES.R)
-private fun buildCredentialAuthPrompt(
- title: CharSequence,
- description: CharSequence?
-): CredentialAuthPrompt =
- CredentialAuthPrompt.Builder(title).apply { description?.let { setDescription(it) } }.build()
diff --git a/biometric/biometric/api/current.txt b/biometric/biometric/api/current.txt
index eb18aba..fccedf3 100644
--- a/biometric/biometric/api/current.txt
+++ b/biometric/biometric/api/current.txt
@@ -151,108 +151,3 @@
}
-package androidx.biometric.auth {
-
- public interface AuthPrompt {
- method public void cancelAuthentication();
- }
-
- public abstract class AuthPromptCallback {
- ctor public AuthPromptCallback();
- method public void onAuthenticationError(androidx.fragment.app.FragmentActivity?, int, CharSequence);
- method public void onAuthenticationFailed(androidx.fragment.app.FragmentActivity?);
- method public void onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity?, androidx.biometric.BiometricPrompt.AuthenticationResult);
- }
-
- public class AuthPromptHost {
- ctor public AuthPromptHost(androidx.fragment.app.Fragment);
- ctor public AuthPromptHost(androidx.fragment.app.FragmentActivity);
- method public androidx.fragment.app.FragmentActivity? getActivity();
- method public androidx.fragment.app.Fragment? getFragment();
- }
-
- public class Class2BiometricAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getNegativeButtonText();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class2BiometricAuthPrompt.Builder {
- ctor public Class2BiometricAuthPrompt.Builder(CharSequence, CharSequence);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt build();
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- public class Class2BiometricOrCredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class2BiometricOrCredentialAuthPrompt.Builder {
- ctor public Class2BiometricOrCredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt build();
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- public class Class3BiometricAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getNegativeButtonText();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class3BiometricAuthPrompt.Builder {
- ctor public Class3BiometricAuthPrompt.Builder(CharSequence, CharSequence);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt build();
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.R) public class Class3BiometricOrCredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class3BiometricOrCredentialAuthPrompt.Builder {
- ctor public Class3BiometricOrCredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt build();
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.R) public class CredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getTitle();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class CredentialAuthPrompt.Builder {
- ctor public CredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.CredentialAuthPrompt build();
- method public androidx.biometric.auth.CredentialAuthPrompt.Builder setDescription(CharSequence);
- }
-
-}
-
diff --git a/biometric/biometric/api/restricted_current.txt b/biometric/biometric/api/restricted_current.txt
index eb18aba..fccedf3 100644
--- a/biometric/biometric/api/restricted_current.txt
+++ b/biometric/biometric/api/restricted_current.txt
@@ -151,108 +151,3 @@
}
-package androidx.biometric.auth {
-
- public interface AuthPrompt {
- method public void cancelAuthentication();
- }
-
- public abstract class AuthPromptCallback {
- ctor public AuthPromptCallback();
- method public void onAuthenticationError(androidx.fragment.app.FragmentActivity?, int, CharSequence);
- method public void onAuthenticationFailed(androidx.fragment.app.FragmentActivity?);
- method public void onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity?, androidx.biometric.BiometricPrompt.AuthenticationResult);
- }
-
- public class AuthPromptHost {
- ctor public AuthPromptHost(androidx.fragment.app.Fragment);
- ctor public AuthPromptHost(androidx.fragment.app.FragmentActivity);
- method public androidx.fragment.app.FragmentActivity? getActivity();
- method public androidx.fragment.app.Fragment? getFragment();
- }
-
- public class Class2BiometricAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getNegativeButtonText();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class2BiometricAuthPrompt.Builder {
- ctor public Class2BiometricAuthPrompt.Builder(CharSequence, CharSequence);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt build();
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class2BiometricAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- public class Class2BiometricOrCredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class2BiometricOrCredentialAuthPrompt.Builder {
- ctor public Class2BiometricOrCredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt build();
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class2BiometricOrCredentialAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- public class Class3BiometricAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getNegativeButtonText();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class3BiometricAuthPrompt.Builder {
- ctor public Class3BiometricAuthPrompt.Builder(CharSequence, CharSequence);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt build();
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class3BiometricAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.R) public class Class3BiometricOrCredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence? getSubtitle();
- method public CharSequence getTitle();
- method public boolean isConfirmationRequired();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class Class3BiometricOrCredentialAuthPrompt.Builder {
- ctor public Class3BiometricOrCredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt build();
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setConfirmationRequired(boolean);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setDescription(CharSequence);
- method public androidx.biometric.auth.Class3BiometricOrCredentialAuthPrompt.Builder setSubtitle(CharSequence);
- }
-
- @RequiresApi(android.os.Build.VERSION_CODES.R) public class CredentialAuthPrompt {
- method public CharSequence? getDescription();
- method public CharSequence getTitle();
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, androidx.biometric.auth.AuthPromptCallback);
- method public androidx.biometric.auth.AuthPrompt startAuthentication(androidx.biometric.auth.AuthPromptHost, androidx.biometric.BiometricPrompt.CryptoObject?, java.util.concurrent.Executor, androidx.biometric.auth.AuthPromptCallback);
- }
-
- public static final class CredentialAuthPrompt.Builder {
- ctor public CredentialAuthPrompt.Builder(CharSequence);
- method public androidx.biometric.auth.CredentialAuthPrompt build();
- method public androidx.biometric.auth.CredentialAuthPrompt.Builder setDescription(CharSequence);
- }
-
-}
-
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index 49370d4..4bd2bd8 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -57,8 +57,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation("androidx.fragment:fragment-testing:1.4.1")
}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java b/biometric/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
index 878dfad..0611d1f2 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
@@ -141,9 +141,6 @@
@Nullable
TextView mHelpMessageView;
- // Prevent direct instantiation.
- private FingerprintDialogFragment() {}
-
/**
* Creates a new instance of {@link FingerprintDialogFragment}.
*
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPrompt.java
deleted file mode 100644
index 108bfd6..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPrompt.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-/**
- * A handle to the prompt that is shown while the user is authenticating.
- *
- * <p>This interface is common across all sub-types of authentication prompts.
- */
-public interface AuthPrompt {
- /**
- * Cancels an ongoing authentication attempt and dismisses the prompt.
- */
- void cancelAuthentication();
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptCallback.java b/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptCallback.java
deleted file mode 100644
index a8f61c0b..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptCallback.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.biometric.BiometricPrompt;
-import androidx.biometric.BiometricPrompt.AuthenticationError;
-import androidx.fragment.app.FragmentActivity;
-
-/**
- * A collection of methods that may be invoked by an auth prompt during authentication.
- *
- * <p>Each method receives a reference to the (possibly {@code null}) activity instance that is
- * currently hosting the prompt. This reference should be used to fetch or update any necessary
- * activity state in order for changes to be reflected across configuration changes.
- */
-public abstract class AuthPromptCallback {
- /**
- * Called when an unrecoverable error has been encountered and authentication has stopped.
- *
- * <p>After this method is called, no further events will be sent for the current
- * authentication session.
- *
- * @param activity The activity that is currently hosting the prompt.
- * @param errorCode An integer ID associated with the error.
- * @param errString A human-readable string that describes the error.
- */
- public void onAuthenticationError(
- @Nullable FragmentActivity activity,
- @AuthenticationError int errorCode,
- @NonNull CharSequence errString) {}
-
- /**
- * Called when the user has successfully authenticated.
- *
- * <p>After this method is called, no further events will be sent for the current
- * authentication session.
- *
- * @param activity The activity that is currently hosting the prompt.
- * @param result An object containing authentication-related data.
- */
- public void onAuthenticationSucceeded(
- @Nullable FragmentActivity activity,
- @NonNull BiometricPrompt.AuthenticationResult result) {}
-
- /**
- * Called when an authentication attempt by the user has been rejected.
- *
- * @param activity The activity that is currently hosting the prompt.
- */
- public void onAuthenticationFailed(@Nullable FragmentActivity activity) {}
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptHost.java b/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptHost.java
deleted file mode 100644
index e8ac2fc..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptHost.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-
-/**
- * A wrapper class for the component that will be used to host an auth prompt.
- */
-public class AuthPromptHost {
- @Nullable private FragmentActivity mActivity;
- @Nullable private Fragment mFragment;
-
- /**
- * Constructs an {@link AuthPromptHost} wrapper for the given activity.
- *
- * @param activity The activity that will host the prompt.
- */
- public AuthPromptHost(@NonNull FragmentActivity activity) {
- mActivity = activity;
- }
-
- /**
- * Constructs an {@link AuthPromptHost} wrapper for the given fragment.
- *
- * @param fragment The fragment that will host the prompt.
- */
- public AuthPromptHost(@NonNull Fragment fragment) {
- mFragment = fragment;
- }
-
- /**
- * Gets the activity that will host the prompt, if set.
- *
- * @return The activity that will host the prompt, or {@code null} if the prompt will be hosted
- * by a different type of component.
- */
- @Nullable
- public FragmentActivity getActivity() {
- return mActivity;
- }
-
- /**
- * Gets the fragment that will host the prompt, if set.
- *
- * @return The fragment that will host the prompt, or {@code null} if the prompt will be hosted
- * by a different type of component.
- */
- @Nullable
- public Fragment getFragment() {
- return mFragment;
- }
-}
\ No newline at end of file
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptUtils.java b/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptUtils.java
deleted file mode 100644
index 1c69754..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/AuthPromptUtils.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.biometric.BiometricPrompt;
-import androidx.biometric.BiometricViewModel;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProvider;
-
-import java.lang.ref.WeakReference;
-import java.util.concurrent.Executor;
-
-/**
- * Utilities used by various auth prompt classes.
- */
-class AuthPromptUtils {
- // Prevent instantiation.
- private AuthPromptUtils() {}
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param executor A custom executor that will be used to run callback methods. If
- * {@code null}, callback methods will be run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- */
- @NonNull
- static AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @NonNull BiometricPrompt.PromptInfo promptInfo,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @Nullable Executor executor,
- @NonNull AuthPromptCallback callback) {
-
- final BiometricPrompt biometricPrompt =
- AuthPromptUtils.createBiometricPrompt(host, executor, callback);
-
- if (crypto == null) {
- biometricPrompt.authenticate(promptInfo);
- } else {
- biometricPrompt.authenticate(promptInfo, crypto);
- }
-
- return new AuthPromptWrapper(biometricPrompt);
- }
-
- /**
- * Creates a {@link BiometricPrompt} with the given parameters.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param executor A custom executor that will be used to run callback methods. If {@code null},
- * callback methods will be run on the main thread.
- * @param callback The object that will receive and process authentication events.
- * @return An instance of {@link BiometricPrompt}.
- *
- * @throws IllegalArgumentException If the given host wrapper does not contain an activity or a
- * fragment that is associated with an activity.
- */
- @NonNull
- private static BiometricPrompt createBiometricPrompt(
- @NonNull AuthPromptHost host,
- @Nullable Executor executor,
- @NonNull AuthPromptCallback callback) {
-
- final Executor executorOrDefault = executor != null ? executor : new DefaultExecutor();
-
- final BiometricPrompt prompt;
- if (host.getActivity() != null) {
- final ViewModelProvider provider = new ViewModelProvider(host.getActivity());
- final AuthenticationCallbackWrapper wrappedCallback = wrapCallback(callback, provider);
- prompt = new BiometricPrompt(host.getActivity(), executorOrDefault, wrappedCallback);
- } else if (host.getFragment() != null && host.getFragment().getActivity() != null) {
- final FragmentActivity activity = host.getFragment().getActivity();
- final ViewModelProvider provider = new ViewModelProvider(activity);
- final AuthenticationCallbackWrapper wrappedCallback = wrapCallback(callback, provider);
- prompt = new BiometricPrompt(host.getFragment(), executorOrDefault, wrappedCallback);
- } else {
- throw new IllegalArgumentException("AuthPromptHost must contain a FragmentActivity or"
- + " an attached Fragment.");
- }
-
- return prompt;
- }
-
- /**
- * Wraps the given callback in a new {@link AuthenticationCallbackWrapper} instance, for
- * compatibility with {@link BiometricPrompt}.
- *
- * @param callback A callback object that is compatible with {@link BiometricPrompt}.
- * @param provider A provider that can be used to get a {@link BiometricViewModel} instance.
- * @return An instance of {@link AuthenticationCallbackWrapper} that wraps the given callback.
- */
- private static AuthenticationCallbackWrapper wrapCallback(
- @NonNull AuthPromptCallback callback, @NonNull ViewModelProvider provider) {
- return new AuthenticationCallbackWrapper(callback, provider.get(BiometricViewModel.class));
- }
-
- /**
- * A wrapper class that provides an {@link AuthPrompt} interface for a {@link BiometricPrompt}.
- */
- private static class AuthPromptWrapper implements AuthPrompt {
- @NonNull private final WeakReference<BiometricPrompt> mBiometricPromptRef;
-
- /**
- * Constructs an {@link AuthPromptWrapper} interface for the given prompt.
- *
- * @param biometricPrompt An instance of {@link BiometricPrompt}.
- */
- AuthPromptWrapper(@NonNull BiometricPrompt biometricPrompt) {
- mBiometricPromptRef = new WeakReference<>(biometricPrompt);
- }
-
- @Override
- public void cancelAuthentication() {
- if (mBiometricPromptRef.get() != null) {
- mBiometricPromptRef.get().cancelAuthentication();
- }
- }
- }
-
- /**
- * The default executor class used to run authentication callback methods on the main thread.
- */
- private static class DefaultExecutor implements Executor {
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- DefaultExecutor() {}
-
- @Override
- public void execute(@NonNull Runnable runnable) {
- mHandler.post(runnable);
- }
- }
-
- /**
- * A wrapper class that provides a {@link BiometricPrompt.AuthenticationCallback} interface for
- * an {@link AuthPromptCallback}.
- */
- private static class AuthenticationCallbackWrapper
- extends BiometricPrompt.AuthenticationCallback {
-
- @NonNull private final AuthPromptCallback mClientCallback;
- @NonNull private final WeakReference<BiometricViewModel> mViewModelRef;
-
- /**
- * Creates an {@link AuthenticationCallbackWrapper} with the given parameters.
- *
- * @param callback A callback object that is compatible with {@link BiometricPrompt}.
- * @param viewModel A {@link BiometricViewModel} that maintains a reference to the host
- * activity across configuration changes.
- */
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- AuthenticationCallbackWrapper(
- @NonNull AuthPromptCallback callback,
- @NonNull BiometricViewModel viewModel) {
- mClientCallback = callback;
- mViewModelRef = new WeakReference<>(viewModel);
- }
-
- @Override
- public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
- mClientCallback.onAuthenticationError(getActivity(mViewModelRef), errorCode, errString);
- }
-
- @Override
- public void onAuthenticationSucceeded(
- @NonNull BiometricPrompt.AuthenticationResult result) {
- mClientCallback.onAuthenticationSucceeded(getActivity(mViewModelRef), result);
- }
-
- @Override
- public void onAuthenticationFailed() {
- mClientCallback.onAuthenticationFailed(getActivity(mViewModelRef));
- }
-
- @Nullable
- private static FragmentActivity getActivity(
- @NonNull WeakReference<BiometricViewModel> viewModelRef) {
- return viewModelRef.get() != null ? viewModelRef.get().getClientActivity() : null;
- }
- }
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricAuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricAuthPrompt.java
deleted file mode 100644
index 7808053..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricAuthPrompt.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.biometric.BiometricManager.Authenticators;
-import androidx.biometric.BiometricPrompt;
-
-import java.util.concurrent.Executor;
-
-/**
- * An authentication prompt that requires the user to present a <strong>Class 2</strong> biometric
- * (e.g. fingerprint, face, or iris).
- *
- * <p>Note that <strong>Class 3</strong> biometrics are guaranteed to meet the requirements for
- * <strong>Class 2</strong> and thus will also be accepted.
- *
- * @see Authenticators#BIOMETRIC_WEAK
- * @see Class2BiometricOrCredentialAuthPrompt
- * @see Class3BiometricAuthPrompt
- * @see Class3BiometricOrCredentialAuthPrompt
- * @see CredentialAuthPrompt
- */
-public class Class2BiometricAuthPrompt {
- @NonNull private final BiometricPrompt.PromptInfo mPromptInfo;
-
- /**
- * Constructs an authentication prompt with the given parameters.
- *
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- */
- Class2BiometricAuthPrompt(@NonNull BiometricPrompt.PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param callback The callback object that will receive and process authentication events.
- * Each callback method will be run on the main thread.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, Executor, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host, @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, null /* crypto */, null /* executor */, callback);
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param executor The executor that will be used to run authentication callback methods.
- * @param callback The callback object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @NonNull Executor executor,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, null /* crypto */, executor, callback);
- }
-
- /**
- * Gets the title to be displayed on the prompt.
- *
- * @return The title for the prompt.
- */
- @NonNull
- public CharSequence getTitle() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the label text for the negative button on the prompt.
- *
- * @return The negative button text for the prompt.
- */
- @NonNull
- public CharSequence getNegativeButtonText() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the subtitle to be displayed on the prompt, if set.
- *
- * @return The subtitle for the prompt.
- *
- * @see Builder#setSubtitle(CharSequence)
- */
- @Nullable
- public CharSequence getSubtitle() {
- return mPromptInfo.getSubtitle();
- }
-
- /**
- * Gets the description to be displayed on the prompt, if set.
- *
- * @return The description for the prompt.
- *
- * @see Builder#setDescription(CharSequence)
- */
- @Nullable
- public CharSequence getDescription() {
- return mPromptInfo.getDescription();
- }
-
- /**
- * Checks if the prompt should require explicit user confirmation after a passive biometric
- * (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity,
- * BiometricPrompt.AuthenticationResult)} is called.
- *
- * @return Whether the prompt should require explicit user confirmation for passive biometrics.
- *
- * @see Builder#setConfirmationRequired(boolean)
- */
- public boolean isConfirmationRequired() {
- return mPromptInfo.isConfirmationRequired();
- }
-
- /**
- * Builder for a {@link Class2BiometricAuthPrompt} with configurable options.
- */
- public static final class Builder {
- // Required fields.
- @NonNull private final CharSequence mTitle;
- @NonNull private final CharSequence mNegativeButtonText;
-
- // Optional fields.
- @Nullable private CharSequence mSubtitle = null;
- @Nullable private CharSequence mDescription = null;
- private boolean mIsConfirmationRequired = true;
-
- /**
- * Constructs a prompt builder with the given required options.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- */
- public Builder(@NonNull CharSequence title, @NonNull CharSequence negativeButtonText) {
- mTitle = title;
- mNegativeButtonText = negativeButtonText;
- }
-
- /**
- * Sets a subtitle that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param subtitle A subtitle for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setSubtitle(@NonNull CharSequence subtitle) {
- mSubtitle = subtitle;
- return this;
- }
-
- /**
- * Sets a description that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param description A description for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setDescription(@NonNull CharSequence description) {
- mDescription = description;
- return this;
- }
-
- /**
- * Sets a hint indicating whether the prompt should require explicit user confirmation
- * after a passive biometric (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(
- * androidx.fragment.app.FragmentActivity, BiometricPrompt.AuthenticationResult)} is
- * called. Defaults to {@code true}.
- *
- * <p>Setting this option to {@code false} is generally only appropriate for frequent,
- * low-value transactions, such as re-authenticating for a previously authorized app.
- *
- * <p>As a hint, the value of this option may be ignored by the system. For example,
- * explicit confirmation may always be required if the user has toggled a system-wide
- * setting to disallow pure passive authentication. This option will also be ignored on any
- * device with an OS version prior to Android 10 (API 29).
- *
- * @param confirmationRequired Whether the prompt should require explicit user confirmation
- * for passive biometrics.
- * @return This builder.
- */
- @NonNull
- public Builder setConfirmationRequired(boolean confirmationRequired) {
- mIsConfirmationRequired = confirmationRequired;
- return this;
- }
-
- /**
- * Creates a new prompt with the specified options.
- *
- * @return An instance of {@link Class2BiometricAuthPrompt}.
- */
- @NonNull
- public Class2BiometricAuthPrompt build() {
- final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
- .setTitle(mTitle)
- .setSubtitle(mSubtitle)
- .setDescription(mDescription)
- .setNegativeButtonText(mNegativeButtonText)
- .setConfirmationRequired(mIsConfirmationRequired)
- .setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK)
- .build();
- return new Class2BiometricAuthPrompt(promptInfo);
- }
- }
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthPrompt.java
deleted file mode 100644
index 7cff2d3..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/Class2BiometricOrCredentialAuthPrompt.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.biometric.BiometricManager.Authenticators;
-import androidx.biometric.BiometricPrompt;
-
-import java.util.concurrent.Executor;
-
-/**
- * An authentication prompt that requires the user to present a <strong>Class 2</strong> biometric
- * (e.g. fingerprint, face, or iris) or the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * <p>Note that <strong>Class 3</strong> biometrics are guaranteed to meet the requirements for
- * <strong>Class 2</strong> and thus will also be accepted.
- *
- * @see Authenticators#BIOMETRIC_WEAK
- * @see Authenticators#DEVICE_CREDENTIAL
- * @see Class2BiometricAuthPrompt
- * @see Class3BiometricAuthPrompt
- * @see Class3BiometricOrCredentialAuthPrompt
- * @see CredentialAuthPrompt
- */
-public class Class2BiometricOrCredentialAuthPrompt {
- @NonNull private final BiometricPrompt.PromptInfo mPromptInfo;
-
- /**
- * Constructs an authentication prompt with the given parameters.
- *
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- */
- Class2BiometricOrCredentialAuthPrompt(@NonNull BiometricPrompt.PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param callback The callback object that will receive and process authentication events.
- * Each callback method will be run on the main thread.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, Executor, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host, @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, null /* crypto */, null /* executor */, callback);
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param executor The executor that will be used to run authentication callback methods.
- * @param callback The callback object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @NonNull Executor executor,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, null /* crypto */, executor, callback);
- }
-
- /**
- * Gets the title to be displayed on the prompt.
- *
- * @return The title for the prompt.
- */
- @NonNull
- public CharSequence getTitle() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the subtitle to be displayed on the prompt, if set.
- *
- * @return The subtitle for the prompt.
- */
- @Nullable
- public CharSequence getSubtitle() {
- return mPromptInfo.getSubtitle();
- }
-
- /**
- * Gets the description to be displayed on the prompt, if set.
- *
- * @return The description for the prompt.
- *
- * @see Builder#setDescription(CharSequence)
- */
- @Nullable
- public CharSequence getDescription() {
- return mPromptInfo.getDescription();
- }
-
- /**
- * Checks if the prompt should require explicit user confirmation after a passive biometric
- * (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity,
- * BiometricPrompt.AuthenticationResult)} is called.
- *
- * @return Whether the prompt should require explicit user confirmation for passive biometrics.
- *
- * @see Builder#setConfirmationRequired(boolean)
- */
- public boolean isConfirmationRequired() {
- return mPromptInfo.isConfirmationRequired();
- }
-
- /**
- * Builder for a {@link Class2BiometricOrCredentialAuthPrompt} with configurable options.
- */
- public static final class Builder {
- // Required fields.
- @NonNull private final CharSequence mTitle;
-
- // Optional fields.
- @Nullable private CharSequence mSubtitle = null;
- @Nullable private CharSequence mDescription = null;
- private boolean mIsConfirmationRequired = true;
-
- /**
- * Constructs a prompt builder with the given required options.
- *
- * @param title The title to be displayed on the prompt.
- */
- public Builder(@NonNull CharSequence title) {
- mTitle = title;
- }
-
- /**
- * Sets a subtitle that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param subtitle A subtitle for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setSubtitle(@NonNull CharSequence subtitle) {
- mSubtitle = subtitle;
- return this;
- }
-
- /**
- * Sets a description that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param description A description for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setDescription(@NonNull CharSequence description) {
- mDescription = description;
- return this;
- }
-
- /**
- * Sets a hint indicating whether the prompt should require explicit user confirmation
- * after a passive biometric (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(
- * androidx.fragment.app.FragmentActivity, BiometricPrompt.AuthenticationResult)} is
- * called. Defaults to {@code true}.
- *
- * <p>Setting this option to {@code false} is generally only appropriate for frequent,
- * low-value transactions, such as re-authenticating for a previously authorized app.
- *
- * <p>As a hint, the value of this option may be ignored by the system. For example,
- * explicit confirmation may always be required if the user has toggled a system-wide
- * setting to disallow pure passive authentication. This option will also be ignored on any
- * device with an OS version prior to Android 10 (API 29).
- *
- * @param confirmationRequired Whether the prompt should require explicit user confirmation
- * for passive biometrics.
- * @return This builder.
- */
- @NonNull
- public Builder setConfirmationRequired(boolean confirmationRequired) {
- mIsConfirmationRequired = confirmationRequired;
- return this;
- }
-
- /**
- * Creates a new prompt with the specified options.
- *
- * @return An instance of {@link Class2BiometricOrCredentialAuthPrompt}.
- */
- @NonNull
- public Class2BiometricOrCredentialAuthPrompt build() {
- final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
- .setTitle(mTitle)
- .setSubtitle(mSubtitle)
- .setDescription(mDescription)
- .setConfirmationRequired(mIsConfirmationRequired)
- .setAllowedAuthenticators(
- Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL)
- .build();
- return new Class2BiometricOrCredentialAuthPrompt(promptInfo);
- }
- }
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricAuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricAuthPrompt.java
deleted file mode 100644
index c8fbca3..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricAuthPrompt.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.biometric.BiometricManager.Authenticators;
-import androidx.biometric.BiometricPrompt;
-
-import java.util.concurrent.Executor;
-
-/**
- * An authentication prompt that requires the user to present a <strong>Class 3</strong> biometric
- * (e.g. fingerprint, face, or iris).
- *
- * @see Authenticators#BIOMETRIC_STRONG
- * @see Class2BiometricAuthPrompt
- * @see Class2BiometricOrCredentialAuthPrompt
- * @see Class3BiometricOrCredentialAuthPrompt
- * @see CredentialAuthPrompt
- */
-public class Class3BiometricAuthPrompt {
- @NonNull private final BiometricPrompt.PromptInfo mPromptInfo;
-
- /**
- * Constructs an authentication prompt with the given parameters.
- *
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- */
- Class3BiometricAuthPrompt(@NonNull BiometricPrompt.PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param callback The callback object that will receive and process authentication events. Each
- * callback method will be run on the main thread.
- * @return A handle to the shown prompt.
- *
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, Executor,
- * AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, null /* executor */, callback);
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param executor The executor that will be used to run authentication callback methods.
- * @param callback The callback object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull Executor executor,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, executor, callback);
- }
-
- /**
- * Gets the title to be displayed on the prompt.
- *
- * @return The title for the prompt.
- */
- @NonNull
- public CharSequence getTitle() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the label text for the negative button on the prompt.
- *
- * @return The negative button text for the prompt.
- */
- @NonNull
- public CharSequence getNegativeButtonText() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the subtitle to be displayed on the prompt, if set.
- *
- * @return The subtitle for the prompt.
- *
- * @see Builder#setSubtitle(CharSequence)
- */
- @Nullable
- public CharSequence getSubtitle() {
- return mPromptInfo.getSubtitle();
- }
-
- /**
- * Gets the description to be displayed on the prompt, if set.
- *
- * @return The description for the prompt.
- *
- * @see Builder#setDescription(CharSequence)
- */
- @Nullable
- public CharSequence getDescription() {
- return mPromptInfo.getDescription();
- }
-
- /**
- * Checks if the prompt should require explicit user confirmation after a passive biometric
- * (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity,
- * BiometricPrompt.AuthenticationResult)} is called.
- *
- * @return Whether the prompt should require explicit user confirmation for passive biometrics.
- *
- * @see Builder#setConfirmationRequired(boolean)
- */
- public boolean isConfirmationRequired() {
- return mPromptInfo.isConfirmationRequired();
- }
-
- /**
- * Builder for a {@link Class3BiometricAuthPrompt} with configurable options.
- */
- public static final class Builder {
- // Required fields.
- @NonNull private final CharSequence mTitle;
- @NonNull private final CharSequence mNegativeButtonText;
-
- // Optional fields.
- @Nullable private CharSequence mSubtitle = null;
- @Nullable private CharSequence mDescription = null;
- private boolean mIsConfirmationRequired = true;
-
- /**
- * Constructs a prompt builder with the given required options.
- *
- * @param title The title to be displayed on the prompt.
- * @param negativeButtonText The label for the negative button on the prompt.
- */
- public Builder(@NonNull CharSequence title, @NonNull CharSequence negativeButtonText) {
- mTitle = title;
- mNegativeButtonText = negativeButtonText;
- }
-
- /**
- * Sets a subtitle that should be displayed on the prompt. Defaults to {@code null}
- *
- * @param subtitle A subtitle for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setSubtitle(@NonNull CharSequence subtitle) {
- mSubtitle = subtitle;
- return this;
- }
-
- /**
- * Sets a description that should be displayed on the prompt. Defaults to {@code null}
- *
- * @param description A description for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setDescription(@NonNull CharSequence description) {
- mDescription = description;
- return this;
- }
-
- /**
- * Sets a hint indicating whether the prompt should require explicit user confirmation
- * after a passive biometric (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(
- * androidx.fragment.app.FragmentActivity, BiometricPrompt.AuthenticationResult)} is
- * called. Defaults to {@code true}.
- *
- * <p>Setting this option to {@code false} is generally only appropriate for frequent,
- * low-value transactions, such as re-authenticating for a previously authorized app.
- *
- * <p>As a hint, the value of this option may be ignored by the system. For example,
- * explicit confirmation may always be required if the user has toggled a system-wide
- * setting to disallow pure passive authentication. This option will also be ignored on any
- * device with an OS version prior to Android 10 (API 29).
- *
- * @param confirmationRequired Whether the prompt should require explicit user confirmation
- * for passive biometrics.
- * @return This builder.
- */
- @NonNull
- public Builder setConfirmationRequired(boolean confirmationRequired) {
- mIsConfirmationRequired = confirmationRequired;
- return this;
- }
-
- /**
- * Creates a new prompt with the specified options.
- *
- * @return An instance of {@link Class3BiometricAuthPrompt}.
- */
- @NonNull
- public Class3BiometricAuthPrompt build() {
- final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
- .setTitle(mTitle)
- .setSubtitle(mSubtitle)
- .setDescription(mDescription)
- .setNegativeButtonText(mNegativeButtonText)
- .setConfirmationRequired(mIsConfirmationRequired)
- .setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG)
- .build();
- return new Class3BiometricAuthPrompt(promptInfo);
- }
- }
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthPrompt.java
deleted file mode 100644
index 2135b57..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/Class3BiometricOrCredentialAuthPrompt.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager.Authenticators;
-import androidx.biometric.BiometricPrompt;
-
-import java.util.concurrent.Executor;
-
-/**
- * An authentication prompt that requires the user to present a <strong>Class 3</strong> biometric
- * (e.g. fingerprint, face, or iris) or the screen lock credential (i.e. PIN, pattern, or password)
- * for the device.
- *
- * @see Authenticators#BIOMETRIC_STRONG
- * @see Authenticators#DEVICE_CREDENTIAL
- * @see Class2BiometricAuthPrompt
- * @see Class2BiometricOrCredentialAuthPrompt
- * @see Class3BiometricAuthPrompt
- * @see CredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public class Class3BiometricOrCredentialAuthPrompt {
- @NonNull private final BiometricPrompt.PromptInfo mPromptInfo;
-
- /**
- * Constructs an authentication prompt with the given parameters.
- *
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- */
- Class3BiometricOrCredentialAuthPrompt(@NonNull BiometricPrompt.PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param callback The callback object that will receive and process authentication events. Each
- * callback method will be run on the main thread.
- * @return A handle to the shown prompt.
- *
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, Executor,
- * AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, null /* executor */, callback);
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param executor The executor that will be used to run authentication callback methods.
- * @param callback The callback object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull Executor executor,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, executor, callback);
- }
-
- /**
- * Gets the title to be displayed on the prompt.
- *
- * @return The title for the prompt.
- */
- @NonNull
- public CharSequence getTitle() {
- return mPromptInfo.getTitle();
- }
-
-
- /**
- * Gets the subtitle to be displayed on the prompt, if set.
- *
- * @return The subtitle for the prompt.
- *
- * @see Builder#setSubtitle(CharSequence)
- */
- @Nullable
- public CharSequence getSubtitle() {
- return mPromptInfo.getSubtitle();
- }
-
- /**
- * Gets the description to be displayed on the prompt, if set.
- *
- * @return The description for the prompt.
- *
- * @see Builder#setDescription(CharSequence)
- */
- @Nullable
- public CharSequence getDescription() {
- return mPromptInfo.getDescription();
- }
-
- /**
- * Checks if the prompt should require explicit user confirmation after a passive biometric
- * (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(androidx.fragment.app.FragmentActivity,
- * BiometricPrompt.AuthenticationResult)} is called.
- *
- * @return Whether the prompt should require explicit user confirmation for passive biometrics.
- *
- * @see Builder#setConfirmationRequired(boolean)
- */
- public boolean isConfirmationRequired() {
- return mPromptInfo.isConfirmationRequired();
- }
-
- /**
- * Builder for a {@link Class3BiometricOrCredentialAuthPrompt} with configurable options.
- */
- public static final class Builder {
- // Required fields.
- @NonNull private final CharSequence mTitle;
-
- // Optional fields.
- @Nullable private CharSequence mSubtitle = null;
- @Nullable private CharSequence mDescription = null;
- private boolean mIsConfirmationRequired = true;
-
- /**
- * Constructs a prompt builder with the given required options.
- *
- * @param title The title to be displayed on the prompt.
- */
- public Builder(@NonNull CharSequence title) {
- mTitle = title;
- }
-
- /**
- * Sets a subtitle that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param subtitle A subtitle for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setSubtitle(
- @NonNull CharSequence subtitle) {
- mSubtitle = subtitle;
- return this;
- }
-
- /**
- * Sets a description that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param description A description for the prompt.
- * @return This builder.
- */
- @NonNull
- public Builder setDescription(
- @NonNull CharSequence description) {
- mDescription = description;
- return this;
- }
-
- /**
- * Sets a hint indicating whether the prompt should require explicit user confirmation
- * after a passive biometric (e.g. iris or face) has been recognized but before
- * {@link AuthPromptCallback#onAuthenticationSucceeded(
- * androidx.fragment.app.FragmentActivity, BiometricPrompt.AuthenticationResult)} is
- * called. Defaults to {@code true}.
- *
- * <p>Setting this option to {@code false} is generally only appropriate for frequent,
- * low-value transactions, such as re-authenticating for a previously authorized app.
- *
- * <p>As a hint, the value of this option may be ignored by the system. For example,
- * explicit confirmation may always be required if the user has toggled a system-wide
- * setting to disallow pure passive authentication. This option will also be ignored on any
- * device with an OS version prior to Android 10 (API 29).
- *
- * @param confirmationRequired Whether the prompt should require explicit user confirmation
- * for passive biometrics.
- * @return This builder.
- */
- @NonNull
- public Builder setConfirmationRequired(boolean confirmationRequired) {
- mIsConfirmationRequired = confirmationRequired;
- return this;
- }
-
- /**
- * Creates a new prompt with the specified options.
- *
- * @return An instance of {@link Class3BiometricOrCredentialAuthPrompt}.
- */
- @NonNull
- public Class3BiometricOrCredentialAuthPrompt build() {
- final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
- .setTitle(mTitle)
- .setSubtitle(mSubtitle)
- .setDescription(mDescription)
- .setConfirmationRequired(mIsConfirmationRequired)
- .setAllowedAuthenticators(
- Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL)
- .build();
- return new Class3BiometricOrCredentialAuthPrompt(promptInfo);
- }
- }
-}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/auth/CredentialAuthPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/auth/CredentialAuthPrompt.java
deleted file mode 100644
index 9bdd483..0000000
--- a/biometric/biometric/src/main/java/androidx/biometric/auth/CredentialAuthPrompt.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.auth;
-
-import android.annotation.SuppressLint;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricManager.Authenticators;
-import androidx.biometric.BiometricPrompt;
-
-import java.util.concurrent.Executor;
-
-/**
- * An authentication prompt that requires the user to present the screen lock credential (i.e. PIN,
- * pattern, or password) for the device.
- *
- * @see Authenticators#DEVICE_CREDENTIAL
- * @see Class2BiometricAuthPrompt
- * @see Class2BiometricOrCredentialAuthPrompt
- * @see Class3BiometricAuthPrompt
- * @see Class3BiometricOrCredentialAuthPrompt
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public class CredentialAuthPrompt {
- @NonNull private final BiometricPrompt.PromptInfo mPromptInfo;
-
- /**
- * Constructs an authentication prompt with the given parameters.
- *
- * @param promptInfo A set of options describing how the prompt should appear and behave.
- */
- CredentialAuthPrompt(@NonNull BiometricPrompt.PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param callback The callback object that will receive and process authentication events. Each
- * callback method will be run on the main thread.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, Executor,
- * AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, null /* executor */, callback);
- }
-
- /**
- * Shows an authentication prompt to the user.
- *
- * @param host A wrapper for the component that will host the prompt.
- * @param crypto A cryptographic object to be associated with this authentication.
- * @param executor The executor that will be used to run authentication callback methods.
- * @param callback The callback object that will receive and process authentication events.
- * @return A handle to the shown prompt.
- *
- * @see #startAuthentication(AuthPromptHost, BiometricPrompt.CryptoObject, AuthPromptCallback)
- */
- @NonNull
- public AuthPrompt startAuthentication(
- @NonNull AuthPromptHost host,
- @Nullable BiometricPrompt.CryptoObject crypto,
- @NonNull Executor executor,
- @NonNull AuthPromptCallback callback) {
- return AuthPromptUtils.startAuthentication(
- host, mPromptInfo, crypto, executor, callback);
- }
-
- /**
- * Gets the title to be displayed on the prompt.
- *
- * @return The title for the prompt.
- */
- @NonNull
- public CharSequence getTitle() {
- return mPromptInfo.getTitle();
- }
-
- /**
- * Gets the description to be displayed on the prompt, if set.
- *
- * @return The description for the prompt.
- *
- * @see Builder#setDescription(CharSequence)
- */
- @Nullable
- public CharSequence getDescription() {
- return mPromptInfo.getDescription();
- }
-
- /**
- * Builder for a {@link CredentialAuthPrompt} with configurable options.
- */
- public static final class Builder {
- // Required fields.
- @NonNull private final CharSequence mTitle;
-
- // Optional fields.
- @Nullable private CharSequence mDescription = null;
-
- /**
- * Constructs a prompt builder with the given required options.
- *
- * @param title The title to be displayed on the prompt.
- */
- @SuppressLint("ExecutorRegistration")
- public Builder(@NonNull CharSequence title) {
- mTitle = title;
- }
-
- /**
- * Sets a description that should be displayed on the prompt. Defaults to {@code null}.
- *
- * @param description A description for the prompt.
- * @return This builder.
- */
- @NonNull
- public CredentialAuthPrompt.Builder setDescription(@NonNull CharSequence description) {
- mDescription = description;
- return this;
- }
-
- /**
- * Creates a new prompt with the specified options.
- *
- * @return An instance of {@link CredentialAuthPrompt}.
- */
- @NonNull
- public CredentialAuthPrompt build() {
- final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
- .setTitle(mTitle)
- .setDescription(mDescription)
- .setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL)
- .build();
- return new CredentialAuthPrompt(promptInfo);
- }
- }
-}
diff --git a/biometric/integration-tests/testapp/build.gradle b/biometric/integration-tests/testapp/build.gradle
index d435a00..ae40263 100644
--- a/biometric/integration-tests/testapp/build.gradle
+++ b/biometric/integration-tests/testapp/build.gradle
@@ -41,7 +41,7 @@
dependencies {
implementation("androidx.annotation:annotation:1.8.1")
- implementation(project(":biometric:biometric-ktx"))
+ implementation(project(":biometric:biometric"))
implementation("androidx.activity:activity-ktx:1.1.0")
implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.fragment:fragment-ktx:1.2.5")
diff --git a/biometric/integration-tests/testapp/src/main/AndroidManifest.xml b/biometric/integration-tests/testapp/src/main/AndroidManifest.xml
index 6d3cb6c..fe79a63 100644
--- a/biometric/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/biometric/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -41,12 +41,5 @@
android:exported="false"
android:theme="@style/Theme.AppCompat.Light"
android:screenOrientation="fullSensor" />
-
- <activity
- android:name=".AuthPromptTestActivity"
- android:label="@string/auth_prompt_test_title"
- android:exported="false"
- android:theme="@style/Theme.AppCompat.Light"
- android:screenOrientation="fullSensor" />
</application>
</manifest>
\ No newline at end of file
diff --git a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt b/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt
deleted file mode 100644
index 21bc918..0000000
--- a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/AuthPromptTestActivity.kt
+++ /dev/null
@@ -1,295 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.biometric.integration.testapp
-
-import android.os.Build
-import android.os.Bundle
-import android.widget.TextView
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricManager.Authenticators
-import androidx.biometric.BiometricPrompt
-import androidx.biometric.auth.AuthPrompt
-import androidx.biometric.auth.AuthPromptCallback
-import androidx.biometric.auth.authenticateWithClass3Biometrics
-import androidx.biometric.auth.startClass2BiometricAuthentication
-import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
-import androidx.biometric.auth.startClass3BiometricOrCredentialAuthentication
-import androidx.biometric.auth.startCredentialAuthentication
-import androidx.biometric.integration.testapp.R.string.biometric_prompt_description
-import androidx.biometric.integration.testapp.R.string.biometric_prompt_negative_text
-import androidx.biometric.integration.testapp.R.string.biometric_prompt_subtitle
-import androidx.biometric.integration.testapp.R.string.biometric_prompt_title
-import androidx.biometric.integration.testapp.databinding.AuthPromptTestActivityBinding
-import androidx.fragment.app.FragmentActivity
-import java.nio.charset.Charset
-
-/** Interactive test activity for the [androidx.biometric.auth] APIs. */
-class AuthPromptTestActivity : FragmentActivity() {
- private lateinit var binding: AuthPromptTestActivityBinding
-
- /** A handle to the prompt for an ongoing authentication session. */
- private var authPrompt: AuthPrompt? = null
-
- /** A bit field representing the currently allowed authenticator type(s). */
- private val allowedAuthenticators: Int
- get() {
- var authenticators = 0
-
- if (
- binding.class3BiometricButton.isChecked ||
- binding.class3BiometricOrCredentialButton.isChecked
- ) {
- authenticators = authenticators or Authenticators.BIOMETRIC_STRONG
- }
-
- if (
- binding.class2BiometricButton.isChecked ||
- binding.class2BiometricOrCredentialButton.isChecked
- ) {
- authenticators = authenticators or Authenticators.BIOMETRIC_WEAK
- }
-
- if (
- binding.class2BiometricOrCredentialButton.isChecked ||
- binding.class3BiometricOrCredentialButton.isChecked ||
- binding.credentialButton.isChecked
- ) {
- authenticators = authenticators or Authenticators.DEVICE_CREDENTIAL
- }
-
- return authenticators
- }
-
- /** Whether the selected options allow for biometric authentication. */
- private val isBiometricAllowed: Boolean
- get() {
- return binding.class2BiometricButton.isChecked ||
- binding.class2BiometricOrCredentialButton.isChecked ||
- binding.class3BiometricButton.isChecked ||
- binding.class3BiometricOrCredentialButton.isChecked
- }
-
- /** Whether the selected options allow for device credential authentication. */
- private val isCredentialAllowed: Boolean
- get() {
- return binding.class2BiometricOrCredentialButton.isChecked ||
- binding.class3BiometricOrCredentialButton.isChecked ||
- binding.credentialButton.isChecked
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = AuthPromptTestActivityBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- // Disallow unsupported authentication type combinations.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
- binding.class3BiometricOrCredentialButton.isEnabled = false
- binding.credentialButton.isEnabled = false
- }
-
- // Crypto-based authentication is not supported prior to Android 6.0 (API 23).
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- binding.common.useCryptoAuthCheckbox.isEnabled = false
- }
-
- // Set button callbacks.
- binding.authTypeGroup.setOnCheckedChangeListener { _, checkedId ->
- updateCryptoCheckboxState(checkedId)
- }
- binding.common.canAuthenticateButton.setOnClickListener { canAuthenticate() }
- binding.common.authenticateButton.setOnClickListener { authenticate() }
- binding.common.clearLogButton.setOnClickListener { clearLog() }
-
- // Restore logged messages on activity recreation (e.g. due to device rotation).
- if (savedInstanceState != null) {
- binding.common.logTextView.text = savedInstanceState.getCharSequence(KEY_LOG_TEXT, "")
- }
- }
-
- override fun onStop() {
- super.onStop()
-
- // If option is selected, dismiss the prompt on rotation.
- if (binding.common.cancelConfigChangeCheckbox.isChecked && isChangingConfigurations) {
- authPrompt?.cancelAuthentication()
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
-
- // Save the current log messages to be restored on activity recreation.
- outState.putCharSequence(KEY_LOG_TEXT, binding.common.logTextView.text)
- }
-
- /**
- * Updates the state of the crypto-based auth checkbox when a given [checkedId] is selected from
- * the authentication types radio group.
- */
- private fun updateCryptoCheckboxState(checkedId: Int) {
- // Crypto-based authentication is not supported prior to Android 6.0 (API 23).
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- return
- }
-
- val isCheckboxEnabled =
- checkedId != R.id.class2_biometric_button &&
- checkedId != R.id.class2_biometric_or_credential_button
-
- binding.common.useCryptoAuthCheckbox.isEnabled = isCheckboxEnabled
- if (!isCheckboxEnabled) {
- binding.common.useCryptoAuthCheckbox.isChecked = false
- }
- }
-
- /** Logs the authentication status given by [BiometricManager.canAuthenticate]. */
- private fun canAuthenticate() {
- val result = BiometricManager.from(this).canAuthenticate(allowedAuthenticators)
- log("canAuthenticate: ${result.toAuthenticationStatusString()}")
- }
-
- /** Launches the appropriate [AuthPrompt] to begin authentication. */
- private fun authenticate() {
- val title = getString(biometric_prompt_title)
- val subtitle = getString(biometric_prompt_subtitle)
- val description = getString(biometric_prompt_description)
- val negativeButtonText = getString(biometric_prompt_negative_text)
- val confirmationRequired = binding.common.requireConfirmationCheckbox.isChecked
- val callback = AuthCallback()
-
- authPrompt =
- when (val buttonId = binding.authTypeGroup.checkedRadioButtonId) {
- R.id.class2_biometric_button ->
- startClass2BiometricAuthentication(
- title = title,
- negativeButtonText = negativeButtonText,
- callback = callback
- )
- R.id.class3_biometric_button ->
- authenticateWithClass3Biometrics(
- crypto = createCryptoOrNull(),
- title = title,
- subtitle = subtitle,
- description = description,
- negativeButtonText = negativeButtonText,
- callback = callback
- )
- R.id.class2_biometric_or_credential_button ->
- startClass2BiometricOrCredentialAuthentication(
- title = title,
- subtitle = subtitle,
- description = description,
- confirmationRequired = confirmationRequired,
- callback = callback
- )
- R.id.class3_biometric_or_credential_button ->
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- startClass3BiometricOrCredentialAuthentication(
- crypto = createCryptoOrNull(),
- title = title,
- subtitle = subtitle,
- description = description,
- confirmationRequired = confirmationRequired,
- callback = callback
- )
- } else {
- val sdkInt = Build.VERSION.SDK_INT
- log(
- "Error: Class 3 biometric or credential auth not supported on API $sdkInt."
- )
- null
- }
- R.id.credential_button ->
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- startCredentialAuthentication(
- crypto = createCryptoOrNull(),
- title = title,
- description = description,
- callback = callback
- )
- } else {
- val sdkInt = Build.VERSION.SDK_INT
- log("Error: Credential-only auth not supported on API $sdkInt.")
- null
- }
- else -> throw IllegalStateException("Invalid checked button ID: $buttonId")
- }
- }
-
- /** Returns a new crypto object for authentication or `null`, based on the selected options. */
- private fun createCryptoOrNull(): BiometricPrompt.CryptoObject? {
- return if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
- binding.common.useCryptoAuthCheckbox.isChecked
- ) {
- createCryptoObject(isBiometricAllowed, isCredentialAllowed)
- } else {
- null
- }
- }
-
- /** Logs a new [message] to the in-app [TextView]. */
- internal fun log(message: CharSequence) {
- binding.common.logTextView.prependLogMessage(message)
- }
-
- /** Clears all logged messages from the in-app [TextView]. */
- private fun clearLog() {
- binding.common.logTextView.text = ""
- }
-
- /** Sample callback that logs all authentication events. */
- private class AuthCallback : AuthPromptCallback() {
- override fun onAuthenticationError(
- activity: FragmentActivity?,
- errorCode: Int,
- errString: CharSequence
- ) {
- super.onAuthenticationError(activity, errorCode, errString)
- if (activity is AuthPromptTestActivity) {
- activity.log("onAuthenticationError $errorCode: $errString")
- }
- }
-
- override fun onAuthenticationSucceeded(
- activity: FragmentActivity?,
- result: BiometricPrompt.AuthenticationResult
- ) {
- super.onAuthenticationSucceeded(activity, result)
- if (activity is AuthPromptTestActivity) {
- activity.log("onAuthenticationSucceeded: ${result.toDataString()}")
-
- // Encrypt a test payload using the result of crypto-based auth.
- if (activity.binding.common.useCryptoAuthCheckbox.isChecked) {
- val encryptedPayload =
- result.cryptoObject
- ?.cipher
- ?.doFinal(PAYLOAD.toByteArray(Charset.defaultCharset()))
- activity.log("Encrypted payload: ${encryptedPayload?.contentToString()}")
- }
- }
- }
-
- override fun onAuthenticationFailed(activity: FragmentActivity?) {
- super.onAuthenticationFailed(activity)
- if (activity is AuthPromptTestActivity) {
- activity.log("onAuthenticationFailed")
- }
- }
- }
-}
diff --git a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/MainActivity.kt b/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/MainActivity.kt
index 72e402c..0e725fe 100644
--- a/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/MainActivity.kt
+++ b/biometric/integration-tests/testapp/src/main/java/androidx/biometric/integration/testapp/MainActivity.kt
@@ -16,6 +16,7 @@
package androidx.biometric.integration.testapp
+import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.biometric.integration.testapp.databinding.MainActivityBinding
@@ -32,11 +33,10 @@
// Set button callbacks.
binding.biometricPromptButton.setOnClickListener { launch<BiometricPromptTestActivity>() }
- binding.authPromptButton.setOnClickListener { launch<AuthPromptTestActivity>() }
}
/** Launches an instance of the given test activity [T]. */
- private inline fun <reified T : FragmentActivity> launch() {
+ private inline fun <reified T : Activity> launch() {
startActivity(Intent(this, T::class.java))
}
}
diff --git a/biometric/integration-tests/testapp/src/main/res/layout/auth_prompt_test_activity.xml b/biometric/integration-tests/testapp/src/main/res/layout/auth_prompt_test_activity.xml
deleted file mode 100644
index 2017fca..0000000
--- a/biometric/integration-tests/testapp/src/main/res/layout/auth_prompt_test_activity.xml
+++ /dev/null
@@ -1,81 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2019 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<ScrollView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- style="@style/ScreenLayout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <TextView
- style="@style/LabelText"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/auth_type_label" />
-
- <RadioGroup
- android:id="@+id/auth_type_group"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:checkedButton="@+id/class2_biometric_button">
-
- <RadioButton
- style="@style/LabelText"
- android:id="@+id/class2_biometric_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/class2_biometric_label"/>
-
- <RadioButton
- style="@style/LabelText"
- android:id="@+id/class3_biometric_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/class3_biometric_label"/>
-
- <RadioButton
- style="@style/LabelText"
- android:id="@+id/class2_biometric_or_credential_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/class2_biometric_or_credential_label" />
-
- <RadioButton
- style="@style/LabelText"
- android:id="@+id/class3_biometric_or_credential_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/class3_biometric_or_credential_label" />
-
- <RadioButton
- style="@style/LabelText"
- android:id="@+id/credential_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/credential_label" />
- </RadioGroup>
-
- <include android:id="@+id/common" layout="@layout/common_section" />
-
- </LinearLayout>
-</ScrollView>
\ No newline at end of file
diff --git a/biometric/integration-tests/testapp/src/main/res/layout/main_activity.xml b/biometric/integration-tests/testapp/src/main/res/layout/main_activity.xml
index 47876a7..7e320a0 100644
--- a/biometric/integration-tests/testapp/src/main/res/layout/main_activity.xml
+++ b/biometric/integration-tests/testapp/src/main/res/layout/main_activity.xml
@@ -33,11 +33,4 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/biometric_prompt_test_title" />
-
- <Button
- android:id="@+id/auth_prompt_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/auth_prompt_test_title" />
-
</LinearLayout>
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 06d7941..7df667d 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -123,6 +123,7 @@
method public android.os.Bundle? extraCommand(String, android.os.Bundle?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
+ method public static boolean isSetNetworkSupported(android.content.Context, String);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index b39f101..26fd37d 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -134,6 +134,7 @@
method public android.os.Bundle? extraCommand(String, android.os.Bundle?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
+ method public static boolean isSetNetworkSupported(android.content.Context, String);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
diff --git a/browser/browser/build.gradle b/browser/browser/build.gradle
index 9af6f1b..7606121 100644
--- a/browser/browser/build.gradle
+++ b/browser/browser/build.gradle
@@ -51,8 +51,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
}
diff --git a/browser/browser/src/androidTest/AndroidManifest.xml b/browser/browser/src/androidTest/AndroidManifest.xml
index 7ca9246..38f43cd 100644
--- a/browser/browser/src/androidTest/AndroidManifest.xml
+++ b/browser/browser/src/androidTest/AndroidManifest.xml
@@ -92,6 +92,16 @@
</service>
<service
+ android:name="androidx.browser.customtabs.TestCustomTabsServiceSetNetwork"
+ android:enabled="false"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.support.customtabs.action.CustomTabsService" />
+ <category android:name="androidx.browser.customtabs.category.SetNetwork" />
+ </intent-filter>
+ </service>
+
+ <service
android:name="androidx.browser.customtabs.PostMessageService"
android:enabled="false"
android:exported="true" />
@@ -128,4 +138,4 @@
</intent-filter>
</service>
</application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsClientTest.java b/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsClientTest.java
new file mode 100644
index 0000000..fe67547
--- /dev/null
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsClientTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.customtabs;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests for {@link CustomTabsClient}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CustomTabsClientTest {
+ private static final String TEST_CUSTOM_TABS_PROVIDER = "androidx.browser.test";
+ private static final String TEST_NONEXISTENT_CUSTOM_TABS_PROVIDER =
+ "androidx.browser.nonexistent";
+
+ @Rule
+ public final EnableComponentsTestRule mEnableComponents = new EnableComponentsTestRule(
+ TestCustomTabsServiceSetNetwork.class
+ );
+
+ private Context mContext;
+
+ @Before
+ public void setup() {
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Test
+ public void testCustomTabsServiceCategorySetNetwork() {
+ // Specify the package name of androidTest suite that can handle the CustomTabsService
+ // action, CustomTabsClient.getPackageName will return the package name of test suite
+ // instead of the default browser package name if any installed in the target device, it
+ // may or may not have the SET_NETWORK category. So we always specify with the package name
+ // of androidTest to avoid the flakiness.
+ List<String> packages = Collections.singletonList(TEST_CUSTOM_TABS_PROVIDER);
+ String provider =
+ CustomTabsClient.getPackageName(mContext, packages, true /* ignoreDefault */);
+ assertNotNull(provider);
+ assertTrue(CustomTabsClient.isSetNetworkSupported(mContext, provider));
+ }
+
+ @Test
+ public void testCustomTabsServiceCategorySetNetwork_intentFilterCategoryDoesNotMatch() {
+ // Disable the TestCustomTabsServiceSetNetwork service and enable
+ // TestCustomTabsServiceSupportsTwas service intentionally, which doesn't offer
+ // the CATEGORY_SET_NETWORK, check for the support should fail.
+ mEnableComponents.manuallyDisable(TestCustomTabsServiceSetNetwork.class);
+ mEnableComponents.manuallyEnable(TestCustomTabsServiceSupportsTwas.class);
+
+ List<String> packages = Collections.singletonList(TEST_CUSTOM_TABS_PROVIDER);
+ String provider =
+ CustomTabsClient.getPackageName(mContext, packages, true /* ignoreDefault */);
+ assertNotNull(provider);
+ assertFalse(CustomTabsClient.isSetNetworkSupported(mContext, provider));
+ }
+
+ @Test
+ public void testCustomTabsServiceCategorySetNetwork_packageNameDoesNotMatch() {
+ assertFalse(CustomTabsClient.isSetNetworkSupported(mContext,
+ TEST_NONEXISTENT_CUSTOM_TABS_PROVIDER));
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSetNetwork.java
similarity index 72%
copy from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
copy to browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSetNetwork.java
index c273ad3..7d0f9a3 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsServiceSetNetwork.java
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.browser.customtabs;
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+/**
+ * A {@link TestCustomTabsService} that supports multi-network (i.e. SetNetwork intent filter
+ * category)
+ */
+public class TestCustomTabsServiceSetNetwork extends TestCustomTabsService {}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
index b71d475..45b003d 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -20,9 +20,11 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -469,4 +471,32 @@
public CustomTabsSession attachSession(@NonNull CustomTabsSession.PendingSession session) {
return newSessionInternal(session.getCallback(), session.getId());
}
+
+ /**
+ * Check whether the Custom Tabs provider supports multi-network feature {@link
+ * CustomTabsIntent.Builder#setNetwork}, i.e. be able to bind a custom tab to a
+ * particular network.
+ *
+ * @param context Application context.
+ * @param provider the package name of Custom Tabs provider.
+ * @return whether a Custom Tabs provider supports multi-network feature.
+ * @see CustomTabsIntent.Builder#setNetwork and CustomTabsService#CATEGORY_SET_NETWORK.
+ */
+ public static boolean isSetNetworkSupported(@NonNull Context context,
+ @NonNull String provider) {
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> services = pm.queryIntentServices(
+ new Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION),
+ PackageManager.GET_RESOLVED_FILTER);
+ for (ResolveInfo service : services) {
+ ServiceInfo serviceInfo = service.serviceInfo;
+ if (serviceInfo != null && provider.equals(serviceInfo.packageName)) {
+ IntentFilter filter = service.filter;
+ if (filter != null && filter.hasCategory(CustomTabsService.CATEGORY_SET_NETWORK)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index cf8f78f..acb410e 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -1452,6 +1452,10 @@
* default network is a cellular connection. All URLRequests created in the future via this
* tab will be bound to {@link Network}.
*
+ * If the browser does not support this feature it will be ignored and a Custom Tab will
+ * be opened using the default network. Check the support by calling {@link
+ * CustomTabsClient#isSetNetworkSupported}.
+ *
* @param network {@link Network} the target network to be bound.
* @see CustomTabsIntent#EXTRA_NETWORK
*/
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index 4ec3c2a..2ce6b17 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -221,7 +221,7 @@
public void prefetch(@NonNull ICustomTabsCallback callback, @NonNull Uri url,
@NonNull Bundle options) {
CustomTabsService.this.prefetch(
- new CustomTabsSessionToken(callback, null), url,
+ new CustomTabsSessionToken(callback, getSessionIdFromBundle(options)), url,
PrefetchOptions.fromBundle(options));
}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index ca297bb..505dffc 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -128,8 +128,9 @@
@ExperimentalPrefetch
@SuppressWarnings("NullAway") // TODO: b/142938599
public void prefetch(@NonNull Uri url, @NonNull PrefetchOptions options) {
+ Bundle optionsWithId = createBundleWithId(options.toBundle());
try {
- mService.prefetch(mCallback, url, options.toBundle());
+ mService.prefetch(mCallback, url, optionsWithId);
} catch (RemoteException e) {
return;
}
@@ -146,9 +147,10 @@
@ExperimentalPrefetch
@SuppressWarnings("NullAway") // TODO: b/142938599
public void prefetch(@NonNull List<Uri> urls, @NonNull PrefetchOptions options) {
+ Bundle optionsWithId = createBundleWithId(options.toBundle());
try {
for (Uri uri : urls) {
- mService.prefetch(mCallback, uri, options.toBundle());
+ mService.prefetch(mCallback, uri, optionsWithId);
}
} catch (RemoteException e) {
return;
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 235650f..8282927 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -138,6 +138,69 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val extensions = project.rootProject.extensions"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val compilerProject = project.rootProject.resolveProject(":compose")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.layout.buildDirectory"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return File(rootProject.projectDir, "../../external").canonicalFile"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" for (propertyName in project.properties.keys) {"
errorLine2=" ~~~~~~~~~~">
@@ -147,11 +210,137 @@
<issue
id="GradleProjectIsolation"
- message="Use providers.gradleProperty instead of getProperties"
- errorLine1=" if (properties.containsKey("android.injected.invoked.from.ide")) {"
- errorLine2=" ~~~~~~~~~~">
+ message="Avoid using method getRootProject"
+ errorLine1=" AndroidXPlaygroundRootImplPlugin.projectOrArtifact(rootProject, this)"
+ errorLine2=" ~~~~~~~~~~~">
<location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXRootImplPlugin.kt"/>
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" allProjectsExist || findProject(otherGradlePath) != null"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir == project.getSupportRootFolder()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType<NodeJsRootExtension>()?.version = getVersionByName("node")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.extensions.findByType(YarnRootExtension::class.java)?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.register("createYarnRcFile", CreateYarnRcFileTask::class.java) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.yarnrcFile.set(rootProject.layout.buildDirectory.file("js/.yarnrc"))"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.withType<KotlinNpmInstallTask>().configureEach {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val requested = rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("test-xml-configs")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.layout.buildDirectory.dir("privacysandbox-files")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val actualRootProject = if (project.isRoot) project else project.rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" get() = this == rootProject"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/gradle/Extensions.kt"/>
</issue>
<issue
@@ -174,6 +363,15 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/FilteredAnchorTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use providers.gradleProperty instead of getProperties"
errorLine1=" task.pathPrefix = properties[PROP_PATH_PREFIX] as String"
errorLine2=" ~~~~~~~~~~">
@@ -191,6 +389,204 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" it.parameters.workingDir.set(rootProject.layout.projectDirectory)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/gitclient/GitClient.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" ${project.parent}."
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.findProject(path)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.findProject(":lint:lint-gradle")?.let {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val tasksByOutput = project.rootProject.findAllTasksByOutput()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ListTaskOutputsTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" project.findProject(projectPath)?.plugins?.let { plugins ->"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/MavenUploadHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks.named(CREATE_MODULE_INFO).configure {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/OwnersService.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.gradle.sharedServices.registerIfAbsent("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ProjectParser.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" regenerate(project.rootProject, groupId, artifactId, version, location)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.getRepositoryDirectory()"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" return project.rootProject.maybeRegister("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val localPropsFile = rootProject.projectDir.resolve("local.properties")"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkHelper.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.rootDir.toRelativeString(project.projectDir)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkResourceGenerator.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" (project.rootProject.extensions.extraProperties).let { it.get("supportRootFolder") as File }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/studio/StudioTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" rootProject.tasks"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" val parentProject = project.parent!!"
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" project.rootProject.tasks.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!.dependsOn(task)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+ </issue>
+
+ <issue
id="InternalAgpApiUsage"
message="Avoid using internal Android Gradle Plugin APIs"
errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
@@ -382,8 +778,8 @@
<issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
- errorLine1=" .withPluginClasspath()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ errorLine1=" .withPluginClasspath()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt"/>
</issue>
diff --git a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
index c9c28a6..b0ffec5 100644
--- a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
@@ -16,6 +16,7 @@
package androidx.build.buildInfo
+import androidx.build.PlatformIdentifier
import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.asBuildInfoDependencies
import androidx.build.jetpad.LibraryBuildInfoFile
import androidx.testutils.gradle.ProjectSetupRule
@@ -96,12 +97,27 @@
}
@Test
- fun resolveTarget_succeeds() {
- assertThat(resolveTarget(false)).isEqualTo("androidx")
- assertThat(resolveTarget(true)).isEqualTo("androidx_multiplatform_mac")
+ fun hasApplePlatform_withAtLeastOnePlatformIdentifierTargetingAnApplePlatform_returnsTrue() {
+ val platforms =
+ setOf(
+ PlatformIdentifier.ANDROID,
+ PlatformIdentifier.IOS_ARM_64,
+ PlatformIdentifier.JVM,
+ )
+ assertThat(hasApplePlatform(platforms)).isTrue()
}
- fun setupBuildInfoProject() {
+ @Test
+ fun hasApplePlatform_withNoPlatformIdentifiersTargetingAnApplePlatform_returnsFalse() {
+ val platforms =
+ setOf(
+ PlatformIdentifier.ANDROID,
+ PlatformIdentifier.JVM,
+ )
+ assertThat(hasApplePlatform(platforms)).isFalse()
+ }
+
+ private fun setupBuildInfoProject() {
projectSetup.writeDefaultBuildGradle(
prefix =
"""
@@ -147,7 +163,8 @@
it.artifactId,
project.provider { "fakeSha" },
false,
- false
+ false,
+ "androidx"
)
}
}
diff --git a/buildSrc/dependencies.gradle b/buildSrc/dependencies.gradle
index 23c0358..48fa25e 100644
--- a/buildSrc/dependencies.gradle
+++ b/buildSrc/dependencies.gradle
@@ -16,10 +16,6 @@
// Add ext.libs for library versions
def excludes = [:]
-excludes.bytebuddy = {
- exclude group: "net.bytebuddy"
-}
-
excludes.espresso = {
exclude group: "androidx.annotation"
exclude group: "androidx.appcompat"
diff --git a/buildSrc/ndk.gradle b/buildSrc/ndk.gradle
new file mode 100644
index 0000000..ee863e5
--- /dev/null
+++ b/buildSrc/ndk.gradle
@@ -0,0 +1,3 @@
+android {
+ ndkVersion = "27.0.12077973"
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index cd8fc46..ac255a3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -34,9 +34,8 @@
import androidx.build.sbom.configureSbomPublishing
import androidx.build.sbom.validateAllArchiveInputsRecognized
import androidx.build.studio.StudioTask
-import androidx.build.testConfiguration.ModuleInfoGenerator
-import androidx.build.testConfiguration.TestModule
import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.addToModuleInfo
import androidx.build.testConfiguration.configureTestConfigGeneration
import androidx.build.uptodatedness.TaskUpToDateValidator
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
@@ -306,16 +305,7 @@
val xmlReportDestDir = project.getHostTestResultDirectory()
val testName = "${project.path}:${task.name}"
- project.rootProject.tasks.named("createModuleInfo").configure {
- it as ModuleInfoGenerator
- it.testModules.add(
- TestModule(
- name = testName,
- path =
- listOf(project.projectDir.toRelativeString(project.getSupportRootFolder()))
- )
- )
- }
+ project.addToModuleInfo(testName)
val archiveName = "$testName.zip"
if (project.isDisplayTestOutput()) {
// Enable tracing to see results in command line
@@ -771,20 +761,7 @@
private fun Project.buildOnServerDependsOnLint() {
if (!project.usingMaxDepVersions()) {
- val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java)
- androidComponents?.onVariants { variant ->
- if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
- val taskName =
- "lint${variant.name.replaceFirstChar {
- if (it.isLowerCase()) {
- it.titlecase(Locale.getDefault())
- } else {
- it.toString()
- }
- }}"
- project.addToBuildOnServer(taskName)
- }
- }
+ project.addToBuildOnServer("lint")
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index 607b2c3..ac34ccd 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -528,6 +528,8 @@
watchosX64(block),
watchosArm32(block),
watchosArm64(block),
+ // TODO: enable this once all the libraries are ready to use it.
+ // watchosDeviceArm64(block),
watchosSimulatorArm64(block)
)
}
@@ -553,6 +555,16 @@
}
@JvmOverloads
+ fun watchosDeviceArm64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
+ supportedPlatforms.add(PlatformIdentifier.WATCHOS_DEVICE_ARM_64)
+ return if (project.enableMac()) {
+ kotlinExtension.watchosDeviceArm64 { block?.execute(this) }
+ } else {
+ null
+ }
+ }
+
+ @JvmOverloads
fun watchosX64(block: Action<KotlinNativeTarget>? = null): KotlinNativeTarget? {
supportedPlatforms.add(PlatformIdentifier.WATCHOS_X_64)
return if (project.enableMac()) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index e777875..ba8881f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -61,7 +61,7 @@
// If we're running inside Studio, validate the Android Gradle Plugin version.
val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
- if (properties.containsKey("android.injected.invoked.from.ide")) {
+ if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) {
if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) {
throw GradleException(
"""
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
index f632090..73aca82 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -62,7 +62,10 @@
* match the requested path prefix and task name.
*/
internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
- if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+ if (
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent &&
+ providers.gradleProperty(PROP_TASK_NAME).isPresent
+ ) {
val pathPrefixes = (properties[PROP_PATH_PREFIX] as String).split(",")
if (pathPrefixes.any { pathPrefix -> relativePathForFiltering().startsWith(pathPrefix) }) {
val taskName = properties[PROP_TASK_NAME] as String
@@ -84,7 +87,10 @@
* -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
*/
internal fun Project.maybeRegisterFilterableTask() {
- if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+ if (
+ providers.gradleProperty(PROP_TASK_NAME).isPresent &&
+ providers.gradleProperty(PROP_PATH_PREFIX).isPresent
+ ) {
tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
task.pathPrefix = properties[PROP_PATH_PREFIX] as String
task.taskName = properties[PROP_TASK_NAME] as String
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index c215eea..0b0ffd7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -29,7 +29,10 @@
import java.io.File
import java.io.StringWriter
import org.dom4j.Element
+import org.dom4j.Namespace
+import org.dom4j.QName
import org.dom4j.io.XMLWriter
+import org.dom4j.tree.DefaultText
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.XmlProvider
@@ -273,6 +276,10 @@
}
}
+private val ARTIFACT_ID = QName("artifactId", Namespace("", "http://maven.apache.org/POM/4.0.0"))
+
+private fun Element.textElements() = content().filterIsInstance<DefaultText>()
+
/** Looks for a dependencies XML element within [pom] and sorts its contents. */
fun sortPomDependencies(pom: String): String {
// Workaround for using the default namespace in dom4j.
@@ -284,7 +291,16 @@
element ->
val deps = element.elements()
val sortedDeps = deps.toSortedSet(compareBy { it.stringValue }).toList()
-
+ sortedDeps.map { // b/356612738 https://github.com/gradle/gradle/issues/30112
+ val itsArtifactId = it.element(ARTIFACT_ID)
+ if (itsArtifactId.stringValue.endsWith("-debug")) {
+ itsArtifactId.textElements().last().text =
+ itsArtifactId.textElements().last().text.removeSuffix("-debug")
+ } else if (itsArtifactId.stringValue.endsWith("-release")) {
+ itsArtifactId.textElements().last().text =
+ itsArtifactId.textElements().last().text.removeSuffix("-release")
+ }
+ }
// Content contains formatting nodes, so to avoid modifying those we replace
// each element with the sorted element from its respective index. Note this
// will not move adjacent elements, so any comments would remain in their
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
index d7c6057..3cd6611 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
@@ -19,6 +19,8 @@
import androidx.build.AndroidXExtension
import androidx.build.AndroidXMultiplatformExtension
import androidx.build.LibraryGroup
+import androidx.build.PlatformGroup
+import androidx.build.PlatformIdentifier
import androidx.build.addToBuildOnServer
import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.TASK_NAME
import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.requiresDocs
@@ -120,7 +122,7 @@
val outputDir = resolvedOutputFile.parentFile
if (!outputDir.exists()) {
if (!outputDir.mkdirs()) {
- throw RuntimeException("Failed to create " + "output directory: $outputDir")
+ throw RuntimeException("Failed to create output directory: $outputDir")
}
}
if (!resolvedOutputFile.exists()) {
@@ -269,6 +271,20 @@
val anchorTask = tasks.register("${TASK_NAME}Anchor")
addToBuildOnServer(anchorTask)
configure<PublishingExtension> {
+
+ /**
+ * Select the appropriate target based on if the project targets any Apple platforms
+ *
+ * If the project targets any Apple platform then the project can only be built on the
+ * 'androidx_multiplatform_mac' target. Otherwise the 'androidx' build target is used.
+ */
+ val buildTarget =
+ if (hasApplePlatform(androidXKmpExtension.supportedPlatforms)) {
+ "androidx_multiplatform_mac"
+ } else {
+ "androidx"
+ }
+
// Unfortunately, dependency information is only available through internal API
// (See https://github.com/gradle/gradle/issues/21345).
publications.withType(MavenPublicationInternal::class.java).configureEach { mavenPub ->
@@ -282,6 +298,7 @@
artifactId = mavenPub.artifactId,
shouldPublishDocs = androidXExtension.requiresDocs(),
isKmp = androidXKmpExtension.supportedPlatforms.isNotEmpty(),
+ buildTarget = buildTarget,
)
}
}
@@ -296,6 +313,7 @@
artifactId: String,
shouldPublishDocs: Boolean,
isKmp: Boolean,
+ buildTarget: String,
) {
val task =
createBuildInfoTask(
@@ -304,7 +322,8 @@
artifactId,
getHeadShaProvider(project),
shouldPublishDocs,
- isKmp
+ isKmp,
+ buildTarget,
)
anchorTask.dependsOn(task)
addTaskToAggregateBuildInfoFileTask(task)
@@ -317,6 +336,7 @@
shaProvider: Provider<String>,
shouldPublishDocs: Boolean,
isKmp: Boolean,
+ buildTarget: String,
): TaskProvider<CreateLibraryBuildInfoFileTask> {
val kmpTaskSuffix = computeTaskSuffix(name, artifactId)
return CreateLibraryBuildInfoFileTask.setup(
@@ -342,7 +362,7 @@
// suffix is listed in docs-public/build.gradle.
shouldPublishDocs = shouldPublishDocs && kmpTaskSuffix == "",
isKmp = isKmp,
- target = resolveTarget(isKmp),
+ target = buildTarget,
)
}
@@ -365,14 +385,11 @@
}
/**
- * Select the appropriate target based on if the project is KMP
+ * Indicates if any of the given [PlatformIdentifier]s targets an Apple platform
*
- * All non-Kotlin multiplatform projects use the `androidx` target. Projects that are KMP are built
- * for various platforms; specifically, the `iosarm64` and `iosx64` platforms can only be built on
- * the `androidx_multiplatform_mac` target.
- *
- * @param isKmp indicates if the project is KMP
- * @return target
+ * @param supportedPlatforms the set of [PlatformIdentifier] to examine
+ * @return true if any [PlatformIdentifier]s targets an Apple platform, false otherwise
*/
@VisibleForTesting
-fun resolveTarget(isKmp: Boolean) = if (isKmp) "androidx_multiplatform_mac" else "androidx"
+fun hasApplePlatform(supportedPlatforms: Set<PlatformIdentifier>) =
+ supportedPlatforms.any { it.group == PlatformGroup.MAC }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
index 902949a..f916624 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
@@ -15,6 +15,7 @@
*/
package androidx.build.license
+import androidx.build.capitalize
import androidx.build.getPrebuiltsRoot
import androidx.build.getVersionByName
import androidx.build.kotlinExtensionOrNull
@@ -22,12 +23,12 @@
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ExternalDependency
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.artifacts.result.ResolvedArtifactResult
-import org.gradle.api.attributes.Category
-import org.gradle.api.attributes.Usage
+import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.plugin.GradlePluginApiVersion
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.FileCollection
@@ -103,7 +104,6 @@
}
companion object {
- internal const val CONFIGURATION_NAME = "allExternalDependencies"
const val TASK_NAME = "checkExternalLicenses"
}
}
@@ -113,68 +113,21 @@
CheckExternalDependencyLicensesTask.TASK_NAME,
CheckExternalDependencyLicensesTask::class.java
) { task ->
+ @OptIn(ExperimentalBuildToolsApi::class, ExperimentalKotlinGradlePluginApi::class)
+ val kotlinVersion =
+ kotlinExtensionOrNull?.compilerVersion?.get() ?: project.getVersionByName("kotlin")
task.prebuiltsRoot.set(project.provider { project.getPrebuiltsRoot().absolutePath })
-
+ // configurations.toList() to avoid modify the collection while we iterate over it
+ val duplicateConfigs =
+ configurations.toList().map { configuration ->
+ duplicateForLicenseCheck(configuration, kotlinVersion)
+ }
+ val localArtifactRepositories = project.findLocalMavenRepositories()
task.filesToCheck.from(
- project.provider {
- val configName = "CheckExternalLicences"
- val container = project.configurations
- val checkerConfig =
- container.findByName(configName)
- ?: container.create(configName) { checkerConfig ->
- checkerConfig.isCanBeConsumed = false
- checkerConfig.attributes {
- it.attribute(
- Usage.USAGE_ATTRIBUTE,
- project.objects.named<Usage>(Usage.JAVA_RUNTIME)
- )
- it.attribute(
- Category.CATEGORY_ATTRIBUTE,
- project.objects.named<Category>(Category.LIBRARY)
- )
- it.attribute(
- GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
- project.objects.named<GradlePluginApiVersion>(
- GradleVersion.current().getVersion()
- )
- )
- }
-
- @OptIn(
- ExperimentalBuildToolsApi::class,
- ExperimentalKotlinGradlePluginApi::class
- )
- val kotlinVersion =
- kotlinExtensionOrNull?.compilerVersion?.get()
- ?: project.getVersionByName("kotlin")
-
- project.configurations
- .flatMap {
- it.allDependencies
- .filterIsInstance(ExternalDependency::class.java)
- .filterNot { it.group?.startsWith("com.android") == true }
- .filterNot { it.group?.startsWith("android.arch") == true }
- .filterNot { it.group?.startsWith("androidx") == true }
- }
- .forEach { dep ->
- /* workaround for dependency constraint applied in Kotlin Plugin */
- if (
- dep.group == "org.jetbrains.kotlin" &&
- dep.name == "kotlin-build-tools-impl"
- ) {
- dep.version { it.strictly(kotlinVersion) }
- }
- checkerConfig.dependencies.add(dep)
- }
- }
-
- val localArtifactRepositories = project.findLocalMavenRepositories()
- val dependencyArtifacts =
- checkerConfig.incoming.artifacts.artifacts.mapNotNull {
- project.validateAndGetArtifactInPrebuilts(it, localArtifactRepositories)
- }
-
- dependencyArtifacts
+ duplicateConfigs.flatMap { configuration ->
+ configuration.incoming.artifacts.artifacts.mapNotNull {
+ project.validateAndGetArtifactInPrebuilts(it, localArtifactRepositories)
+ }
}
)
}
@@ -249,3 +202,45 @@
.map { File(it) }
return project.files(fileList)
}
+
+private fun Project.duplicateForLicenseCheck(
+ configuration: Configuration,
+ kotlinVersion: String
+): Configuration {
+ val duplicate = configurations.create("${configuration.name}${configurationNameSuffix}")
+ duplicate.copyAttributesFrom(configuration)
+ duplicate.attributes.attribute(
+ GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
+ project.objects.named(GradleVersion.current().version)
+ )
+ val dependencies =
+ configuration.dependencies
+ .filterIsInstance<ExternalDependency>()
+ .filter { it.isValidForLicenseCheck() }
+ .onEach { it.fixVersion(kotlinVersion) }
+ duplicate.dependencies.addAll(dependencies)
+ duplicate.isCanBeConsumed = false
+ return duplicate
+}
+
+private fun Configuration.copyAttributesFrom(configuration: Configuration) {
+ configuration.attributes.keySet().forEach { attrKey ->
+ val value: Any = configuration.attributes.getAttribute(attrKey)!!
+ @Suppress("UNCHECKED_CAST") attributes.attribute(attrKey as Attribute<Any>, value)
+ }
+}
+
+private fun ExternalDependency.isValidForLicenseCheck(): Boolean {
+ return group?.startsWith("com.android") == false &&
+ group?.startsWith("android.arch") == false &&
+ group?.startsWith("androidx") == false
+}
+
+private fun ExternalDependency.fixVersion(kotlinVersion: String) {
+ /* workaround for dependency constraint applied in Kotlin Plugin */
+ if (group == "org.jetbrains.kotlin" && name == "kotlin-build-tools-impl") {
+ this.version { version -> version.strictly(kotlinVersion) }
+ }
+}
+
+private val configurationNameSuffix = CheckExternalDependencyLicensesTask.TASK_NAME.capitalize()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
index fa2856a..7b7547a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
@@ -17,6 +17,7 @@
package androidx.build.testConfiguration
import androidx.build.getDistributionDirectory
+import androidx.build.getSupportRootFolder
import com.google.gson.GsonBuilder
import java.io.File
import org.gradle.api.DefaultTask
@@ -76,9 +77,23 @@
task.includeEmptyDirs = false
}
- tasks.register("createModuleInfo", ModuleInfoGenerator::class.java) { task ->
+ tasks.register(CREATE_MODULE_INFO, ModuleInfoGenerator::class.java) { task ->
task.outputFile.set(File(getDistributionDirectory(), "module-info.json"))
}
}
+internal fun Project.addToModuleInfo(testName: String) {
+ rootProject.tasks.named(CREATE_MODULE_INFO).configure {
+ it as ModuleInfoGenerator
+ it.testModules.add(
+ TestModule(
+ name = testName,
+ path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+ )
+ )
+ }
+}
+
data class TestModule(val name: String, val path: List<String>)
+
+private const val CREATE_MODULE_INFO = "createModuleInfo"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 144e37e..2bd1ab6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -23,7 +23,6 @@
import androidx.build.deviceTestsForEachCompat
import androidx.build.getFileInTestConfigDirectory
import androidx.build.getPrivacySandboxFilesDirectory
-import androidx.build.getSupportRootFolder
import androidx.build.hasBenchmarkPlugin
import androidx.build.isMacrobenchmark
import androidx.build.isPresubmitBuild
@@ -192,14 +191,7 @@
rootProject.tasks
.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(generateTestConfigurationTask)
- rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
- it.testModules.add(
- TestModule(
- name = xmlName,
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
- )
- }
+ addToModuleInfo(testName = xmlName)
}
/**
@@ -414,41 +406,30 @@
}Tests$variantName.json"
}
- fun ModuleInfoGenerator.addTestModule(clientToT: Boolean, serviceToT: Boolean) {
+ fun Project.addTestModule(clientToT: Boolean, serviceToT: Boolean) {
// We don't test the combination of previous versions of service and client as that is not
// useful data. We always want at least one tip of tree project.
if (!clientToT && !serviceToT) return
- testModules.add(
- TestModule(
- name =
- getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = true),
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
+ addToModuleInfo(
+ testName =
+ getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = true)
)
- testModules.add(
- TestModule(
- name =
- getJsonName(
- clientToT = clientToT,
- serviceToT = serviceToT,
- clientTests = false
- ),
- path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
- )
+ addToModuleInfo(
+ testName =
+ getJsonName(clientToT = clientToT, serviceToT = serviceToT, clientTests = false)
)
}
val isClient = this.name.contains("client")
val isPrevious = this.name.contains("previous")
- rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
- if (isClient) {
- it.addTestModule(clientToT = !isPrevious, serviceToT = false)
- it.addTestModule(clientToT = !isPrevious, serviceToT = true)
- } else {
- it.addTestModule(clientToT = true, serviceToT = !isPrevious)
- it.addTestModule(clientToT = false, serviceToT = !isPrevious)
- }
+ if (isClient) {
+ addTestModule(clientToT = !isPrevious, serviceToT = false)
+ addTestModule(clientToT = !isPrevious, serviceToT = true)
+ } else {
+ addTestModule(clientToT = true, serviceToT = !isPrevious)
+ addTestModule(clientToT = false, serviceToT = !isPrevious)
}
+
mediaTask.configure {
if (isClient) {
if (isPrevious) {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
index e2c376d..ca78a94 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
@@ -56,10 +56,7 @@
}
/** Target platforms supported by the AndroidX implementation of Kotlin multi-platform. */
-enum class PlatformIdentifier(
- val id: String,
- @Suppress("unused") private val group: PlatformGroup
-) {
+enum class PlatformIdentifier(val id: String, val group: PlatformGroup) {
JVM("jvm", PlatformGroup.JVM),
JVM_STUBS("jvmStubs", PlatformGroup.JVM),
JS("js", PlatformGroup.JS),
@@ -82,6 +79,7 @@
WATCHOS_X_64("watchosx64", PlatformGroup.MAC),
WATCHOS_ARM_32("watchosarm64", PlatformGroup.MAC),
WATCHOS_ARM_64("watchosarm64", PlatformGroup.MAC),
+ WATCHOS_DEVICE_ARM_64("watchosdevicearm64", PlatformGroup.MAC),
TVOS_SIMULATOR_ARM_64("tvossimulatorarm64", PlatformGroup.MAC),
TVOS_X_64("tvosx64", PlatformGroup.MAC),
TVOS_ARM_64("tvosarm64", PlatformGroup.MAC),
diff --git a/busytown/androidx_host_tests_docker_2004.sh b/busytown/androidx_host_tests_docker_2004.sh
new file mode 100755
index 0000000..6856127
--- /dev/null
+++ b/busytown/androidx_host_tests_docker_2004.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+echo "Starting $0 at $(date)"
+
+cd "$(dirname $0)"
+
+echo "Completing $0 at $(date)"
diff --git a/busytown/impl/parse_profile_html.py b/busytown/impl/parse_profile_html.py
index 0ce5cae..436db9f 100755
--- a/busytown/impl/parse_profile_html.py
+++ b/busytown/impl/parse_profile_html.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2016 The Android Open Source Project
#
diff --git a/camera/camera-camera2-pipe-integration/lint-baseline.xml b/camera/camera-camera2-pipe-integration/lint-baseline.xml
index cfacd44..fb33fef 100644
--- a/camera/camera-camera2-pipe-integration/lint-baseline.xml
+++ b/camera/camera-camera2-pipe-integration/lint-baseline.xml
@@ -3,6 +3,24 @@
<issue
id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" callbacks.forEach { callback -> addCaptureCallback(callback, executor) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" val quirkSettings = QuirkSettingsHolder.instance().get()"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
errorLine1=" assertThat(fakeRequestControl.torchUpdateEventList.removeFirstKt()).isTrue()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -57,6 +75,15 @@
<issue
id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" .get(2, TimeUnit.SECONDS)"
+ errorLine2=" ~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
errorLine1=" assertThat(fakeRequestControl.torchUpdateEventList.removeFirstKt() == state).isTrue()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -66,6 +93,78 @@
<issue
id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" postviewDeferrableSurface.surface.get()!!"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/SessionProcessorManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCasesExpectedResultMap.keys.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCasesExpectedDynamicRangeMap.keys.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCases.forEach { put(it, it.currentConfig) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCaseConfigs.forEach { put(it, supportedSizes.toList()) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.util.Map#forEach`"
+ errorLine1=" streamConfigMap.forEach { (streamConfig, deferrableSurface) ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" deferrableSurfaces.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" forEach { captureConfig ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
errorLine1=" val lastRequest = fakeCameraGraph.fakeCameraGraphSession.repeatingRequests.removeLastKt()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -110,10 +209,82 @@
</issue>
<issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" cameraGraph.setSurface(it.id, deferrableSurface.surface.get())"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" [email protected] { useCase ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" [email protected] { useCase ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" forEach { useCase -> validatingBuilder.add(useCase.sessionConfig) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" forEach { useCase -> validatingBuilder.add(useCase.sessionConfig) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.util.Map#forEach`"
+ errorLine1=" surfaceToStreamMap.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseSurfaceManager.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 23): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" if (surface == testSessionParameters.deferrableSurface.surface.get()) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 23): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" if (surface == testSessionParameters.deferrableSurface.surface.get()) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt"/>
+ </issue>
+
+ <issue
id="CameraXQuirksClassDetector"
message="CameraX quirks should include this template in the javadoc:

* <p>QuirkSummary
* Bug Id:
* Description:
* Device(s):"
- errorLine1="class AspectRatioLegacyApi21Quirk : Quirk {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1="public class AspectRatioLegacyApi21Quirk : Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt"/>
</issue>
@@ -121,8 +292,8 @@
<issue
id="CameraXQuirksClassDetector"
message="CameraX quirks should include this template in the javadoc:

* <p>QuirkSummary
* Bug Id:
* Description:
* Device(s):"
- errorLine1="class ExcludedSupportedSizesQuirk : Quirk {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1="public class ExcludedSupportedSizesQuirk : Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExcludedSupportedSizesQuirk.kt"/>
</issue>
@@ -130,8 +301,8 @@
<issue
id="CameraXQuirksClassDetector"
message="CameraX quirks should include this template in the javadoc:

* <p>QuirkSummary
* Bug Id:
* Description:
* Device(s):"
- errorLine1="class ExtraCroppingQuirk : Quirk {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ errorLine1="public class ExtraCroppingQuirk : Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt"/>
</issue>
@@ -139,8 +310,8 @@
<issue
id="CameraXQuirksClassDetector"
message="CameraX quirks should include this template in the javadoc:

* <p>QuirkSummary
* Bug Id:
* Description:
* Device(s):"
- errorLine1="class ExtraSupportedSurfaceCombinationsQuirk : Quirk {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1="public class ExtraSupportedSurfaceCombinationsQuirk : Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt"/>
</issue>
@@ -148,19 +319,10 @@
<issue
id="CameraXQuirksClassDetector"
message="CameraX quirks should include this template in the javadoc:

* <p>QuirkSummary
* Bug Id:
* Description:
* Device(s):"
- errorLine1="class Nexus4AndroidLTargetAspectRatioQuirk : Quirk {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ errorLine1="public class Nexus4AndroidLTargetAspectRatioQuirk : Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt"/>
</issue>
- <issue
- id="SupportAnnotationUsage"
- message="Did you mean `@get:VisibleForTesting`? Without `get:` this annotates the constructor parameter itself instead of the associated getter."
- errorLine1=" @VisibleForTesting internal val requestListener: ComboRequestListener,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt"/>
- </issue>
-
</issues>
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
index 7a8a0f8..8440729 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
@@ -20,7 +20,6 @@
import android.media.CamcorderProfile
import android.media.EncoderProfiles.VideoProfile.HDR_NONE
import android.media.EncoderProfiles.VideoProfile.YUV_420
-import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraId
@@ -29,8 +28,6 @@
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CamcorderProfileResolutionQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
-import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
-import androidx.camera.camera2.pipe.integration.compat.quirk.InvalidVideoProfilesQuirk
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.core.CameraSelector
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
@@ -41,7 +38,6 @@
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
-import org.junit.Assume
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -154,6 +150,7 @@
val videoProxy = profilesProxy!!.videoProfiles[0]
val audioProxy = profilesProxy.audioProfiles[0]
+ // Don't check video/audio profile, see cts/CamcorderProfileTest.java
assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
assertThat(videoProxy.codec).isEqualTo(video.codec)
@@ -162,7 +159,6 @@
assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
assertThat(videoProxy.width).isEqualTo(video.width)
assertThat(videoProxy.height).isEqualTo(video.height)
- assertThat(videoProxy.profile).isEqualTo(video.profile)
assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
@@ -171,7 +167,6 @@
assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
assertThat(audioProxy.channels).isEqualTo(audio.channels)
- assertThat(audioProxy.profile).isEqualTo(audio.profile)
}
@SdkSuppress(minSdkVersion = 33)
@@ -186,6 +181,7 @@
val videoProxy = profilesProxy!!.videoProfiles[0]
val audioProxy = profilesProxy.audioProfiles[0]
+ // Don't check video/audio profile, see cts/CamcorderProfileTest.java
assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
assertThat(videoProxy.codec).isEqualTo(video.codec)
@@ -194,7 +190,6 @@
assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
assertThat(videoProxy.width).isEqualTo(video.width)
assertThat(videoProxy.height).isEqualTo(video.height)
- assertThat(videoProxy.profile).isEqualTo(video.profile)
assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
@@ -203,17 +198,6 @@
assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
assertThat(audioProxy.channels).isEqualTo(audio.channels)
- assertThat(audioProxy.profile).isEqualTo(audio.profile)
- }
-
- @LabTestRule.LabTestOnly
- @SdkSuppress(minSdkVersion = 31)
- @Test
- fun detectNullVideoProfile() {
- assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
- skipTestOnDevicesWithProblematicBuild()
- val profiles = CamcorderProfile.getAll(cameraId, quality)!!
- assertThat(profiles.videoProfiles[0]).isNotNull()
}
@LabTestRule.LabTestOnly
@@ -226,20 +210,6 @@
assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
}
- private fun skipTestOnDevicesWithProblematicBuild() {
- // Skip test for b/265613005, b/223439995 and b/277174217
- val hasVideoProfilesQuirk = DeviceQuirks[InvalidVideoProfilesQuirk::class.java] != null
- Assume.assumeFalse(
- "Skip test with null VideoProfile issue. Unable to test.",
- hasVideoProfilesQuirk || isProblematicCuttlefishBuild()
- )
- }
-
- private fun isProblematicCuttlefishBuild(): Boolean {
- return Build.MODEL.contains("Cuttlefish", true) &&
- (Build.ID.startsWith("TP1A", true) || Build.ID.startsWith("TSE4", true))
- }
-
@Suppress("DEPRECATION")
private fun assumeValidCamcorderProfile(quality: Int) {
assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index c615c95..9787e17 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -98,7 +98,6 @@
requestListener,
cameraConfig,
cameraQuirks,
- null,
ZslControlNoOpImpl(),
NoOpTemplateParamsOverride,
)
@@ -144,6 +143,7 @@
templateParamsOverride = NoOpTemplateParamsOverride,
),
useCaseGraphConfig = useCaseCameraGraphConfig,
+ threads = threads,
)
.apply {
SessionConfigAdapter(useCases).getValidSessionConfigOrNull()?.let { sessionConfig ->
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index b6e55c4..eace746 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -30,7 +30,6 @@
import androidx.camera.camera2.pipe.integration.impl.StillCaptureRequestControl
import androidx.camera.camera2.pipe.integration.impl.TorchControl
import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
-import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -49,8 +48,8 @@
import androidx.camera.core.impl.utils.futures.Futures
import com.google.common.util.concurrent.ListenableFuture
import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
/**
* Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -71,7 +70,6 @@
private val focusMeteringControl: FocusMeteringControl,
private val stillCaptureRequestControl: StillCaptureRequestControl,
private val torchControl: TorchControl,
- private val threads: UseCaseThreads,
private val zoomControl: ZoomControl,
private val zslControl: ZslControl,
public val camera2cameraControl: Camera2CameraControl,
@@ -112,11 +110,10 @@
override fun cancelFocusAndMetering(): ListenableFuture<Void> {
return Futures.nonCancellationPropagating(
- threads.sequentialScope
- .async {
- focusMeteringControl.cancelFocusAndMeteringAsync().join()
+ CompletableDeferred<Void?>()
+ .also {
// Convert to null once the task is done, ignore the results.
- return@async null
+ focusMeteringControl.cancelFocusAndMeteringAsync().propagateTo(it) { null }
}
.asListenableFuture()
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index 852e9b2..e1b06bb6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -48,6 +48,11 @@
camera2InteropCallbacks: CameraInteropStateCallbackRepository,
availableCamerasSelector: CameraSelector?,
) : CameraFactory {
+ private val cameraCoordinator: CameraCoordinatorAdapter =
+ CameraCoordinatorAdapter(
+ lazyCameraPipe.value,
+ lazyCameraPipe.value.cameras(),
+ )
private val appComponent: CameraAppComponent by lazy {
Debug.traceStart { "CameraFactoryAdapter#appComponent" }
val timeSource = SystemTimeSource()
@@ -59,7 +64,8 @@
context,
threadConfig,
lazyCameraPipe.value,
- camera2InteropCallbacks
+ camera2InteropCallbacks,
+ cameraCoordinator
)
)
.build()
@@ -68,11 +74,6 @@
result
}
private val availableCameraIds: LinkedHashSet<String>
- private val cameraCoordinator: CameraCoordinatorAdapter =
- CameraCoordinatorAdapter(
- appComponent.getCameraPipe(),
- appComponent.getCameraDevices(),
- )
init {
val optimizedCameraIds =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
index 76d7012..5353105 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -74,21 +74,48 @@
return CallbackToFutureAdapter.getFuture(resolver)
}
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ */
public fun <T> Deferred<T>.propagateTo(destination: CompletableDeferred<T>) {
- invokeOnCompletion { propagateOnceTo(destination, it) }
+ invokeOnCompletion { propagateCompletion(destination, it) }
}
-@OptIn(ExperimentalCoroutinesApi::class)
-public fun <T> Deferred<T>.propagateOnceTo(
- destination: CompletableDeferred<T>,
- throwable: Throwable?,
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ *
+ * @param destination The destination [CompletableDeferred] to which result is propagated to.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+public fun <T, R> Deferred<T>.propagateTo(
+ destination: CompletableDeferred<R>,
+ transform: (T) -> R,
) {
- if (throwable != null) {
- if (throwable is CancellationException) {
- destination.cancel(throwable)
- } else {
- destination.completeExceptionally(throwable)
- }
+ invokeOnCompletion { propagateCompletion(destination, it, transform) }
+}
+
+/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<T>,
+ completionCause: Throwable?,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
} else {
// Ignore exceptions - This should never throw in this situation.
destination.complete(getCompleted())
@@ -96,6 +123,46 @@
}
/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ * `Deferred.invokeOnCompletion`.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T, R> Deferred<T>.propagateCompletion(
+ destination: CompletableDeferred<R>,
+ completionCause: Throwable?,
+ transform: (T) -> R,
+) {
+ if (completionCause != null) {
+ destination.completeFailing(completionCause)
+ } else {
+ // Ignore exceptions - This should never throw in this situation.
+ destination.complete(transform(getCompleted()))
+ }
+}
+
+/**
+ * Completes this `Deferred` as failure based on the provided `cause`.
+ *
+ * @param cause If it's an instance of [CancellationException], [Deferred.cancel] is invoked for
+ * this, otherwise, [CompletableDeferred.completeExceptionally] is invoked.
+ */
+public fun <T> CompletableDeferred<T>.completeFailing(
+ cause: Throwable,
+) {
+ if (cause is CancellationException) {
+ cancel(cause)
+ } else {
+ completeExceptionally(cause)
+ }
+}
+
+/**
* Waits for [Deferred.await] to be completed until the given timeout.
*
* @return true if `Deferred.await` had completed, false otherwise.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
index 7901a98..6f58574 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/EvCompCompat.kt
@@ -38,7 +38,6 @@
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.launch
public interface EvCompCompat {
public val supported: Boolean
@@ -92,7 +91,7 @@
private var updateListener: Request.Listener? = null
override fun stopRunningTask(throwable: Throwable) {
- threads.sequentialScope.launch { updateSignal?.completeExceptionally(throwable) }
+ updateSignal?.completeExceptionally(throwable)
}
override fun applyAsync(
@@ -102,69 +101,64 @@
): Deferred<Int> {
val signal = CompletableDeferred<Int>()
- threads.sequentialScope.launch {
- updateSignal?.let { previousUpdateSignal ->
- if (cancelPreviousTask) {
- // Cancel the previous request signal if exist.
- previousUpdateSignal.completeExceptionally(
- CameraControl.OperationCanceledException(
- "Cancelled by another setExposureCompensationIndex()"
- )
+ updateSignal?.let { previousUpdateSignal ->
+ if (cancelPreviousTask) {
+ // Cancel the previous request signal if exist.
+ previousUpdateSignal.completeExceptionally(
+ CameraControl.OperationCanceledException(
+ "Cancelled by another setExposureCompensationIndex()"
)
- } else {
- // Propagate the result to the previous updateSignal
- signal.propagateTo(previousUpdateSignal)
- }
+ )
+ } else {
+ // Propagate the result to the previous updateSignal
+ signal.propagateTo(previousUpdateSignal)
}
- updateSignal = signal
- updateListener?.let {
- comboRequestListener.removeListener(it)
- updateListener = null
- }
-
- requestControl.setParametersAsync(
- values = mapOf(CONTROL_AE_EXPOSURE_COMPENSATION to evCompIndex)
- )
-
- // Prepare the listener to wait for the exposure value to reach the target.
- updateListener =
- object : Request.Listener {
- override fun onComplete(
- requestMetadata: RequestMetadata,
- frameNumber: FrameNumber,
- result: FrameInfo,
- ) {
- val state = result.metadata[CaptureResult.CONTROL_AE_STATE]
- val evResult =
- result.metadata[CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION]
- if (state != null && evResult != null) {
- when (state) {
- CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
- CaptureResult.CONTROL_AE_STATE_CONVERGED,
- CaptureResult.CONTROL_AE_STATE_LOCKED ->
- if (evResult == evCompIndex) {
- signal.complete(evCompIndex)
- }
- else -> {}
- }
- } else if (evResult != null && evResult == evCompIndex) {
- // If AE state is null, only wait for the exposure result to the
- // desired
- // value.
- signal.complete(evCompIndex)
- }
- }
- }
- .also { requestListener ->
- comboRequestListener.addListener(
- requestListener,
- threads.sequentialExecutor
- )
- signal.invokeOnCompletion {
- comboRequestListener.removeListener(requestListener)
- }
- }
}
+ updateSignal = signal
+ updateListener?.let {
+ comboRequestListener.removeListener(it)
+ updateListener = null
+ }
+
+ requestControl.setParametersAsync(
+ values = mapOf(CONTROL_AE_EXPOSURE_COMPENSATION to evCompIndex)
+ )
+
+ // Prepare the listener to wait for the exposure value to reach the target.
+ updateListener =
+ object : Request.Listener {
+ override fun onComplete(
+ requestMetadata: RequestMetadata,
+ frameNumber: FrameNumber,
+ result: FrameInfo,
+ ) {
+ val state = result.metadata[CaptureResult.CONTROL_AE_STATE]
+ val evResult =
+ result.metadata[CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION]
+ if (state != null && evResult != null) {
+ when (state) {
+ CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+ CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AE_STATE_LOCKED ->
+ if (evResult == evCompIndex) {
+ signal.complete(evCompIndex)
+ }
+ else -> {}
+ }
+ } else if (evResult != null && evResult == evCompIndex) {
+ // If AE state is null, only wait for the exposure result to the
+ // desired
+ // value.
+ signal.complete(evCompIndex)
+ }
+ }
+ }
+ .also { requestListener ->
+ comboRequestListener.addListener(requestListener, threads.sequentialExecutor)
+ signal.invokeOnCompletion {
+ comboRequestListener.removeListener(requestListener)
+ }
+ }
return signal
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index 7aa48d6..dc61707 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -35,9 +35,13 @@
try {
streamConfigurationMap?.outputFormats
} catch (e: NullPointerException) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ null
+ } catch (e: IllegalArgumentException) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
null
}
+
return outputFormats?.toTypedArray()
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index 81e63e1..2578dba 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -218,6 +218,14 @@
}
if (
quirkSettings.shouldEnableQuirk(
+ QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk::class.java,
+ QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk.isEnabled(cameraMetadata)
+ )
+ ) {
+ quirks.add(QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk())
+ }
+ if (
+ quirkSettings.shouldEnableQuirk(
ImageCaptureFailedWhenVideoCaptureIsBoundQuirk::class.java,
ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.isEnabled()
)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CaptureSessionStuckQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CaptureSessionStuckQuirk.kt
index 14519de..b4d712a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CaptureSessionStuckQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CaptureSessionStuckQuirk.kt
@@ -35,8 +35,8 @@
public companion object {
/**
* Always return false as CameraPipe handles this automatically. Please refer to
- * [androidx.camera.camera2.pipe.compat.Camera2Quirks.shouldWaitForRepeatingRequest] for the
- * conditions under which the quirk will be applied.
+ * [androidx.camera.camera2.pipe.compat.Camera2Quirks.shouldWaitForRepeatingRequestStartOnDisconnect]
+ * for the conditions under which the quirk will be applied.
*/
public fun isEnabled(): Boolean = false
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
index a400dfa..81d49c1 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
@@ -187,6 +187,14 @@
) {
quirks.add(ZslDisablerQuirk())
}
+ if (
+ quirkSettings.shouldEnableQuirk(
+ SmallDisplaySizeQuirk::class.java,
+ SmallDisplaySizeQuirk.load()
+ )
+ ) {
+ quirks.add(SmallDisplaySizeQuirk())
+ }
return quirks
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk.kt
new file mode 100644
index 0000000..e7211bd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy
+import androidx.camera.camera2.pipe.integration.compat.quirk.Device.isSamsungDevice
+import androidx.camera.core.impl.Quirk
+
+/**
+ * Quirk about still image (non-repeating) capture quickly succeeding a repeating request leading to
+ * failures.
+ *
+ * QuirkSummary
+ * - Bug Id: 356792665
+ * - Description: On some legacy devices from Samsung J1 Mini, this can lead to an invalid parameter
+ * in the repeating request resulting in a variety of failures. Waiting for the repeating request
+ * start to be completed before image capture submission can workaround such issues.
+ * - Device(s): All Samsung legacy devices
+ */
+@SuppressLint("CameraXQuirksClassDetector") // TODO(b/270421716): enable when kotlin is supported.
+public class QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk : Quirk {
+ public companion object {
+ public fun isEnabled(cameraMetadata: CameraMetadata): Boolean =
+ isSamsungDevice() && cameraMetadata.isHardwareLevelLegacy
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/SmallDisplaySizeQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/SmallDisplaySizeQuirk.kt
new file mode 100644
index 0000000..8e50ab6
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/SmallDisplaySizeQuirk.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: b/287341266
+ * - Description: Quirk required to return the display size for problematic devices. Some devices
+ * might return abnormally small display size (16x16). This might cause PREVIEW size to be
+ * incorrectly determined and all supported output sizes are filtered out.
+ * - Device(s): Redmi Note8, Redmi Note 7, SM-A207M (see b/287341266 for the devices list)
+ *
+ * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+public class SmallDisplaySizeQuirk : Quirk {
+ public val displaySize: Size
+ get() = MODEL_TO_DISPLAY_SIZE_MAP[Build.MODEL.uppercase()]!!
+
+ public companion object {
+ private val MODEL_TO_DISPLAY_SIZE_MAP =
+ mapOf(
+ "REDMI NOTE 8" to Size(1080, 2340),
+ "REDMI NOTE 7" to Size(1080, 2340),
+ "SM-A207M" to Size(720, 1560),
+ "REDMI NOTE 7S" to Size(1080, 2340),
+ "SM-A127F" to Size(720, 1600),
+ "SM-A536E" to Size(1080, 2400),
+ "220233L2I" to Size(720, 1600),
+ "V2149" to Size(720, 1600),
+ "VIVO 1920" to Size(1080, 2340),
+ "CPH2223" to Size(1080, 2400),
+ "V2029" to Size(720, 1600),
+ "CPH1901" to Size(720, 1520),
+ "REDMI Y3" to Size(720, 1520),
+ "SM-A045M" to Size(720, 1600),
+ "SM-A146U" to Size(1080, 2408),
+ "CPH1909" to Size(720, 1520),
+ "NOKIA 4.2" to Size(720, 1520),
+ "SM-G960U1" to Size(1440, 2960),
+ "SM-A137F" to Size(1080, 2408),
+ "VIVO 1816" to Size(720, 1520),
+ "INFINIX X6817" to Size(720, 1612),
+ "SM-A037F" to Size(720, 1600),
+ "NOKIA 2.4" to Size(720, 1600),
+ "SM-A125M" to Size(720, 1600),
+ "INFINIX X670" to Size(1080, 2400)
+ )
+
+ public fun load(): Boolean {
+ return MODEL_TO_DISPLAY_SIZE_MAP.containsKey(Build.MODEL.uppercase())
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrector.kt
new file mode 100644
index 0000000..4221d5d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrector.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.SmallDisplaySizeQuirk
+
+/**
+ * Provides the correct display size for the problematic devices which might return abnormally small
+ * display size.
+ */
+public class DisplaySizeCorrector {
+ private val smallDisplaySizeQuirk: SmallDisplaySizeQuirk? =
+ DeviceQuirks[SmallDisplaySizeQuirk::class.java]
+
+ public val displaySize: Size?
+ /**
+ * Returns the device's correct display size if it is included in the SmallDisplaySizeQuirk.
+ */
+ get() = smallDisplaySizeQuirk?.displaySize
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
index c6ebdf0..65f44eb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -20,6 +20,7 @@
import androidx.camera.camera2.pipe.CameraDevices
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
+import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
import dagger.Component
@@ -44,7 +45,8 @@
private val context: Context,
private val cameraThreadConfig: CameraThreadConfig,
private val cameraPipe: CameraPipe,
- private val camera2InteropCallbacks: CameraInteropStateCallbackRepository
+ private val camera2InteropCallbacks: CameraInteropStateCallbackRepository,
+ private val cameraCoordinator: CameraCoordinator
) {
@Provides public fun provideContext(): Context = context
@@ -55,6 +57,8 @@
@Provides
public fun provideCamera2InteropCallbacks(): CameraInteropStateCallbackRepository =
camera2InteropCallbacks
+
+ @Provides public fun provideCameraCoordinator(): CameraCoordinator = cameraCoordinator
}
/** Dagger component for Application (Process) scoped dependencies. */
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
index 8155184..dd385b7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -21,7 +21,6 @@
import android.os.Build
import androidx.annotation.Nullable
import androidx.annotation.VisibleForTesting
-import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
@@ -38,8 +37,6 @@
import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
-import androidx.camera.camera2.pipe.integration.compat.quirk.CaptureSessionStuckQuirk
-import androidx.camera.camera2.pipe.integration.compat.quirk.FinalizeSessionOnCloseQuirk
import androidx.camera.camera2.pipe.integration.impl.CameraPipeCameraProperties
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
@@ -151,23 +148,6 @@
@CameraScope
@Provides
- public fun provideCameraGraphFlags(cameraQuirks: CameraQuirks): CameraGraph.Flags {
- if (cameraQuirks.quirks.contains(CaptureSessionStuckQuirk::class.java)) {
- Log.debug { "CameraPipe should be enabling CaptureSessionStuckQuirk" }
- }
- // TODO(b/276354253): Set quirkWaitForRepeatingRequestOnDisconnect flag for overrides.
-
- // TODO(b/277310425): When creating a CameraGraph, this flag should be turned OFF when
- // this behavior is not needed based on the use case interaction and the device on
- // which the test is running.
- val quirkFinalizeSessionOnCloseBehavior = FinalizeSessionOnCloseQuirk.getBehavior()
- return CameraGraph.Flags(
- quirkFinalizeSessionOnCloseBehavior = quirkFinalizeSessionOnCloseBehavior,
- )
- }
-
- @CameraScope
- @Provides
@Named("cameraQuirksValues")
public fun provideCameraQuirksValues(cameraQuirks: CameraQuirks): Quirks =
cameraQuirks.quirks
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index 9501444..fd71c87 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -24,17 +24,27 @@
import android.os.Looper
import android.util.Size
import android.view.Display
+import androidx.camera.camera2.pipe.integration.compat.workaround.DisplaySizeCorrector
import androidx.camera.camera2.pipe.integration.compat.workaround.MaxPreviewSize
+import androidx.camera.core.internal.utils.SizeUtil
import javax.inject.Inject
import javax.inject.Singleton
@Suppress("DEPRECATION") // getRealSize
@Singleton
public class DisplayInfoManager @Inject constructor(context: Context) {
- private val MAX_PREVIEW_SIZE = Size(1920, 1080)
- private val maxPreviewSize: MaxPreviewSize = MaxPreviewSize()
+ private val maxPreviewSize = MaxPreviewSize()
+ private val displaySizeCorrector = DisplaySizeCorrector()
public companion object {
+ private val MAX_PREVIEW_SIZE = Size(1920, 1080)
+ /** This is the smallest size from a device which had issue reported to CameraX. */
+ private val ABNORMAL_DISPLAY_SIZE_THRESHOLD: Size = Size(320, 240)
+ /**
+ * The fallback display size for the case that the retrieved display size is abnormally
+ * small and no correct display size can be retrieved from DisplaySizeCorrector.
+ */
+ private val FALLBACK_DISPLAY_SIZE: Size = Size(640, 480)
private var lazyMaxDisplay: Display? = null
private var lazyPreviewSize: Size? = null
@@ -131,26 +141,32 @@
return it
}
- val displaySize = Point()
- val display: Display = defaultDisplay
- // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
- // is deprecated since API level 31.
- display.getRealSize(displaySize)
- var displayViewSize: Size
- displayViewSize =
- if (displaySize.x > displaySize.y) {
- Size(displaySize.x, displaySize.y)
- } else {
- Size(displaySize.y, displaySize.x)
- }
- if (
- displayViewSize.width * displayViewSize.height >
- MAX_PREVIEW_SIZE.width * MAX_PREVIEW_SIZE.height
- ) {
+ var displayViewSize = getCorrectedDisplaySize()
+ if (SizeUtil.isSmallerByArea(MAX_PREVIEW_SIZE, displayViewSize)) {
displayViewSize = MAX_PREVIEW_SIZE
}
- displayViewSize = maxPreviewSize.getMaxPreviewResolution(displayViewSize)
+ return maxPreviewSize.getMaxPreviewResolution(displayViewSize).also { lazyPreviewSize = it }
+ }
- return displayViewSize.also { lazyPreviewSize = displayViewSize }
+ private fun getCorrectedDisplaySize(): Size {
+ val displaySize = Point()
+ defaultDisplay.getRealSize(displaySize)
+ var displayViewSize = Size(displaySize.x, displaySize.y)
+
+ // Checks whether the display size is abnormally small.
+ if (SizeUtil.isSmallerByArea(displayViewSize, ABNORMAL_DISPLAY_SIZE_THRESHOLD)) {
+ // Gets the display size from DisplaySizeCorrector if the display size retrieved from
+ // DisplayManager is abnormally small. Falls back the display size to 640x480 if
+ // DisplaySizeCorrector doesn't contain the device's display size info.
+ displayViewSize = displaySizeCorrector.displaySize ?: FALLBACK_DISPLAY_SIZE
+ }
+
+ // Flips the size to landscape orientation
+ if (displayViewSize.height > displayViewSize.width) {
+ displayViewSize =
+ Size(/* width= */ displayViewSize.height, /* height= */ displayViewSize.width)
+ }
+
+ return displayViewSize
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
index dbc0d6b..b0468cc 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
@@ -37,7 +37,6 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
@@ -64,7 +63,7 @@
override fun reset() {
_flashMode = DEFAULT_FLASH_MODE
_screenFlash = null
- threads.sequentialScope.launch { stopRunningTask() }
+ stopRunningTask()
setFlashAsync(DEFAULT_FLASH_MODE)
}
@@ -104,20 +103,18 @@
// returns correct value.
_flashMode = flashMode
- threads.sequentialScope.launch {
- if (cancelPreviousTask) {
- stopRunningTask()
- } else {
- // Propagate the result to the previous updateSignal
- _updateSignal?.let { previousUpdateSignal ->
- signal.propagateTo(previousUpdateSignal)
- }
+ if (cancelPreviousTask) {
+ stopRunningTask()
+ } else {
+ // Propagate the result to the previous updateSignal
+ _updateSignal?.let { previousUpdateSignal ->
+ signal.propagateTo(previousUpdateSignal)
}
-
- _updateSignal = signal
- state3AControl.flashMode = flashMode
- state3AControl.updateSignal?.propagateTo(signal) ?: run { signal.complete(Unit) }
}
+
+ _updateSignal = signal
+ state3AControl.flashMode = flashMode
+ state3AControl.updateSignal?.propagateTo(signal) ?: run { signal.complete(Unit) }
}
?: run {
signal.completeExceptionally(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 8bfcd72..aef4885 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -121,131 +121,121 @@
val signal = CompletableDeferred<FocusMeteringResult>()
requestControl?.let { requestControl ->
- threads.sequentialScope.launch {
- focusTimeoutJob?.cancel()
- autoCancelJob?.cancel()
- cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
- updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
+ focusTimeoutJob?.cancel()
+ autoCancelJob?.cancel()
+ cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
+ updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
- updateSignal = signal
+ updateSignal = signal
- val aeRectangles =
- meteringRegionsFromMeteringPoints(
- action.meteringPointsAe,
- maxAeRegionCount,
- cropSensorRegion,
- defaultAspectRatio,
- FocusMeteringAction.FLAG_AE,
- meteringRegionCorrection,
- )
- val afRectangles =
- meteringRegionsFromMeteringPoints(
- action.meteringPointsAf,
- maxAfRegionCount,
- cropSensorRegion,
- defaultAspectRatio,
- FocusMeteringAction.FLAG_AF,
- meteringRegionCorrection,
- )
- val awbRectangles =
- meteringRegionsFromMeteringPoints(
- action.meteringPointsAwb,
- maxAwbRegionCount,
- cropSensorRegion,
- defaultAspectRatio,
- FocusMeteringAction.FLAG_AWB,
- meteringRegionCorrection,
- )
- if (aeRectangles.isEmpty() && afRectangles.isEmpty() && awbRectangles.isEmpty()) {
- signal.completeExceptionally(
- IllegalArgumentException(
- "None of the specified AF/AE/AWB MeteringPoints is supported on" +
- " this camera."
- )
- )
- return@launch
- }
- if (afRectangles.isNotEmpty()) {
- state3AControl.preferredFocusMode = CaptureRequest.CONTROL_AF_MODE_AUTO
- }
-
- val aeRegions =
- if (maxAeRegionCount > 0)
- aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null
- val afRegions =
- if (maxAfRegionCount > 0)
- afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null
- val awbRegions =
- if (maxAwbRegionCount > 0)
- awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null
-
- val deferredResult3A =
- if (
- afRectangles.isEmpty() ||
- !cameraProperties.metadata.supportsAutoFocusTrigger
- ) {
- /*
- * Controller3A.lock3A() returns early in such cases without updating the 3A
- * regions which conflicts with [CameraControl.startFocusAndMetering] doc.
- * However, we should update the regions explicitly here only in these cases
- * instead of all cases because Controller3A.update3A() will invalidate
- * the CameraGraph and thus may cause extra requests to the camera.
- */
- debug { "startFocusAndMetering: updating 3A regions only" }
- requestControl.update3aRegions(
- aeRegions = aeRegions,
- afRegions = afRegions,
- awbRegions = awbRegions,
- )
- } else {
- // No need to keep trying to focus if auto-cancel is already triggered
- val finalFocusTimeout =
- if (
- action.isAutoCancelEnabled &&
- action.autoCancelDurationInMillis < autoFocusTimeoutMs
- ) {
- action.autoCancelDurationInMillis
- } else {
- autoFocusTimeoutMs
- }
-
- debug { "startFocusAndMetering: updating 3A regions & triggering AF" }
- /*
- * If device does not support a 3A region, we should not update it at all.
- * If device does support but a region list is empty, it means any previously
- * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
- */
- requestControl.startFocusAndMeteringAsync(
- aeRegions = aeRegions,
- afRegions = afRegions,
- awbRegions = awbRegions,
- afLockBehavior =
- if (maxAfRegionCount > 0) Lock3ABehavior.IMMEDIATE else null,
- afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
- timeLimitNs =
- TimeUnit.NANOSECONDS.convert(
- finalFocusTimeout,
- TimeUnit.MILLISECONDS
- )
- )
- }
-
- deferredResult3A.propagateToFocusMeteringResultDeferred(
- resultDeferred = signal,
- shouldTriggerAf = afRectangles.isNotEmpty(),
+ val aeRectangles =
+ meteringRegionsFromMeteringPoints(
+ action.meteringPointsAe,
+ maxAeRegionCount,
+ cropSensorRegion,
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AE,
+ meteringRegionCorrection,
)
+ val afRectangles =
+ meteringRegionsFromMeteringPoints(
+ action.meteringPointsAf,
+ maxAfRegionCount,
+ cropSensorRegion,
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AF,
+ meteringRegionCorrection,
+ )
+ val awbRectangles =
+ meteringRegionsFromMeteringPoints(
+ action.meteringPointsAwb,
+ maxAwbRegionCount,
+ cropSensorRegion,
+ defaultAspectRatio,
+ FocusMeteringAction.FLAG_AWB,
+ meteringRegionCorrection,
+ )
+ if (aeRectangles.isEmpty() && afRectangles.isEmpty() && awbRectangles.isEmpty()) {
+ signal.completeExceptionally(
+ IllegalArgumentException(
+ "None of the specified AF/AE/AWB MeteringPoints is supported on" +
+ " this camera."
+ )
+ )
+ return signal.asListenableFuture()
+ }
+ if (afRectangles.isNotEmpty()) {
+ state3AControl.preferredFocusMode = CaptureRequest.CONTROL_AF_MODE_AUTO
+ }
- // camera-pipe core layer invokes timeout when there is a new frame result from
- // camera, this is not precise enough for CameraX since it may allow auto-cancel to
- // be triggered first for same or very close timeout values
- triggerFocusTimeout(autoFocusTimeoutMs, signal)
+ val aeRegions =
+ if (maxAeRegionCount > 0) aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null
+ val afRegions =
+ if (maxAfRegionCount > 0) afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null
+ val awbRegions =
+ if (maxAwbRegionCount > 0)
+ awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null
- if (action.isAutoCancelEnabled) {
- triggerAutoCancel(action.autoCancelDurationInMillis, signal, requestControl)
+ val deferredResult3A =
+ if (afRectangles.isEmpty() || !cameraProperties.metadata.supportsAutoFocusTrigger) {
+ /*
+ * Controller3A.lock3A() returns early in such cases without updating the 3A
+ * regions which conflicts with [CameraControl.startFocusAndMetering] doc.
+ * However, we should update the regions explicitly here only in these cases
+ * instead of all cases because Controller3A.update3A() will invalidate
+ * the CameraGraph and thus may cause extra requests to the camera.
+ */
+ debug { "startFocusAndMetering: updating 3A regions only" }
+ requestControl.update3aRegions(
+ aeRegions = aeRegions,
+ afRegions = afRegions,
+ awbRegions = awbRegions,
+ )
+ } else {
+ // No need to keep trying to focus if auto-cancel is already triggered
+ val finalFocusTimeout =
+ if (
+ action.isAutoCancelEnabled &&
+ action.autoCancelDurationInMillis < autoFocusTimeoutMs
+ ) {
+ action.autoCancelDurationInMillis
+ } else {
+ autoFocusTimeoutMs
+ }
+
+ debug { "startFocusAndMetering: updating 3A regions & triggering AF" }
+ /*
+ * If device does not support a 3A region, we should not update it at all.
+ * If device does support but a region list is empty, it means any previously
+ * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
+ */
+ requestControl.startFocusAndMeteringAsync(
+ aeRegions = aeRegions,
+ afRegions = afRegions,
+ awbRegions = awbRegions,
+ afLockBehavior =
+ if (maxAfRegionCount > 0) Lock3ABehavior.IMMEDIATE else null,
+ afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
+ timeLimitNs =
+ TimeUnit.NANOSECONDS.convert(finalFocusTimeout, TimeUnit.MILLISECONDS)
+ )
}
+
+ deferredResult3A.propagateToFocusMeteringResultDeferred(
+ resultDeferred = signal,
+ shouldTriggerAf = afRectangles.isNotEmpty(),
+ )
+
+ // camera-pipe core layer invokes timeout when there is a new frame result from
+ // camera, this is not precise enough for CameraX since it may allow auto-cancel to
+ // be triggered first for same or very close timeout values
+ triggerFocusTimeout(autoFocusTimeoutMs, signal)
+
+ if (action.isAutoCancelEnabled) {
+ triggerAutoCancel(action.autoCancelDurationInMillis, signal, requestControl)
}
}
?: run {
@@ -367,13 +357,11 @@
public fun cancelFocusAndMeteringAsync(): Deferred<Result3A?> {
val signal = CompletableDeferred<Result3A?>()
requestControl?.let { requestControl ->
- threads.sequentialScope.launch {
- focusTimeoutJob?.cancel()
- autoCancelJob?.cancel()
- cancelSignal?.setCancelException("Cancelled by another cancelFocusAndMetering()")
- cancelSignal = signal
- cancelFocusAndMeteringNowAsync(requestControl, updateSignal).propagateTo(signal)
- }
+ focusTimeoutJob?.cancel()
+ autoCancelJob?.cancel()
+ cancelSignal?.setCancelException("Cancelled by another cancelFocusAndMetering()")
+ cancelSignal = signal
+ cancelFocusAndMeteringNowAsync(requestControl, updateSignal).propagateTo(signal)
}
?: run {
signal.completeExceptionally(OperationCanceledException("Camera is not active."))
@@ -382,7 +370,7 @@
return signal
}
- private suspend fun cancelFocusAndMeteringNowAsync(
+ private fun cancelFocusAndMeteringNowAsync(
requestControl: UseCaseCameraRequestControl,
signalToCancel: CompletableDeferred<FocusMeteringResult>?,
): Deferred<Result3A> {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 82da040..ac29fa3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -45,6 +45,7 @@
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
import kotlin.math.min
private val DEFAULT_PREVIEW_SIZE = Size(0, 0)
@@ -187,13 +188,15 @@
if (product == maxSizeProduct) {
return outputSize
} else if (product > maxSizeProduct) {
- return previousSize ?: break // fallback to minimum size.
+ // Returns the maximum supported resolution that is <= min(VGA, display resolution)
+ // if it is found
+ return previousSize ?: break
}
previousSize = outputSize
}
// If not found, return the minimum size.
- return outputSizes[0]
+ return previousSize ?: outputSizes[0]
}
public class MeteringRepeatingConfig : UseCaseConfig<MeteringRepeating>, ImageInputConfig {
@@ -203,6 +206,7 @@
OPTION_SESSION_CONFIG_UNPACKER,
CameraUseCaseAdapter.DefaultSessionOptionsUnpacker
)
+ insertOption(OPTION_TARGET_NAME, "MeteringRepeating")
insertOption(OPTION_CAPTURE_TYPE, CaptureType.METERING_REPEATING)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
index da6fcdb..fa9fdb7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -90,6 +90,14 @@
public var flashMode: Int by updateOnPropertyChange(DEFAULT_FLASH_MODE)
public var template: Int by updateOnPropertyChange(DEFAULT_REQUEST_TEMPLATE)
public var tryExternalFlashAeMode: Boolean by updateOnPropertyChange(false)
+
+ /**
+ * The [CaptureRequest.CONTROL_AE_MODE] that is set to camera if supported.
+ *
+ * If null, a value based on other settings is calculated and available via
+ * [getFinalPreferredAeMode]. If not supported, [getSupportedAeMode] is used to find the next
+ * best option.
+ */
public var preferredAeMode: Int? by updateOnPropertyChange(null)
public var preferredFocusMode: Int? by updateOnPropertyChange(null)
public var preferredAeFpsRange: Range<Int>? by
@@ -118,6 +126,19 @@
}
}
+ /**
+ * Returns the AE mode that is finally set to camera based on all other settings and camera
+ * capabilities.
+ */
+ public fun getFinalSupportedAeMode(): Int =
+ cameraProperties.metadata.getSupportedAeMode(getFinalPreferredAeMode())
+
+ /**
+ * Returns the AE mode that is finally set to camera based on all other settings.
+ *
+ * Note that this may not be supported via the camera and should be sanitized with
+ * [getSupportedAeMode].
+ */
private fun getFinalPreferredAeMode(): Int {
var preferAeMode =
preferredAeMode
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index 7ffef5d..94f03ea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -19,7 +19,7 @@
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
-import androidx.camera.camera2.pipe.integration.adapter.propagateOnceTo
+import androidx.camera.camera2.pipe.integration.adapter.propagateCompletion
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
@@ -197,7 +197,7 @@
}
}
} else {
- propagateOnceTo(submittedRequest.result, cause)
+ propagateCompletion(submittedRequest.result, cause)
}
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index 7d2deb3..0c8f827 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -17,6 +17,9 @@
package androidx.camera.camera2.pipe.integration.impl
import android.hardware.camera2.CaptureRequest
+import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.config.CameraScope
@@ -32,7 +35,6 @@
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.launch
/** Implementation of Torch control exposed by [CameraControlInternal]. */
@CameraScope
@@ -61,7 +63,7 @@
override fun reset() {
_torchState.setLiveDataValue(false)
- threads.sequentialScope.launch { stopRunningTaskInternal() }
+ stopRunningTaskInternal()
setTorchAsync(false)
}
@@ -86,6 +88,8 @@
cancelPreviousTask: Boolean = true,
ignoreFlashUnitAvailability: Boolean = false
): Deferred<Unit> {
+ debug { "TorchControl#setTorchAsync: torch = $torch" }
+
val signal = CompletableDeferred<Unit>()
if (!ignoreFlashUnitAvailability && !hasFlashUnit) {
@@ -95,28 +99,40 @@
requestControl?.let { requestControl ->
_torchState.setLiveDataValue(torch)
- threads.sequentialScope.launch {
- if (cancelPreviousTask) {
- stopRunningTaskInternal()
- } else {
- // Propagate the result to the previous updateSignal
- _updateSignal?.let { previousUpdateSignal ->
- signal.propagateTo(previousUpdateSignal)
- }
+ if (cancelPreviousTask) {
+ stopRunningTaskInternal()
+ } else {
+ // Propagate the result to the previous updateSignal
+ _updateSignal?.let { previousUpdateSignal ->
+ signal.propagateTo(previousUpdateSignal)
}
+ }
- _updateSignal = signal
+ _updateSignal = signal
- // TODO(b/209757083), handle the failed result of the setTorchAsync().
- requestControl.setTorchAsync(torch).join()
+ // Hold the internal AE mode to ON while the torch is turned ON. If torch is OFF, a
+ // value of null will make the state3AControl calculate the correct AE mode based on
+ // other settings.
+ state3AControl.preferredAeMode = if (torch) CaptureRequest.CONTROL_AE_MODE_ON else null
+ val aeMode: AeMode =
+ AeMode.fromIntOrNull(state3AControl.getFinalSupportedAeMode())
+ ?: run {
+ warn {
+ "TorchControl#setTorchAsync: Failed to convert ae mode of value" +
+ " ${state3AControl.getFinalSupportedAeMode()} with" +
+ " AeMode.fromIntOrNull, fallback to AeMode.ON"
+ }
+ AeMode.ON
+ }
- // Hold the internal AE mode to ON while the torch is turned ON.
- state3AControl.preferredAeMode =
- if (torch) CaptureRequest.CONTROL_AE_MODE_ON else null
-
- // Always update3A again to reset the AE state in the Camera-pipe controller.
- state3AControl.invalidate()
- state3AControl.updateSignal?.propagateTo(signal) ?: run { signal.complete(Unit) }
+ val deferred =
+ if (torch) requestControl.setTorchOnAsync()
+ else requestControl.setTorchOffAsync(aeMode)
+ deferred.propagateTo(signal) {
+ // TODO: b/209757083 - handle the failed result of the setTorchAsync().
+ // Since we are not handling the result here, signal is completed with Unit
+ // value here without exception when source deferred completes (returning Unit
+ // explicitly is redundant and thus this block looks empty)
}
}
?: run {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 0731bda..b76233c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -19,7 +19,7 @@
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.MeteringRectangle
-import androidx.annotation.GuardedBy
+import androidx.annotation.AnyThread
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
@@ -28,7 +28,6 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -56,6 +55,10 @@
*
* Parameters can be stored and managed according to different configuration types. Each type can be
* modified or overridden independently without affecting other types.
+ *
+ * This class should be used as the entry point for submitting requests to the [UseCaseCameraScope]
+ * layer. This ensures that thread confinement are properly applied at a single place for the whole
+ * [UseCaseCameraScope] and reduces concurrency issues.
*/
@JvmDefaultWithCompatibility
public interface UseCaseCameraRequestControl {
@@ -82,6 +85,7 @@
* multiple times.
* @return A [Deferred] object representing the asynchronous operation.
*/
+ @AnyThread
public fun setParametersAsync(
type: Type = Type.DEFAULT,
values: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
@@ -109,6 +113,7 @@
* type.
* @return A [Deferred] representing the asynchronous update operation.
*/
+ @AnyThread
public fun setConfigAsync(
type: Type,
config: Config? = null,
@@ -121,12 +126,20 @@
// 3A
/**
- * Asynchronously sets the torch (flashlight) state.
+ * Asynchronously sets the torch (flashlight) to ON state.
*
- * @param enabled True to enable the torch, false to disable it.
* @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
*/
- public suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
+ @AnyThread public fun setTorchOnAsync(): Deferred<Result3A>
+
+ /**
+ * Asynchronously sets the torch (flashlight) state to OFF state.
+ *
+ * @param aeMode The [AeMode] to set while setting the torch value. See
+ * [CameraGraph.Session.setTorchOff] for details.
+ * @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
+ */
+ @AnyThread public fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A>
/**
* Asynchronously starts a 3A (Auto Exposure, Auto Focus, Auto White Balance) operation with the
@@ -143,7 +156,8 @@
* [CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS].
* @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
*/
- public suspend fun startFocusAndMeteringAsync(
+ @AnyThread
+ public fun startFocusAndMeteringAsync(
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null,
@@ -159,7 +173,7 @@
*
* @return A [Deferred] representing the asynchronous operation and its result ([Result3A]).
*/
- public suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
+ @AnyThread public fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
// Capture
/**
@@ -171,7 +185,8 @@
* @param flashMode The flash mode (from [ImageCapture.FlashMode]).
* @return A list of [Deferred] objects, one for each capture in the sequence.
*/
- public suspend fun issueSingleCaptureAsync(
+ @AnyThread
+ public fun issueSingleCaptureAsync(
captureSequence: List<CaptureConfig>,
@ImageCapture.CaptureMode captureMode: Int,
@ImageCapture.FlashType flashType: Int,
@@ -186,7 +201,8 @@
*
* @see [CameraGraph.Session.update3A]
*/
- public suspend fun update3aRegions(
+ @AnyThread
+ public fun update3aRegions(
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null,
@@ -202,6 +218,7 @@
private val capturePipeline: CapturePipeline,
private val state: UseCaseCameraState,
private val useCaseGraphConfig: UseCaseGraphConfig,
+ private val threads: UseCaseThreads,
) : UseCaseCameraRequestControl {
private val graph = useCaseGraphConfig.graph
@@ -214,9 +231,7 @@
var template: RequestTemplate? = null,
)
- @GuardedBy("lock")
private val infoBundleMap = mutableMapOf<UseCaseCameraRequestControl.Type, InfoBundle>()
- private val lock = Any()
override fun setParametersAsync(
type: UseCaseCameraRequestControl.Type,
@@ -224,15 +239,17 @@
optionPriority: Config.OptionPriority,
): Deferred<Unit> =
runIfNotClosed {
- synchronized(lock) {
- debug { "[$type] Add request option: $values" }
- infoBundleMap
- .getOrPut(type) { InfoBundle() }
- .options
- .addAllCaptureRequestOptionsWithPriority(values, optionPriority)
- infoBundleMap.merge()
+ threads.confineDeferred {
+ debug {
+ "UseCaseCameraRequestControlImpl#setParametersAsync: [$type] values = $values" +
+ ", optionPriority = $optionPriority"
}
- .updateCameraStateAsync()
+ infoBundleMap
+ .getOrPut(type) { InfoBundle() }
+ .options
+ .addAllCaptureRequestOptionsWithPriority(values, optionPriority)
+ infoBundleMap.merge().updateCameraStateAsync()
+ }
} ?: canceledResult
override fun setConfigAsync(
@@ -245,38 +262,48 @@
sessionConfig: SessionConfig?,
): Deferred<Unit> =
runIfNotClosed {
- synchronized(lock) {
- debug { "[$type] Set config: ${config?.toParameters()}" }
- infoBundleMap[type] =
- InfoBundle(
- Camera2ImplConfig.Builder().apply {
- config?.let { insertAllOptions(it) }
- },
- tags.toMutableMap(),
- listeners.toMutableSet(),
- template,
- )
- infoBundleMap.merge()
+ threads.confineDeferred {
+ debug {
+ "UseCaseCameraRequestControlImpl#setConfigAsync:" +
+ " [$type] config params = ${config?.toParameters()}"
}
- .updateCameraStateAsync(
- streams = streams,
- sessionConfig = sessionConfig,
- )
+ infoBundleMap[type] =
+ InfoBundle(
+ Camera2ImplConfig.Builder().apply { config?.let { insertAllOptions(it) } },
+ tags.toMutableMap(),
+ listeners.toMutableSet(),
+ template,
+ )
+ infoBundleMap
+ .merge()
+ .updateCameraStateAsync(
+ streams = streams,
+ sessionConfig = sessionConfig,
+ )
+ }
} ?: canceledResult
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> =
+ override fun setTorchOnAsync(): Deferred<Result3A> =
runIfNotClosed {
- useGraphSessionOrFailed {
- it.setTorch(
- when (enabled) {
- true -> TorchState.ON
- false -> TorchState.OFF
- }
- )
+ threads.confineDeferredSuspend {
+ debug { "UseCaseCameraRequestControlImpl#setTorchOnAsync" }
+ useGraphSessionOrFailed { it.setTorchOn() }
}
} ?: submitFailedResult
- override suspend fun startFocusAndMeteringAsync(
+ override fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> =
+ runIfNotClosed {
+ threads.confineDeferredSuspend {
+ debug { "UseCaseCameraRequestControlImpl#setTorchOffAsync" }
+ useGraphSessionOrFailed {
+ it.setTorchOff(
+ aeMode = aeMode,
+ )
+ }
+ }
+ } ?: submitFailedResult
+
+ override fun startFocusAndMeteringAsync(
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
@@ -287,47 +314,59 @@
timeLimitNs: Long,
): Deferred<Result3A> =
runIfNotClosed {
- useGraphSessionOrFailed {
- it.lock3A(
- aeRegions = aeRegions,
- afRegions = afRegions,
- awbRegions = awbRegions,
- aeLockBehavior = aeLockBehavior,
- afLockBehavior = afLockBehavior,
- awbLockBehavior = awbLockBehavior,
- afTriggerStartAeMode = afTriggerStartAeMode,
- convergedTimeLimitNs = timeLimitNs,
- lockedTimeLimitNs = timeLimitNs
- )
+ threads.confineDeferredSuspend {
+ debug { "UseCaseCameraRequestControlImpl#startFocusAndMeteringAsync" }
+ useGraphSessionOrFailed {
+ it.lock3A(
+ aeRegions = aeRegions,
+ afRegions = afRegions,
+ awbRegions = awbRegions,
+ aeLockBehavior = aeLockBehavior,
+ afLockBehavior = afLockBehavior,
+ awbLockBehavior = awbLockBehavior,
+ afTriggerStartAeMode = afTriggerStartAeMode,
+ convergedTimeLimitNs = timeLimitNs,
+ lockedTimeLimitNs = timeLimitNs
+ )
+ }
}
} ?: submitFailedResult
- override suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A> =
+ override fun cancelFocusAndMeteringAsync(): Deferred<Result3A> =
runIfNotClosed {
- useGraphSessionOrFailed { it.unlock3A(ae = true, af = true, awb = true) }.await()
+ threads.confineDeferredSuspend {
+ debug { "UseCaseCameraRequestControlImpl#cancelFocusAndMeteringAsync" }
- useGraphSessionOrFailed {
- it.update3A(
- aeRegions = METERING_REGIONS_DEFAULT.asList(),
- afRegions = METERING_REGIONS_DEFAULT.asList(),
- awbRegions = METERING_REGIONS_DEFAULT.asList()
- )
+ useGraphSessionOrFailed { it.unlock3A(ae = true, af = true, awb = true) }.await()
+
+ useGraphSessionOrFailed {
+ it.update3A(
+ aeRegions = METERING_REGIONS_DEFAULT.asList(),
+ afRegions = METERING_REGIONS_DEFAULT.asList(),
+ awbRegions = METERING_REGIONS_DEFAULT.asList()
+ )
+ }
}
} ?: submitFailedResult
- override suspend fun issueSingleCaptureAsync(
+ override fun issueSingleCaptureAsync(
captureSequence: List<CaptureConfig>,
@ImageCapture.CaptureMode captureMode: Int,
@ImageCapture.FlashType flashType: Int,
@ImageCapture.FlashMode flashMode: Int,
): List<Deferred<Void?>> =
runIfNotClosed {
- if (captureSequence.hasInvalidSurface()) {
- failedResults(captureSequence.size, "Capture request failed due to invalid surface")
- }
+ threads.confineDeferredListSuspend(captureSequence.size) {
+ debug { "UseCaseCameraRequestControlImpl#issueSingleCaptureAsync" }
- synchronized(lock) { infoBundleMap.merge() }
- .let { infoBundle ->
+ if (captureSequence.hasInvalidSurface()) {
+ failedResults(
+ captureSequence.size,
+ "Capture request failed due to invalid surface"
+ )
+ }
+
+ infoBundleMap.merge().let { infoBundle ->
debug {
"UseCaseCameraRequestControl: Submitting still captures to capture pipeline"
}
@@ -340,24 +379,28 @@
flashMode = flashMode,
)
}
+ }
}
?: failedResults(
captureSequence.size,
"Capture request is cancelled on closed CameraGraph"
)
- override suspend fun update3aRegions(
+ override fun update3aRegions(
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?
): Deferred<Result3A> =
runIfNotClosed {
- useGraphSessionOrFailed {
- it.update3A(
- aeRegions = aeRegions ?: METERING_REGIONS_DEFAULT.asList(),
- afRegions = afRegions ?: METERING_REGIONS_DEFAULT.asList(),
- awbRegions = awbRegions ?: METERING_REGIONS_DEFAULT.asList()
- )
+ threads.confineDeferredSuspend {
+ debug { "UseCaseCameraRequestControlImpl#update3aRegions" }
+ useGraphSessionOrFailed {
+ it.update3A(
+ aeRegions = aeRegions ?: METERING_REGIONS_DEFAULT.asList(),
+ afRegions = afRegions ?: METERING_REGIONS_DEFAULT.asList(),
+ awbRegions = awbRegions ?: METERING_REGIONS_DEFAULT.asList()
+ )
+ }
}
} ?: submitFailedResult
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 54adf25..aa924ea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.integration.impl
import android.content.Context
+import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
import android.hardware.camera2.CaptureRequest
@@ -29,6 +30,7 @@
import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode
+import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.AT_LEAST
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.CameraStream
@@ -44,34 +46,45 @@
import androidx.camera.camera2.pipe.integration.adapter.SupportedSurfaceCombination
import androidx.camera.camera2.pipe.integration.adapter.ZslControl
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.CaptureSessionStuckQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCameraDeviceOnCameraGraphCloseQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnDisconnectQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnVideoQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
import androidx.camera.camera2.pipe.integration.compat.quirk.DisableAbortCapturesOnStopWithSessionProcessorQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.FinalizeSessionOnCloseQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk
import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsOverride
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.DynamicRange
+import androidx.camera.core.ImageCapture
import androidx.camera.core.MirrorMode
+import androidx.camera.core.Preview
import androidx.camera.core.UseCase
+import androidx.camera.core.concurrent.CameraCoordinator
+import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraMode
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.PreviewConfig
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionConfig.OutputConfig.SURFACE_GROUP_ID_NONE
import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.stabilization.StabilizationMode
+import androidx.camera.core.streamsharing.StreamSharing
+import androidx.camera.core.streamsharing.StreamSharingConfig
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.Deferred
@@ -111,6 +124,7 @@
@Inject
constructor(
private val cameraPipe: CameraPipe,
+ @GuardedBy("lock") private val cameraCoordinator: CameraCoordinator,
private val callbackMap: CameraCallbackMap,
private val requestListener: ComboRequestListener,
private val cameraConfig: CameraConfig,
@@ -122,7 +136,6 @@
private val camera2CameraControl: Camera2CameraControl,
private val cameraStateAdapter: CameraStateAdapter,
private val cameraQuirks: CameraQuirks,
- private val cameraGraphFlags: CameraGraph.Flags,
private val cameraInternal: Provider<CameraInternal>,
private val useCaseThreads: Provider<UseCaseThreads>,
private val cameraInfoInternal: Provider<CameraInfoInternal>,
@@ -171,6 +184,8 @@
)
}
+ private val dynamicRangeResolver = DynamicRangeResolver(cameraProperties.metadata)
+
@Volatile private var _activeComponent: UseCaseCameraComponent? = null
public val camera: UseCaseCamera?
get() = _activeComponent?.getUseCaseCamera()
@@ -602,7 +617,7 @@
return activeSurfaces > 0 &&
with(attachedUseCases.withoutMetering()) {
(onlyVideoCapture() || requireMeteringRepeating()) &&
- supportMeteringCombination()
+ isMeteringCombinationSupported()
}
}
return false
@@ -624,7 +639,7 @@
return activeSurfaces == 0 ||
with(attachedUseCases.withoutMetering()) {
!(onlyVideoCapture() || requireMeteringRepeating()) ||
- !supportMeteringCombination()
+ !isMeteringCombinationSupported()
}
}
return false
@@ -649,7 +664,6 @@
requestListener,
cameraConfig,
cameraQuirks,
- cameraGraphFlags,
zslControl,
templateParamsOverride,
isExtensions,
@@ -664,46 +678,140 @@
}
}
- private fun Collection<UseCase>.supportMeteringCombination(): Boolean {
- val useCases = this.toMutableList().apply { add(meteringRepeating) }
+ private fun Collection<UseCase>.isMeteringCombinationSupported(): Boolean {
if (meteringRepeating.attachedSurfaceResolution == null) {
meteringRepeating.setupSession()
}
- return isCombinationSupported(useCases).also {
- Log.debug { "Combination of $useCases is supported: $it" }
+
+ val attachedSurfaceInfoList = getAttachedSurfaceInfoList()
+
+ if (attachedSurfaceInfoList.isEmpty()) {
+ return false
}
+
+ val sessionSurfacesConfigs = getSessionSurfacesConfigs()
+
+ return supportedSurfaceCombination
+ .checkSupported(
+ SupportedSurfaceCombination.FeatureSettings(
+ getCameraMode(),
+ getRequiredMaxBitDepth(attachedSurfaceInfoList),
+ isPreviewStabilizationOn(),
+ isUltraHdrOn()
+ ),
+ mutableListOf<SurfaceConfig>().apply {
+ addAll(sessionSurfacesConfigs)
+ add(createMeteringRepeatingSurfaceConfig())
+ }
+ )
+ .also {
+ Log.debug {
+ "Combination of $sessionSurfacesConfigs + $meteringRepeating is supported: $it"
+ }
+ }
}
- private fun isCombinationSupported(currentUseCases: Collection<UseCase>): Boolean {
- val surfaceConfigs =
- currentUseCases.map { useCase ->
- // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution
- // is
- // implemented.
- supportedSurfaceCombination.transformSurfaceConfig(
- CameraMode.DEFAULT,
- useCase.imageFormat,
- useCase.attachedSurfaceResolution!!
+ private fun getCameraMode(): Int {
+ synchronized(lock) {
+ if (
+ cameraCoordinator.cameraOperatingMode ==
+ CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT
+ ) {
+ return CameraMode.CONCURRENT_CAMERA
+ }
+ }
+
+ return CameraMode.DEFAULT
+ }
+
+ private fun getRequiredMaxBitDepth(attachedSurfaceInfoList: List<AttachedSurfaceInfo>): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ dynamicRangeResolver
+ .resolveAndValidateDynamicRanges(
+ attachedSurfaceInfoList,
+ listOf(meteringRepeating.currentConfig),
+ listOf(0)
+ )
+ .forEach { (_, u) ->
+ if (u.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+ return DynamicRange.BIT_DEPTH_10_BIT
+ }
+ }
+ }
+
+ return DynamicRange.BIT_DEPTH_8_BIT
+ }
+
+ private fun Collection<UseCase>.getAttachedSurfaceInfoList(): List<AttachedSurfaceInfo> =
+ mutableListOf<AttachedSurfaceInfo>().apply {
+ [email protected] { useCase ->
+ val surfaceResolution = useCase.attachedSurfaceResolution
+ val streamSpec = useCase.attachedStreamSpec
+
+ // When collecting the info, the UseCases might be unbound to make these info
+ // become null.
+ if (surfaceResolution == null || streamSpec == null) {
+ Log.warn { "Invalid surface resolution or stream spec is found." }
+ clear()
+ return@apply
+ }
+
+ val surfaceConfig =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ useCase.currentConfig.inputFormat,
+ surfaceResolution
+ )
+ add(
+ AttachedSurfaceInfo.create(
+ surfaceConfig,
+ useCase.currentConfig.inputFormat,
+ surfaceResolution,
+ streamSpec.dynamicRange,
+ useCase.getCaptureTypes(),
+ streamSpec.implementationOptions ?: MutableOptionsBundle.create(),
+ useCase.currentConfig.getTargetFrameRate(null)
+ )
)
}
+ }
- var isPreviewStabilizationOn = false
- for (useCase in currentUseCases) {
- if (useCase.currentConfig is PreviewConfig) {
- isPreviewStabilizationOn =
- useCase.currentConfig.previewStabilizationMode == StabilizationMode.ON
+ private fun UseCase.getCaptureTypes() =
+ if (this is StreamSharing) {
+ (currentConfig as StreamSharingConfig).captureTypes
+ } else {
+ listOf(currentConfig.captureType)
+ }
+
+ private fun Collection<UseCase>.isPreviewStabilizationOn() =
+ filterIsInstance<Preview>().firstOrNull()?.currentConfig?.previewStabilizationMode ==
+ StabilizationMode.ON
+
+ private fun Collection<UseCase>.isUltraHdrOn() =
+ filterIsInstance<ImageCapture>().firstOrNull()?.currentConfig?.inputFormat ==
+ ImageFormat.JPEG_R
+
+ private fun Collection<UseCase>.getSessionSurfacesConfigs(): List<SurfaceConfig> =
+ mutableListOf<SurfaceConfig>().apply {
+ [email protected] { useCase ->
+ useCase.sessionConfig.surfaces.forEach { deferrableSurface ->
+ add(
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ useCase.currentConfig.inputFormat,
+ deferrableSurface.prescribedSize
+ )
+ )
+ }
}
}
- return supportedSurfaceCombination.checkSupported(
- SupportedSurfaceCombination.FeatureSettings(
- CameraMode.DEFAULT,
- DynamicRange.BIT_DEPTH_8_BIT,
- isPreviewStabilizationOn
- ),
- surfaceConfigs
+ private fun createMeteringRepeatingSurfaceConfig() =
+ supportedSurfaceCombination.transformSurfaceConfig(
+ getCameraMode(),
+ meteringRepeating.imageFormat,
+ meteringRepeating.attachedSurfaceResolution!!
)
- }
private fun Collection<UseCase>.surfaceCount(): Int =
ValidatingBuilder().let { validatingBuilder ->
@@ -779,7 +887,6 @@
requestListener: ComboRequestListener,
cameraConfig: CameraConfig,
cameraQuirks: CameraQuirks,
- cameraGraphFlags: CameraGraph.Flags?,
zslControl: ZslControl,
templateParamsOverride: TemplateParamsOverride,
isExtensions: Boolean = false,
@@ -878,51 +985,8 @@
}
}
}
- val shouldCloseCaptureSessionOnDisconnect =
- if (isExtensions) {
- true
- } else if (CameraQuirks.isImmediateSurfaceReleaseAllowed()) {
- // If we can release Surfaces immediately, we'll finalize the session when the
- // camera graph is closed (through FinalizeSessionOnCloseQuirk), and thus we
- // won't
- // need to explicitly close the capture session.
- false
- } else {
- if (
- cameraQuirks.quirks.contains(CloseCaptureSessionOnVideoQuirk::class.java) &&
- containsVideo
- ) {
- true
- } else {
- DeviceQuirks[CloseCaptureSessionOnDisconnectQuirk::class.java] != null
- }
- }
- val shouldCloseCameraDeviceOnClose =
- DeviceQuirks[CloseCameraDeviceOnCameraGraphCloseQuirk::class.java] != null
- val shouldAbortCapturesOnStop =
- if (
- isExtensions &&
- DeviceQuirks[
- DisableAbortCapturesOnStopWithSessionProcessorQuirk::class.java] != null
- ) {
- false
- } else {
- /** @see [CameraGraph.Flags.abortCapturesOnStop] */
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- }
- val combinedFlags =
- cameraGraphFlags?.copy(
- abortCapturesOnStop = shouldAbortCapturesOnStop,
- quirkCloseCaptureSessionOnDisconnect = shouldCloseCaptureSessionOnDisconnect,
- quirkCloseCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
- )
- ?: CameraGraph.Flags(
- abortCapturesOnStop = shouldAbortCapturesOnStop,
- quirkCloseCaptureSessionOnDisconnect =
- shouldCloseCaptureSessionOnDisconnect,
- quirkCloseCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
- )
+ val combinedFlags = createCameraGraphFlags(cameraQuirks, containsVideo, isExtensions)
// Set video stabilization mode to capture request
var videoStabilizationMode = CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_OFF
@@ -996,5 +1060,75 @@
): OutputStream.StreamUseHint? {
return mapping[deferrableSurface]?.let { OutputStream.StreamUseHint(it) }
}
+
+ private fun createCameraGraphFlags(
+ cameraQuirks: CameraQuirks,
+ containsVideo: Boolean,
+ isExtensions: Boolean,
+ ): CameraGraph.Flags {
+ if (cameraQuirks.quirks.contains(CaptureSessionStuckQuirk::class.java)) {
+ Log.debug { "CameraPipe should be enabling CaptureSessionStuckQuirk by default" }
+ }
+ // TODO(b/276354253): Set quirkWaitForRepeatingRequestOnDisconnect flag for overrides.
+
+ // TODO(b/277310425): When creating a CameraGraph, this flag should be turned OFF when
+ // this behavior is not needed based on the use case interaction and the device on
+ // which the test is running.
+ val shouldFinalizeSessionOnCloseBehavior = FinalizeSessionOnCloseQuirk.getBehavior()
+
+ val shouldCloseCaptureSessionOnDisconnect =
+ when {
+ isExtensions -> true
+ // If we can release Surfaces immediately, we'll finalize the session when the
+ // camera graph is closed (through FinalizeSessionOnCloseQuirk), and thus we
+ // won't need to explicitly close the capture session.
+ CameraQuirks.isImmediateSurfaceReleaseAllowed() -> false
+ cameraQuirks.quirks.contains(CloseCaptureSessionOnVideoQuirk::class.java) &&
+ containsVideo -> true
+ DeviceQuirks[CloseCaptureSessionOnDisconnectQuirk::class.java] != null -> true
+ else -> false
+ }
+
+ val shouldCloseCameraDeviceOnClose =
+ DeviceQuirks[CloseCameraDeviceOnCameraGraphCloseQuirk::class.java] != null
+
+ val shouldAbortCapturesOnStop =
+ when {
+ isExtensions &&
+ DeviceQuirks[
+ DisableAbortCapturesOnStopWithSessionProcessorQuirk::class.java] !=
+ null -> false
+ /** @see [CameraGraph.Flags.abortCapturesOnStop] */
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> true
+ else -> false
+ }
+
+ val repeatingRequestsToCompleteBeforeNonRepeatingCapture =
+ if (
+ cameraQuirks.quirks.contains(
+ QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk::class.java
+ )
+ ) {
+ 1u
+ } else {
+ 0u
+ }
+
+ return CameraGraph.Flags(
+ abortCapturesOnStop = shouldAbortCapturesOnStop,
+ awaitRepeatingRequestBeforeCapture =
+ CameraGraph.RepeatingRequestRequirementsBeforeCapture(
+ repeatingFramesToComplete =
+ repeatingRequestsToCompleteBeforeNonRepeatingCapture,
+ // TODO: b/364491700 - use CompletionBehavior.EXACT to disable CameraPipe
+ // internal workaround when not required. See
+ // Camera2Quirks.getRepeatingRequestFrameCountForCapture for details.
+ completionBehavior = AT_LEAST,
+ ),
+ closeCaptureSessionOnDisconnect = shouldCloseCaptureSessionOnDisconnect,
+ closeCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
+ finalizeSessionOnCloseBehavior = shouldFinalizeSessionOnCloseBehavior,
+ )
+ }
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseThreads.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseThreads.kt
index 205defa..7799902 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseThreads.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseThreads.kt
@@ -17,12 +17,17 @@
package androidx.camera.camera2.pipe.integration.impl
import androidx.annotation.VisibleForTesting
+import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import java.util.concurrent.Executor
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
/** Collection of threads and scope(s) that have been configured and tuned. */
public class UseCaseThreads(
@@ -36,4 +41,102 @@
public var sequentialScope: CoroutineScope =
CoroutineScope(scope.coroutineContext + SupervisorJob() + sequentialDispatcher)
@VisibleForTesting set
+
+ /**
+ * Confines a [Deferred] returning `block` parameter to [UseCaseThreads.sequentialScope] for the
+ * purpose of thread confinement.
+ *
+ * Cancelling the `Deferred` returned from this function does not cancel the `Deferred` returned
+ * by `block`.
+ *
+ * @return A [Deferred] which is completed as per the [Deferred] returned from [block] via
+ * [propagateTo].
+ */
+ public inline fun <T> confineDeferred(crossinline block: () -> Deferred<T>): Deferred<T> {
+ val signal = CompletableDeferred<T>()
+ sequentialScope.launch { block().propagateTo(signal) }
+ return signal
+ }
+
+ /**
+ * Confines a [Deferred] list returning `block` parameter to [UseCaseThreads.sequentialScope]
+ * for the purpose of thread confinement and returns a new `Deferred` list with one-to-one
+ * mapping.
+ *
+ * Cancelling a `Deferred` returned from this function does not cancel the corresponding
+ * `Deferred` returned by `block`.
+ *
+ * @param size Size of the list returned from [block], the list returned via this function will
+ * also have th same size.
+ * @return A list of [Deferred] where each element is completed as per the corresponding
+ * [Deferred] in the list returned from [block].
+ */
+ public inline fun <T> confineDeferredList(
+ size: Int,
+ crossinline block: () -> List<Deferred<T>>
+ ): List<Deferred<T>> {
+ val deferredList = List(size) { CompletableDeferred<T>() }
+ sequentialScope.launch {
+ block().forEachIndexed { index, deferred -> deferred.propagateTo(deferredList[index]) }
+ }
+ return deferredList
+ }
+
+ /**
+ * Confines a [Deferred] returning suspendable `block` parameter to
+ * [UseCaseThreads.sequentialScope] for the purpose of thread confinement.
+ *
+ * Cancelling the `Deferred` returned from this function does not cancel the `Deferred` returned
+ * by `block`.
+ *
+ * @return A [Deferred] which is completed as per the [Deferred] returned from [block] via
+ * [propagateTo].
+ */
+ public inline fun <T> confineDeferredSuspend(
+ crossinline block: suspend () -> Deferred<T>
+ ): Deferred<T> {
+ val signal = CompletableDeferred<T>()
+ sequentialScope.launch { block().propagateTo(signal) }
+ return signal
+ }
+
+ /**
+ * Confines a [Deferred] list returning suspendable `block` parameter to
+ * [UseCaseThreads.sequentialScope] for the purpose of thread confinement and returns a new
+ * `Deferred` list with one-to-one mapping.
+ *
+ * Cancelling a `Deferred` returned from this function does not cancel the corresponding
+ * `Deferred` returned by `block`.
+ *
+ * @param size Size of the list returned from [block], the list returned via this function will
+ * also have th same size.
+ * @return A list of [Deferred] where each element is completed as per the corresponding
+ * [Deferred] in the list returned from [block].
+ */
+ public inline fun <T> confineDeferredListSuspend(
+ size: Int,
+ crossinline block: suspend () -> List<Deferred<T>>
+ ): List<Deferred<T>> {
+ val deferredList = List(size) { CompletableDeferred<T>() }
+ sequentialScope.launch {
+ block().forEachIndexed { index, deferred -> deferred.propagateTo(deferredList[index]) }
+ }
+ return deferredList
+ }
+
+ /**
+ * Confines the `block` parameter to [UseCaseThreads.sequentialScope] for the purpose of thread
+ * confinement.
+ *
+ * This is mainly just a syntax sugar matching [CoroutineScope.launch] to align with the other
+ * confining methods in this class.
+ *
+ * @return A [Job] which is returned via [CoroutineScope.launch] used under-the-hood.
+ */
+ public inline fun confineLaunch(crossinline block: suspend () -> Unit): Job =
+ sequentialScope.launch { block() }
+
+ // TODO - Add convenience function like confineAsync to mimic scope.async while still following
+ // the above pattern when being used for thread confinement. This will keep things clear if we
+ // want to replace sequentialScope with something else in future for thread confinement.
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
index 2437ab8..dd53017 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/ZoomControl.kt
@@ -35,9 +35,7 @@
import dagger.multibindings.IntoSet
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
public const val DEFAULT_ZOOM_RATIO: Float = 1.0f
@@ -45,7 +43,6 @@
public class ZoomControl
@Inject
constructor(
- private val threads: UseCaseThreads,
private val zoomCompat: ZoomCompat,
) : UseCaseCameraControl {
// NOTE: minZoom may be lower than 1.0
@@ -154,16 +151,12 @@
}
updateSignal = signal
- threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
- setZoomState(zoomState)
+ setZoomState(zoomState)
- _requestControl?.let {
- zoomCompat.applyAsync(zoomState.zoomRatio, it).propagateTo(signal)
- }
- ?: signal.completeExceptionally(
- CameraControl.OperationCanceledException("Camera is not active.")
- )
- }
+ requestControl?.let { zoomCompat.applyAsync(zoomState.zoomRatio, it).propagateTo(signal) }
+ ?: signal.completeExceptionally(
+ CameraControl.OperationCanceledException("Camera is not active.")
+ )
/**
* TODO: Use signal.asListenableFuture() directly. Deferred<T>.asListenableFuture() returns
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
index 63da8d3..41940e0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraControl.kt
@@ -30,7 +30,6 @@
import androidx.camera.core.impl.utils.futures.Futures
import androidx.core.util.Preconditions
import com.google.common.util.concurrent.ListenableFuture
-import kotlinx.coroutines.async
/**
* An class that provides ability to interoperate with the [android.hardware.camera2] APIs.
@@ -146,9 +145,7 @@
private fun updateAsync(tag: String): ListenableFuture<Void?> =
Futures.nonCancellationPropagating(
- threads.sequentialScope
- .async { compat.applyAsync(requestControl).await() }
- .asListenableFuture(tag)
+ compat.applyAsync(requestControl).asListenableFuture(tag)
)
public companion object {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index 355bea9..3553a75 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -35,7 +35,6 @@
import androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
-import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
@@ -73,7 +72,7 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraInfoAdapterTest {
- private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+ private val zoomControl = ZoomControl(FakeZoomCompat())
private val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
@get:Rule
@@ -153,7 +152,7 @@
@Test
fun canReturnDefaultZoomState() {
// make new ZoomControl to test first-time initialization scenario
- val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+ val zoomControl = ZoomControl(FakeZoomCompat())
val cameraInfoAdapter = createCameraInfoAdapter(zoomControl = zoomControl)
assertWithMessage("zoomState did not return default zoom ratio successfully")
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
index 9b27a11..28b47b6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
@@ -46,6 +46,23 @@
}
@Test
+ fun propagateTransformedCompleteResult(): Unit = runBlocking {
+ // Arrange.
+ val resultValue = 123
+ val resultValueTransformed = resultValue.toString()
+
+ val sourceDeferred = CompletableDeferred<Int>()
+ val resultDeferred = CompletableDeferred<String>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.complete(resultValue)
+
+ // Assert.
+ assertThat(resultDeferred.await()).isEqualTo(resultValueTransformed)
+ }
+
+ @Test
fun propagateCancelResult() {
// Arrange.
val sourceDeferred = CompletableDeferred<Unit>()
@@ -59,6 +76,20 @@
assertThat(resultDeferred.isCancelled).isTrue()
}
+ @Test
+ fun propagateCancelResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+ // Act.
+ sourceDeferred.cancel()
+
+ // Assert.
+ assertThat(resultDeferred.isCancelled).isTrue()
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun propagateExceptionResult() {
@@ -74,4 +105,20 @@
// Assert.
assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
}
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun propagateExceptionResult_whenTransformFunctionIsUsed() {
+ // Arrange.
+ val sourceDeferred = CompletableDeferred<Unit>()
+ val resultDeferred = CompletableDeferred<Unit>()
+ sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+ val testThrowable = Throwable()
+
+ // Act.
+ sourceDeferred.completeExceptionally(testThrowable)
+
+ // Assert.
+ assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
index a933a0a..d9e49d0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
@@ -168,6 +168,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.setRepeating(requestToSet, callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
@@ -209,6 +211,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.submit(mutableListOf(requestToSubmit), callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirkTest.kt
new file mode 100644
index 0000000..f338695
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/QuickSuccessiveImageCaptureFailsRepeatingRequestQuirkTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_3
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.Quirks
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowBuild
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = 21)
+class QuickSuccessiveImageCaptureFailsRepeatingRequestQuirkTest(
+ private val brand: String,
+ private val cameraHwLevel: Int,
+ private val isEnabledExpected: Boolean
+) {
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(
+ name = "Model: {0}, lens facing: {1}, external ae mode: {2}, enabled: {3}"
+ )
+ fun data() =
+ listOf(
+ arrayOf("Samsung", INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, true),
+ arrayOf("Samsung", INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, false),
+ arrayOf("Samsung", INFO_SUPPORTED_HARDWARE_LEVEL_FULL, false),
+ arrayOf("Samsung", INFO_SUPPORTED_HARDWARE_LEVEL_3, false),
+ arrayOf("Samsung", INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL, false),
+ arrayOf("Google", INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, false),
+ )
+ }
+
+ private fun getCameraQuirks(
+ cameraHwLevel: Int,
+ ): Quirks {
+ val characteristicsMap =
+ mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+ .apply { this[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL] = cameraHwLevel }
+ .toMap()
+
+ val cameraCharacteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+ val shadowCharacteristics =
+ Shadow.extract<ShadowCameraCharacteristics>(cameraCharacteristics)
+ characteristicsMap.forEach { entry -> shadowCharacteristics.set(entry.key, entry.value) }
+
+ val cameraMetadata = FakeCameraMetadata(characteristicsMap)
+
+ return CameraQuirks(
+ cameraMetadata,
+ StreamConfigurationMapCompat(
+ StreamConfigurationMapBuilder.newBuilder().build(),
+ OutputSizesCorrector(
+ cameraMetadata,
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
+ ),
+ )
+ .quirks
+ }
+
+ @Test
+ fun canEnableQuirkCorrectly() {
+ // Arrange
+ ShadowBuild.setBrand(brand)
+ val cameraQuirks = getCameraQuirks(cameraHwLevel)
+
+ // Act
+ val isEnabled =
+ cameraQuirks.contains(QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk::class.java)
+
+ // Verify
+ assertThat(isEnabled).isEqualTo(isEnabledExpected)
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrectorTest.kt
new file mode 100644
index 0000000..7c96dcd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/DisplaySizeCorrectorTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DisplaySizeCorrectorTest {
+ @Test
+ fun returnCorrectDisplaySizeForProblematicDevice() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "REDMI NOTE 8")
+ // See SmallDisplaySizeQuirk for the device display size
+ assertThat(DisplaySizeCorrector().displaySize).isEqualTo(Size(1080, 2340))
+ }
+
+ @Test
+ fun returnNullDisplaySizeForProblematicDevice() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "Fake-Model")
+ // See SmallDisplaySizeQuirk for the device display size
+ assertThat(DisplaySizeCorrector().displaySize).isNull()
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index f968130..c2a1158 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -136,8 +136,14 @@
val torchUpdateEventList = mutableListOf<Boolean>()
val setTorchSemaphore = Semaphore(0)
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
- torchUpdateEventList.add(enabled)
+ override fun setTorchOnAsync(): Deferred<Result3A> {
+ torchUpdateEventList.add(true)
+ setTorchSemaphore.release()
+ return CompletableDeferred(Result3A(Result3A.Status.OK))
+ }
+
+ override fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> {
+ torchUpdateEventList.add(false)
setTorchSemaphore.release()
return CompletableDeferred(Result3A(Result3A.Status.OK))
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index d0fa542..bc0d9db 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -19,21 +19,26 @@
import android.content.Context
import android.graphics.Point
import android.hardware.display.DisplayManager
+import android.os.Build
import android.util.Size
import android.view.Display
+import android.view.WindowManager
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
+import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowDisplay
import org.robolectric.shadows.ShadowDisplayManager
import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
+import org.robolectric.util.ReflectionHelpers
@Suppress("DEPRECATION") // getRealSize
@RunWith(RobolectricCameraPipeTestRunner::class)
@@ -231,4 +236,34 @@
// Assert
assertEquals(Size(1920, 1080), displayInfoManager.getPreviewSize())
}
+
+ @Test
+ fun canReturnFallbackPreviewSize640x480_displaySmallerThan320x240() {
+ // Arrange
+ val windowManager =
+ ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(16)
+ Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(16)
+
+ // Act & Assert
+ val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+ assertThat(displayInfoManager.getPreviewSize()).isEqualTo(Size(640, 480))
+ }
+
+ @Test
+ fun canReturnCorrectPreviewSize_fromDisplaySizeCorrector() {
+ // Arrange
+ val windowManager =
+ ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(16)
+ Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(16)
+
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-A127F")
+
+ // Act & Assert
+ val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+ assertThat(displayInfoManager.getPreviewSize()).isEqualTo(Size(1600, 720))
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index 5698804..873bf3d 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -449,6 +449,7 @@
),
state = fakeUseCaseCameraState,
useCaseGraphConfig = fakeUseCaseGraphConfig,
+ threads = fakeUseCaseThreads,
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
index a6f3eb7..feeadd3 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
@@ -88,6 +88,7 @@
capturePipeline = FakeCapturePipeline(),
state = fakeUseCaseCameraState,
useCaseGraphConfig = fakeUseCaseGraphConfig,
+ threads = useCaseThreads,
)
@After
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 37d015b..cfc6583 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -28,13 +28,13 @@
import android.os.Build
import android.util.Range
import android.util.Size
-import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode.Companion.HIGH_SPEED
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.integration.adapter.BlockingTestDeferrableSurface
+import androidx.camera.camera2.pipe.integration.adapter.CameraCoordinatorAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
import androidx.camera.camera2.pipe.integration.adapter.FakeTestUseCase
@@ -228,6 +228,67 @@
}
@Test
+ fun meteringRepeatingEnabled_whenPreviewEnabledWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageCapture = createImageCapture()
+ useCaseManager.attach(listOf(preview, imageCapture))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageCapture)
+
+ // Assert
+ val enabledUseCaseClasses =
+ useCaseManager.getRunningUseCasesForTest().map { it::class.java }
+ assertThat(enabledUseCaseClasses)
+ .containsExactly(
+ Preview::class.java,
+ ImageCapture::class.java,
+ MeteringRepeating::class.java
+ )
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenImageAnalysisAndPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ val imageAnalysis =
+ ImageAnalysis.Builder().build().apply {
+ setAnalyzer(useCaseThreads.backgroundExecutor) { image -> image.close() }
+ }
+ useCaseManager.attach(listOf(preview, imageAnalysis))
+
+ // Act
+ useCaseManager.activate(preview)
+ useCaseManager.activate(imageAnalysis)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview, imageAnalysis)
+ }
+
+ @Test
+ fun meteringRepeatingNotEnabled_whenOnlyPreviewWithNoSurfaceProvider() = runTest {
+ // Arrange
+ initializeUseCaseThreads(this)
+ val useCaseManager = createUseCaseManager()
+ val preview = createPreview(/* withSurfaceProvider= */ false)
+ useCaseManager.attach(listOf(preview))
+
+ // Act
+ useCaseManager.activate(preview)
+
+ // Assert
+ val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+ assertThat(enabledUseCases).containsExactly(preview)
+ }
+
+ @Test
fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() = runTest {
// Arrange
initializeUseCaseThreads(this)
@@ -615,21 +676,17 @@
val fakeCameraMetadata =
FakeCameraMetadata(cameraId = cameraId, characteristics = characteristicsMap)
val fakeCamera = FakeCamera()
+ val cameraPipe = CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext()))
return UseCaseManager(
- cameraPipe =
- CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext())),
- cameraConfig = CameraConfig(cameraId),
+ cameraPipe = cameraPipe,
+ cameraCoordinator = CameraCoordinatorAdapter(cameraPipe, cameraPipe.cameras()),
callbackMap = CameraCallbackMap(),
requestListener = ComboRequestListener(),
+ cameraConfig = CameraConfig(cameraId),
builder = useCaseCameraComponentBuilder,
cameraControl = fakeCamera.cameraControlInternal,
zslControl = ZslControlNoOpImpl(),
controls = controls as java.util.Set<UseCaseCameraControl>,
- cameraProperties =
- FakeCameraProperties(
- metadata = fakeCameraMetadata,
- cameraId = cameraId,
- ),
camera2CameraControl =
Camera2CameraControl.create(
FakeCamera2CameraControlCompat(),
@@ -637,8 +694,6 @@
ComboRequestListener()
),
cameraStateAdapter = CameraStateAdapter(),
- cameraGraphFlags = CameraGraph.Flags(),
- cameraInternal = { fakeCamera },
cameraQuirks =
CameraQuirks(
fakeCameraMetadata,
@@ -647,12 +702,18 @@
OutputSizesCorrector(fakeCameraMetadata, null)
)
),
- displayInfoManager =
- DisplayInfoManager(ApplicationProvider.getApplicationContext()),
- context = ApplicationProvider.getApplicationContext(),
+ cameraInternal = { fakeCamera },
+ useCaseThreads = { useCaseThreads },
cameraInfoInternal = { fakeCamera.cameraInfoInternal },
templateParamsOverride = templateParamsOverride,
- useCaseThreads = { useCaseThreads },
+ context = ApplicationProvider.getApplicationContext(),
+ cameraProperties =
+ FakeCameraProperties(
+ metadata = fakeCameraMetadata,
+ cameraId = cameraId,
+ ),
+ displayInfoManager =
+ DisplayInfoManager(ApplicationProvider.getApplicationContext()),
)
.also { useCaseManagerList.add(it) }
}
@@ -736,16 +797,18 @@
useCaseList.add(it)
}
- private fun createPreview(): Preview =
+ private fun createPreview(withSurfaceProvider: Boolean = true): Preview =
Preview.Builder()
.setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
.setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
.build()
.apply {
- setSurfaceProvider(
- CameraXExecutors.mainThreadExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider()
- )
+ if (withSurfaceProvider) {
+ setSurfaceProvider(
+ CameraXExecutors.mainThreadExecutor(),
+ SurfaceTextureProvider.createSurfaceTextureProvider()
+ )
+ }
}
.also {
it.simulateActivation()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
index bed7cf8..1275ceb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/ZoomControlTest.kt
@@ -67,9 +67,7 @@
@Before
fun setUp() {
zoomControl =
- ZoomControl(fakeUseCaseThreads, zoomCompat).apply {
- requestControl = FakeUseCaseCameraRequestControl()
- }
+ ZoomControl(zoomCompat).apply { requestControl = FakeUseCaseCameraRequestControl() }
}
@Test
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
index f3fe9a8..e7a993a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
@@ -29,7 +29,6 @@
import androidx.camera.camera2.pipe.OutputStatus
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.ABORTED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.FAILED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.TOTAL_CAPTURE_DONE
@@ -104,7 +103,11 @@
throw NotImplementedError("Not used in testing")
}
- override fun setTorch(torchState: TorchState): Deferred<Result3A> {
+ override fun setTorchOn(): Deferred<Result3A> {
+ throw NotImplementedError("Not used in testing")
+ }
+
+ override fun setTorchOff(aeMode: AeMode?): Deferred<Result3A> {
throw NotImplementedError("Not used in testing")
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 1b53a80..7d30d91 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -87,7 +87,7 @@
intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
)
- private val zoomControl = ZoomControl(useCaseThreads, FakeZoomCompat())
+ private val zoomControl = ZoomControl(FakeZoomCompat())
fun createCameraInfoAdapter(
cameraId: CameraId = CAMERA_ID_0,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 9da279b..6e51547 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -94,13 +94,15 @@
open class FakeUseCaseCameraRequestControl(
private val scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : UseCaseCameraRequestControl {
-
val addParameterCalls = mutableListOf<Map<CaptureRequest.Key<*>, Any>>()
var addParameterResult = CompletableDeferred(Unit)
var setConfigCalls = mutableListOf<RequestParameters>()
var setConfigResult = CompletableDeferred(Unit)
var setTorchResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
+ // TODO - Implement thread-safety in the functions annotated with @AnyThread in
+ // UseCaseCameraRequestControl
+
override fun setParametersAsync(
type: UseCaseCameraRequestControl.Type,
values: Map<CaptureRequest.Key<*>, Any>,
@@ -123,7 +125,11 @@
return CompletableDeferred(Unit)
}
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
+ override fun setTorchOnAsync(): Deferred<Result3A> {
+ return setTorchResult
+ }
+
+ override fun setTorchOffAsync(aeMode: AeMode): Deferred<Result3A> {
return setTorchResult
}
@@ -138,7 +144,7 @@
var focusAutoCompletesAfterTimeout = true
- override suspend fun startFocusAndMeteringAsync(
+ override fun startFocusAndMeteringAsync(
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
@@ -183,12 +189,12 @@
return focusMeteringResult
}
- override suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A> {
+ override fun cancelFocusAndMeteringAsync(): Deferred<Result3A> {
cancelFocusMeteringCallCount++
return cancelFocusMeteringResult
}
- override suspend fun issueSingleCaptureAsync(
+ override fun issueSingleCaptureAsync(
captureSequence: List<CaptureConfig>,
@ImageCapture.CaptureMode captureMode: Int,
@ImageCapture.FlashType flashType: Int,
@@ -197,7 +203,7 @@
return captureSequence.map { CompletableDeferred<Void?>(null).apply { complete(null) } }
}
- override suspend fun update3aRegions(
+ override fun update3aRegions(
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
index 63a3e39..78d0670 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
@@ -34,10 +34,8 @@
import androidx.camera.camera2.pipe.RequestFailure
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.media.ImageSource
-import kotlin.collections.removeFirst as removeFirstKt
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.withTimeout
/**
* This class creates a [CameraPipe] and [CameraGraph] instance using a [FakeCameraBackend].
@@ -166,7 +164,7 @@
}
}
- public suspend fun simulateNextFrame(
+ public fun simulateNextFrame(
advanceClockByNanos: Long = 33_366_666 // (2_000_000_000 / (60 / 1.001))
): FrameSimulator =
generateNextFrame().also {
@@ -174,7 +172,7 @@
it.simulateStarted(clockNanos)
}
- private suspend fun generateNextFrame(): FrameSimulator {
+ private fun generateNextFrame(): FrameSimulator {
val captureSequenceProcessor = cameraController.currentCaptureSequenceProcessor
check(captureSequenceProcessor != null) {
"simulateCameraStarted() must be called before frames can be created!"
@@ -183,16 +181,19 @@
// This checks the pending frame queue and polls for the next request. If no request is
// available it will suspend until the next interaction with the request processor.
if (pendingFrameQueue.isEmpty()) {
- val requestSequence =
- withTimeout(timeMillis = 250) { captureSequenceProcessor.nextRequestSequence() }
+ val captureSequence = captureSequenceProcessor.nextCaptureSequence()
+ checkNotNull(captureSequence) {
+ "Failed to simulate a CaptureSequence from $captureSequenceProcessor! Make sure " +
+ "Requests have been submitted or that the repeating Request has been set."
+ }
// Each sequence is processed as a group, and if a sequence contains multiple requests
// the list of requests is processed in order before polling the next sequence.
- for (request in requestSequence.captureRequestList) {
- pendingFrameQueue.add(FrameSimulator(request, requestSequence))
+ for (request in captureSequence.captureRequestList) {
+ pendingFrameQueue.add(FrameSimulator(request, captureSequence))
}
}
- return pendingFrameQueue.removeFirstKt()
+ return pendingFrameQueue.removeAt(0)
}
/** Utility function to simulate the production of a [FakeImage]s for one or more streams. */
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
new file mode 100644
index 0000000..7e718b1
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.CameraId
+import kotlinx.atomicfu.atomic
+
+/**
+ * Utility class for tracking and creating Fake [CameraId] instances for use in testing.
+ *
+ * These id's are intentionally non-numerical to help prevent code that may assume that camera2
+ * camera ids are parsable.
+ */
+public object FakeCameraIds {
+ private val fakeCameraIds = atomic(0)
+ public val default: CameraId = CameraId("FakeCamera-default")
+
+ public fun next(): CameraId = CameraId("FakeCamera-${fakeCameraIds.getAndIncrement()}")
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
index 1280187..38f6520 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
@@ -27,12 +27,6 @@
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.Metadata
import kotlin.reflect.KClass
-import kotlinx.atomicfu.atomic
-
-private val fakeCameraIds = atomic(0)
-
-internal fun nextFakeCameraId(): CameraId =
- CameraId("FakeCamera-${fakeCameraIds.incrementAndGet()}")
/** Utility class for interacting with objects that require pre-populated Metadata. */
public open class FakeMetadata(private val metadata: Map<Metadata.Key<*>, Any?> = emptyMap()) :
@@ -56,7 +50,7 @@
public class FakeCameraMetadata(
private val characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- cameraId: CameraId = nextFakeCameraId(),
+ cameraId: CameraId = FakeCameraIds.default,
override val keys: Set<CameraCharacteristics.Key<*>> = emptySet(),
override val requestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
override val resultKeys: Set<CaptureResult.Key<*>> = emptySet(),
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index 5010c90..161d428 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -25,48 +25,81 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamId
import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.withTimeout
/**
- * Fake implementation of a [CaptureSequenceProcessor] that passes events to a [Channel].
+ * Fake implementation of a [CaptureSequenceProcessor] that records events and simulates some low
+ * level behavior.
*
* This allows kotlin tests to check sequences of interactions that dispatch in the background
* without blocking between events.
*/
public class FakeCaptureSequenceProcessor(
- private val cameraId: CameraId = CameraId("test-camera"),
+ private val cameraId: CameraId = FakeCameraIds.default,
private val defaultTemplate: RequestTemplate = RequestTemplate(1)
) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
+ private val debugId = debugIds.incrementAndGet()
private val lock = Any()
private val sequenceIds = atomic(0)
- private val eventChannel = Channel<Event>(Channel.UNLIMITED)
- @GuardedBy("lock") private var pendingSequence: CompletableDeferred<FakeCaptureSequence>? = null
+ @GuardedBy("lock") private val captureQueue = mutableListOf<FakeCaptureSequence>()
- @GuardedBy("lock") private val queue: MutableList<FakeCaptureSequence> = mutableListOf()
+ @GuardedBy("lock") private var repeatingCapture: FakeCaptureSequence? = null
- @GuardedBy("lock") private var repeatingRequestSequence: FakeCaptureSequence? = null
+ @GuardedBy("lock") private var shutdown = false
- @GuardedBy("lock") private var _rejectRequests = false
+ @GuardedBy("lock") private val _events = mutableListOf<Event>()
- public var rejectRequests: Boolean
- get() = synchronized(lock) { _rejectRequests }
- set(value) {
- synchronized(lock) { _rejectRequests = value }
+ @GuardedBy("lock") private var nextEventIndex = 0
+ public val events: List<Event>
+ get() = synchronized(lock) { _events }
+
+ /** Get the next event from queue with an option to specify a timeout for tests. */
+ public fun nextEvent(): Event {
+ synchronized(lock) {
+ val eventIdx = nextEventIndex++
+ check(_events.size > 0) {
+ "Failed to get next event for $this, there have been no interactions."
+ }
+ check(eventIdx < _events.size) {
+ "Failed to get next event. Last event was ${events[eventIdx - 1]}"
+ }
+ return events[eventIdx]
}
+ }
- private var _surfaceMap: Map<StreamId, Surface> = emptyMap()
- public var surfaceMap: Map<StreamId, Surface>
- get() = synchronized(lock) { _surfaceMap }
+ public fun clearEvents() {
+ synchronized(lock) {
+ _events.clear()
+ nextEventIndex = 0
+ }
+ }
+
+ public var rejectBuild: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var rejectSubmit: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var surfaceMap: Map<StreamId, Surface> = emptyMap()
+ get() = synchronized(lock) { field }
set(value) =
synchronized(lock) {
- _surfaceMap = value
+ field = value
println("Configured surfaceMap for $this")
}
+ @Volatile public var throwOnBuild: Boolean = false
+
+ @Volatile public var throwOnSubmit: Boolean = false
+
+ @Volatile public var throwOnStop: Boolean = false
+
+ @Volatile public var throwOnAbort: Boolean = false
+
+ @Volatile public var throwOnShutdown: Boolean = false
+
override fun build(
isRepeating: Boolean,
requests: List<Request>,
@@ -75,136 +108,157 @@
listeners: List<Request.Listener>,
sequenceListener: CaptureSequenceListener
): FakeCaptureSequence? {
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultTemplate = defaultTemplate,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ throwTestExceptionIf(throwOnBuild)
+
+ val captureSequence =
+ FakeCaptureSequence.create(
+ cameraId,
+ isRepeating,
+ requests,
+ surfaceMap,
+ defaultTemplate,
+ defaultParameters,
+ requiredParameters,
+ listeners,
+ sequenceListener
+ )
+ synchronized(lock) {
+ if (rejectBuild || shutdown || captureSequence == null) {
+ println("$this: BuildRejected $captureSequence")
+ _events.add(BuildRejected(captureSequence))
+ return null
+ }
+ }
+ println("$this: Build $captureSequence")
+ return captureSequence
}
override fun submit(captureSequence: FakeCaptureSequence): Int {
- println("submit $captureSequence")
+ throwTestExceptionIf(throwOnSubmit)
synchronized(lock) {
- if (rejectRequests) {
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, rejected = true))
- .isSuccess
- )
+ if (rejectSubmit || shutdown) {
+ println("$this: SubmitRejected $captureSequence")
+ _events.add(SubmitRejected(captureSequence))
return -1
}
- queue.add(captureSequence)
+ captureQueue.add(captureSequence)
if (captureSequence.repeating) {
- repeatingRequestSequence = captureSequence
+ repeatingCapture = captureSequence
}
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, submit = true))
- .isSuccess
- )
- // If there is a non-null pending sequence, make sure we complete it here.
- pendingSequence?.also {
- pendingSequence = null
- it.complete(captureSequence)
- }
+ println("$this: Submit $captureSequence")
+ _events.add(Submit(captureSequence))
return sequenceIds.incrementAndGet()
}
}
override fun abortCaptures() {
+ throwTestExceptionIf(throwOnAbort)
+
val requestSequencesToAbort: List<FakeCaptureSequence>
synchronized(lock) {
- requestSequencesToAbort = queue.toList()
- queue.clear()
- check(eventChannel.trySend(Event(abort = true)).isSuccess)
+ println("$this: AbortCaptures")
+ _events.add(AbortCaptures)
+ requestSequencesToAbort = captureQueue.toList()
+ captureQueue.clear()
}
+
for (sequence in requestSequencesToAbort) {
sequence.invokeOnSequenceAborted()
}
}
override fun stopRepeating() {
- val requestSequence =
- synchronized(lock) {
- check(eventChannel.trySend(Event(stop = true)).isSuccess)
- repeatingRequestSequence.also { repeatingRequestSequence = null }
- }
- requestSequence?.invokeOnSequenceAborted()
+ throwTestExceptionIf(throwOnStop)
+ synchronized(lock) {
+ println("$this: StopRepeating")
+ _events.add(StopRepeating)
+ repeatingCapture = null
+ }
}
override suspend fun shutdown() {
+ throwTestExceptionIf(throwOnShutdown)
synchronized(lock) {
- rejectRequests = true
- check(eventChannel.trySend(Event(close = true)).isSuccess)
+ println("$this: Shutdown")
+ shutdown = true
+ _events.add(Shutdown)
}
}
- /** Get the next event from queue with an option to specify a timeout for tests. */
- public suspend fun nextEvent(timeMillis: Long = 500): Event =
- withTimeout(timeMillis) { eventChannel.receive() }
+ override fun toString(): String {
+ return "FakeCaptureSequenceProcessor-$debugId($cameraId)"
+ }
- public suspend fun nextRequestSequence(): FakeCaptureSequence {
- while (true) {
- val pending: Deferred<FakeCaptureSequence>
- synchronized(lock) {
- var sequence = queue.removeFirstOrNull()
- if (sequence == null) {
- sequence = repeatingRequestSequence
- }
- if (sequence != null) {
- return sequence
- }
+ /**
+ * Get the next CaptureSequence from this CaptureSequenceProcessor. If there are non-repeating
+ * capture requests in the queue, remove the first item from the queue. Otherwise, return the
+ * current repeating CaptureSequence, or null if there are no active CaptureSequences.
+ */
+ internal fun nextCaptureSequence(): FakeCaptureSequence? =
+ synchronized(lock) { captureQueue.removeFirstOrNull() ?: repeatingCapture }
- if (pendingSequence == null) {
- pendingSequence = CompletableDeferred()
- }
- pending = pendingSequence!!
- }
-
- pending.await()
+ private fun throwTestExceptionIf(condition: Boolean) {
+ if (condition) {
+ throw RuntimeException("Test Exception")
}
}
- /** TODO: It's probably better to model this as a sealed class. */
- public data class Event(
- val requestSequence: FakeCaptureSequence? = null,
- val rejected: Boolean = false,
- val abort: Boolean = false,
- val close: Boolean = false,
- val stop: Boolean = false,
- val submit: Boolean = false
- )
+ public open class Event
+
+ public object Shutdown : Event()
+
+ public object StopRepeating : Event()
+
+ public object AbortCaptures : Event()
+
+ public data class BuildRejected(val captureSequence: FakeCaptureSequence?) : Event()
+
+ public data class SubmitRejected(val captureSequence: FakeCaptureSequence) : Event()
+
+ public data class Submit(val captureSequence: FakeCaptureSequence) : Event()
public companion object {
- public suspend fun FakeCaptureSequenceProcessor.awaitEvent(
- request: Request? = null,
- filter: (event: Event) -> Boolean
- ): Event {
+ private val debugIds = atomic(0)
+ public val Event.requests: List<Request>
+ get() = checkNotNull(captureSequence).captureRequestList
- var event: Event
- var loopCount = 0
- while (loopCount < 10) {
- loopCount++
- event = this.nextEvent()
+ public val Event.requiredParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).requiredParameters
- if (request != null) {
- val contains =
- event.requestSequence?.captureRequestList?.contains(request) ?: false
- if (filter(event) && contains) {
- return event
- }
- } else if (filter(event)) {
- return event
+ public val Event.defaultParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).defaultParameters
+
+ // TODO: Decide if these should only work on successful submit or not.
+ public val Event.isRepeating: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating ?: false
+
+ public val Event.isCapture: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating == false
+
+ public val Event.isRejected: Boolean
+ get() =
+ when (this) {
+ is BuildRejected,
+ is SubmitRejected -> true
+ else -> false
}
- }
- throw IllegalStateException("Failed to observe a submit event containing $request")
- }
+ public val Event.isAbort: Boolean
+ get() = this is AbortCaptures
+
+ public val Event.isStopRepeating: Boolean
+ get() = this is StopRepeating
+
+ public val Event.isClose: Boolean
+ get() = this is Shutdown
+
+ public val Event.captureSequence: FakeCaptureSequence?
+ get() =
+ when (this) {
+ is Submit -> captureSequence
+ is BuildRejected -> captureSequence
+ is SubmitRejected -> captureSequence
+ else -> null
+ }
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
index 90465f2..f24a252 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
@@ -35,7 +35,7 @@
public class FakeFrameMetadata(
private val resultMetadata: Map<CaptureResult.Key<*>, Any?> = emptyMap(),
extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- override val camera: CameraId = nextFakeCameraId(),
+ override val camera: CameraId = FakeCameraIds.default,
override val frameNumber: FrameNumber = nextFakeFrameNumber(),
override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
) : FakeMetadata(extraResultMetadata), FrameMetadata {
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
index d7373f1..f1e3fd5 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
@@ -36,13 +36,17 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraPipeSimulatorTest {
private val testScope = TestScope()
- private val frontCameraMetadata =
- FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
- )
private val backCameraMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ )
+ private val frontCameraMetadata =
+ FakeCameraMetadata(
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
private val streamConfig = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
index d91dd8e..fed7ff7 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -31,22 +31,30 @@
class FakeCameraDevicesTest {
private val EXTERNAL_BACKEND_ID =
CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraDevicesTest")
- private val metadata1 =
+ private val frontMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
- private val metadata2 =
+ private val backMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
)
- private val metadata3 =
+ private val extMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(
+ CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL
+ )
)
private val cameraMetadataMap =
mapOf(
- FAKE_CAMERA_BACKEND_ID to listOf(metadata1, metadata2),
- EXTERNAL_BACKEND_ID to listOf(metadata3)
+ FAKE_CAMERA_BACKEND_ID to listOf(frontMetadata, backMetadata),
+ EXTERNAL_BACKEND_ID to listOf(extMetadata)
)
@Test
@@ -63,11 +71,13 @@
)
val devices = cameraDevices.getCameraIds()
assertThat(devices)
- .containsExactlyElementsIn(listOf(metadata1.camera, metadata2.camera))
+ .containsExactlyElementsIn(listOf(frontMetadata.camera, backMetadata.camera))
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
- assertThat(cameraDevices.getCameraMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+ assertThat(cameraDevices.getCameraMetadata(frontMetadata.camera))
+ .isSameInstanceAs(frontMetadata)
+ assertThat(cameraDevices.getCameraMetadata(backMetadata.camera))
+ .isSameInstanceAs(backMetadata)
}
@Test
@@ -86,13 +96,13 @@
assertThat(devices)
.containsExactlyElementsIn(
listOf(
- metadata3.camera,
+ extMetadata.camera,
)
)
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera)).isNull()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera, EXTERNAL_BACKEND_ID))
- .isSameInstanceAs(metadata3)
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera)).isNull()
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera, EXTERNAL_BACKEND_ID))
+ .isSameInstanceAs(extMetadata)
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index e276d6c..d418346 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -62,7 +62,6 @@
)
assertThat(metadata1).isNotEqualTo(metadata2)
- assertThat(metadata1.camera).isNotEqualTo(metadata2.camera)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 9ed683c..5d5ee33 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -40,7 +40,7 @@
}
public class CameraAvailable(public val cameraId: CameraId) : CameraStatus() {
- override fun toString(): String = "CameraAvailable(camera=$cameraId"
+ override fun toString(): String = "CameraAvailable(camera=$cameraId)"
}
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index 2d032b7..869f124 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -77,6 +77,18 @@
@JvmStatic
public fun fromIntOrNull(value: Int): AeMode? = values.firstOrNull { it.value == value }
+
+ @JvmStatic
+ public fun fromInt(value: Int): AeMode =
+ when (value) {
+ OFF.value -> OFF
+ ON.value -> ON
+ ON_AUTO_FLASH.value -> ON_AUTO_FLASH
+ ON_ALWAYS_FLASH.value -> ON_ALWAYS_FLASH
+ ON_AUTO_FLASH_REDEYE.value -> ON_AUTO_FLASH_REDEYE
+ ON_EXTERNAL_FLASH.value -> ON_EXTERNAL_FLASH
+ else -> ON
+ }
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index e27775d..f5da2b9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -31,9 +31,12 @@
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode.Companion.EXTENSION
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode.Companion.HIGH_SPEED
import androidx.camera.camera2.pipe.CameraGraph.OperatingMode.Companion.NORMAL
+import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.AT_LEAST
+import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.EXACT
import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
+import androidx.camera.camera2.pipe.compat.Camera2Quirks
import androidx.camera.camera2.pipe.core.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -253,6 +256,39 @@
}
/**
+ * Defines the repeating request requirements before a non-repeating capture.
+ *
+ * If `completionBehavior` is [AT_LEAST], repeating frames are completed at least
+ * `repeatingFramesToComplete` number of times before submitting capture. However, CameraPipe
+ * may wait for more repeating capture frames if required.
+ *
+ * If [EXACT] is used, any CameraPipe behavior is overwritten and exactly
+ * `repeatingFramesToComplete` number of frames are completed before submitting capture. This
+ * can be used in conjunction with a `repeatingFramesToComplete` value of zero to disable any
+ * CameraPipe quirky behavior added as a workaround. See
+ * [Camera2Quirks.getRepeatingRequestFrameCountForCapture] for details.
+ *
+ * @param repeatingFramesToComplete Number of repeating frames to complete before submitting a
+ * capture. A value of zero implies no such requirement.
+ * @param completionBehavior The behavior for how `repeatingFramesToComplete` is handled.
+ */
+ public class RepeatingRequestRequirementsBeforeCapture(
+ public val repeatingFramesToComplete: UInt = 0u,
+ public val completionBehavior: CompletionBehavior = AT_LEAST,
+ ) {
+ /**
+ * Defines the behavior for how [repeatingFramesToComplete] parameter of
+ * `RepeatingRequestRequirementsBeforeCapture` is handled.
+ *
+ * @see RepeatingRequestRequirementsBeforeCapture
+ */
+ public enum class CompletionBehavior {
+ AT_LEAST,
+ EXACT
+ }
+ }
+
+ /**
* Flags define boolean values that are used to adjust the behavior and interactions with
* camera2. These flags should default to the ideal behavior and should be overridden on
* specific devices to be faster or to work around bad behavior.
@@ -284,10 +320,28 @@
val abortCapturesOnStop: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R,
/**
- * A quirk that waits for the last repeating capture request to start before stopping the
- * current capture session. Please refer to the bugs linked here, or
- * [androidx.camera.camera2.pipe.compat.Camera2Quirks.shouldWaitForRepeatingRequest] for
- * more information.
+ * An override flag for quirk that requires waiting for the last repeating capture request
+ * (if any) to start before submitting a non-repeating capture request in case no repeating
+ * request has started yet.
+ *
+ * The value represents how many repeating request captures need to be completed before a
+ * non-repeating capture. Note that CameraPipe may have its own logic to. When null,
+ * CameraPipe will use its own logic to decide whether such a workaround is required. When
+ * zero or negative, CameraPipe will disable such behavior.
+ *
+ * Please refer to the bugs linked here, or
+ * [Camera2Quirks.getRepeatingRequestFrameCountForCapture] for more information. This flag
+ * provides the overrides for you to enable a workaround to fix such issues.
+ * - Bug(s): b/356792665
+ * - API levels: All
+ */
+ val awaitRepeatingRequestBeforeCapture: RepeatingRequestRequirementsBeforeCapture =
+ RepeatingRequestRequirementsBeforeCapture(),
+
+ /**
+ * Flag to wait for the last repeating capture request to start before stopping the current
+ * capture session. Please refer to the bugs linked here, or
+ * [Camera2Quirks.shouldWaitForRepeatingRequestStartOnDisconnect] for more information.
*
* This flag provides the overrides for you to override the default behavior (CameraPipe
* would turn on/off the quirk automatically based on device information).
@@ -295,10 +349,10 @@
* - Device(s): Camera devices on hardware level LEGACY
* - API levels: All
*/
- val quirkWaitForRepeatingRequestOnDisconnect: Boolean? = null,
+ val awaitRepeatingRequestOnDisconnect: Boolean? = null,
/**
- * A quirk that finalizes [androidx.camera.camera2.pipe.compat.CaptureSessionState] when the
+ * Flag to finalize [androidx.camera.camera2.pipe.compat.CaptureSessionState] when the
* CameraGraph is stopped or closed. When a CameraGraph is started, the app might wait for
* the Surfaces to be released before setting the new Surfaces. This creates a potential
* deadlock, and this quirk is aimed to mitigate such behavior by releasing the Surfaces
@@ -307,28 +361,28 @@
* - Device(s): All (but behaviors might differ across devices)
* - API levels: All
*/
- val quirkFinalizeSessionOnCloseBehavior: FinalizeSessionOnCloseBehavior = OFF,
+ val finalizeSessionOnCloseBehavior: FinalizeSessionOnCloseBehavior = OFF,
/**
- * A quirk that closes the camera capture session when the CameraGraph is stopped or closed.
- * This is needed in cases where the app that do not wish to receive further frames, or in
- * cases where not closing the capture session before closing the camera device might cause
- * the camera close call itself to hang indefinitely.
+ * Flag to close the camera capture session when the CameraGraph is stopped or closed. This
+ * is needed in cases where the app that do not wish to receive further frames, or in cases
+ * where not closing the capture session before closing the camera device might cause the
+ * camera close call itself to hang indefinitely.
* - Bug(s): b/277310425, b/277310425
* - Device(s): Depends on the situation and the use case.
* - API levels: All
*/
- val quirkCloseCaptureSessionOnDisconnect: Boolean = false,
+ val closeCaptureSessionOnDisconnect: Boolean = false,
/**
- * A quirk that closes the camera device when the CameraGraph is closed. This is needed on
- * devices where not closing the camera device before creating a new capture session can
- * lead to crashes.
+ * Flag to close the camera device when the CameraGraph is closed. This is needed on devices
+ * where not closing the camera device before creating a new capture session can lead to
+ * crashes.
* - Bug(s): b/282871038
* - Device(s): Exynos7870 platforms.
* - API levels: All
*/
- val quirkCloseCameraDeviceOnClose: Boolean = false,
+ val closeCameraDeviceOnClose: Boolean = false,
) {
@JvmInline
@@ -503,20 +557,30 @@
): Deferred<Result3A>
/**
- * Turns the torch to ON or OFF.
+ * Turns the torch to ON.
*
* This method has a side effect on the currently set AE mode. Ref:
* https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#FLASH_MODE
* To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is already
* not either ON or OFF, we will need to update the AE mode to one of those states, here we
* will choose ON. It is the responsibility of the application layer above CameraPipe to
- * restore the AE mode after the torch control has been used. The [update3A] method can be
- * used to restore the AE state to a previous value.
+ * restore the AE mode after the torch control has been used. The [setTorchOff] or
+ * [update3A] method can be used to restore the AE state to a previous value.
*
* @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
* FrameNumber at which it was completely turned off when the switch was OFF.
*/
- public fun setTorch(torchState: TorchState): Deferred<Result3A>
+ public fun setTorchOn(): Deferred<Result3A>
+
+ /**
+ * Turns the torch to OFF.
+ *
+ * @param aeMode The [AeMode] to set while disabling the torch value. If null which is the
+ * default value, the current AE mode is used.
+ * @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
+ * FrameNumber at which it was completely turned off when the switch was OFF.
+ */
+ public fun setTorchOff(aeMode: AeMode? = null): Deferred<Result3A>
/**
* Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
@@ -660,6 +724,63 @@
*/
public suspend fun unlock3APostCapture(cancelAf: Boolean = true): Deferred<Result3A>
}
+
+ /**
+ * [Parameters] is a Map-like interface that stores the key-value parameter pairs from
+ * [CaptureRequest] and [Metadata] for each [CameraGraph]. Parameter are read/set directly using
+ * get/set methods in this interface.
+ *
+ * During an active [CameraGraph.Session], changes in [Parameters] may not be applied right
+ * away. Instead, the change will be applied after [CameraGraph.Session] closes. When there is
+ * no active [CameraGraph.Session], the change will be applied without having to wait for the
+ * session to close. When applying parameter changes, it will overwrite parameter values that
+ * were configured when building the request, and overwrite [Config.defaultParameters]. It will
+ * not overwrite [Config.requiredParameters].
+ *
+ * Note that [Parameters] only store values that is a result of methods from this interface. The
+ * parameter values that were set from implicit template values, or from building a request
+ * directly will not be reflected here.
+ */
+ public interface Parameters {
+ /** Get the value correspond to the given [CaptureRequest.Key]. */
+ public operator fun <T> get(key: CaptureRequest.Key<T>): T?
+
+ /** Get the value correspond to the given [Metadata.Key]. */
+ public operator fun <T> get(key: Metadata.Key<T>): T?
+
+ /** Store the [CaptureRequest] key value pair in the class. */
+ public operator fun <T> set(key: CaptureRequest.Key<T>, value: T)
+
+ /** Store the [Metadata] key value pair in the class. */
+ public operator fun <T> set(key: Metadata.Key<T>, value: T)
+
+ /**
+ * Store the key value pairs in the class. The key is either [CaptureRequest.Key] or
+ * [Metadata.Key].
+ */
+ public fun setAll(values: Map<*, Any?>)
+
+ /** Clear all [CaptureRequest] and [Metadata] parameters stored in the class. */
+ public fun clear()
+
+ /**
+ * Remove the [CaptureRequest] key value pair associated with the given key. Returns true if
+ * a key was present and removed.
+ */
+ public fun <T> remove(key: CaptureRequest.Key<T>): Boolean
+
+ /**
+ * Remove the [Metadata] key value pair associated with the given key. Returns true if a key
+ * was present and removed.
+ */
+ public fun <T> remove(key: Metadata.Key<T>): Boolean
+
+ /**
+ * Remove all parameters that match the given keys. The key is either [CaptureRequest.Key]
+ * or [Metadata.Key].
+ */
+ public fun removeAll(keys: Set<*>): Boolean
+ }
}
/**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index d75b9f9..89467a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -18,6 +18,8 @@
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.view.Surface
@@ -107,6 +109,25 @@
) {}
/**
+ * This event provides clients with an estimate of the post-processing progress of a capture
+ * which could take significantly more time relative to the rest of the
+ * [CameraExtensionSession.capture] sequence. The callback will be triggered only by
+ * extensions that return true from calls
+ * [CameraExtensionCharacteristics.isCaptureProcessProgressAvailable]. If support for this
+ * callback is present, then clients will be notified at least once with progress value 100.
+ * The callback will be triggered only for still capture requests
+ * [CameraExtensionSession.capture] and is not supported for repeating requests
+ * [CameraExtensionSession.setRepeatingRequest].
+ *
+ * @param requestMetadata the data about the camera2 request that was sent to the camera.
+ * @param progress the value indicating the current post-processing progress (between 0 and
+ * 100 inclusive)
+ * @see
+ * android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback.onCaptureProcessProgressed
+ */
+ public fun onCaptureProgress(requestMetadata: RequestMetadata, progress: Int) {}
+
+ /**
* This event indicates that all of the metadata associated with this frame has been
* produced. If [onPartialCaptureResult] was invoked, the values returned in the
* totalCaptureResult map be a superset of the values produced from the
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 16a26fd..0c41ed2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -176,7 +176,7 @@
ControllerState.ERROR ->
if (
cameraStatus is CameraStatus.CameraAvailable &&
- lastCameraError == CameraError.ERROR_CAMERA_DEVICE
+ lastCameraError != CameraError.ERROR_GRAPH_CONFIG
) {
shouldRestart = true
}
@@ -210,7 +210,7 @@
currentCameraStateJob = null
disconnectSessionAndCamera(session, camera)
- if (graphConfig.flags.quirkCloseCameraDeviceOnClose) {
+ if (graphConfig.flags.closeCameraDeviceOnClose) {
Log.debug { "Quirk: Closing all camera devices" }
virtualCameraManager.closeAll()
}
@@ -303,7 +303,7 @@
session?.disconnect()
camera?.disconnect()
}
- if (graphConfig.flags.quirkCloseCaptureSessionOnDisconnect) {
+ if (graphConfig.flags.closeCaptureSessionOnDisconnect) {
// It seems that on certain devices, CameraCaptureSession.close() can block for an
// extended period of time [1]. Wrap the await call with a timeout to prevent us from
// getting blocked for too long.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
index 9d10ac0..ebc244d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
@@ -46,6 +46,8 @@
frameNumber: FrameNumber
)
+ fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int)
+
fun onCaptureFailed(captureRequest: CaptureRequest, frameNumber: FrameNumber)
fun onCaptureSequenceCompleted(captureSequenceId: Int, captureFrameNumber: Long)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 34a0a1d..cc5e292 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -184,6 +184,15 @@
Debug.traceStop() // onCaptureCompleted
}
+ override fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int) {
+ Debug.traceStart { "onCaptureProcessProgressed" }
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequestMetadata(captureRequest)
+ invokeOnRequest(request) { it.onCaptureProgress(request, progress) }
+ Debug.traceStop()
+ }
+
override fun onCaptureFailed(
captureSession: CameraCaptureSession,
captureRequest: CaptureRequest,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index 17fb37e..efccda7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -74,7 +74,7 @@
graphConfig.defaultTemplate,
surfaceMap,
streamGraph,
- quirks.shouldWaitForRepeatingRequest(graphConfig)
+ quirks.shouldWaitForRepeatingRequestStartOnDisconnect(graphConfig)
)
as CaptureSequenceProcessor<Any, CaptureSequence<Any>>
}
@@ -97,7 +97,7 @@
private val template: RequestTemplate,
private val surfaceMap: Map<StreamId, Surface>,
private val streamGraph: StreamGraph,
- private val shouldWaitForRepeatingRequest: Boolean = false,
+ private val awaitRepeatingRequestOnDisconnect: Boolean = false,
) : CaptureSequenceProcessor<CaptureRequest, Camera2CaptureSequence> {
private val debugId = captureSequenceProcessorDebugIds.incrementAndGet()
private val lock = Any()
@@ -293,7 +293,7 @@
session !is CameraConstrainedHighSpeedCaptureSessionWrapper
) {
if (captureSequence.repeating) {
- if (shouldWaitForRepeatingRequest) {
+ if (awaitRepeatingRequestOnDisconnect) {
lastSingleRepeatingRequestSequence = captureSequence
}
session.setRepeatingRequest(
@@ -334,7 +334,7 @@
captureSequence = lastSingleRepeatingRequestSequence
}
- if (shouldWaitForRepeatingRequest && captureSequence != null) {
+ if (awaitRepeatingRequestOnDisconnect && captureSequence != null) {
awaitRepeatingRequestStarted(captureSequence)
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
index 7d1404e..00f24b5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
@@ -18,10 +18,13 @@
import android.os.Build
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.AT_LEAST
+import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.EXACT
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.math.max
@Singleton
internal class Camera2Quirks
@@ -38,9 +41,11 @@
* - Device(s): Camera devices on hardware level LEGACY
* - API levels: All
*/
- internal fun shouldWaitForRepeatingRequest(graphConfig: CameraGraph.Config): Boolean {
+ internal fun shouldWaitForRepeatingRequestStartOnDisconnect(
+ graphConfig: CameraGraph.Config
+ ): Boolean {
// First, check for overrides.
- graphConfig.flags.quirkWaitForRepeatingRequestOnDisconnect?.let {
+ graphConfig.flags.awaitRepeatingRequestOnDisconnect?.let {
return it
}
@@ -85,17 +90,37 @@
)
/**
- * A quirk that waits for a certain number of repeating requests to complete before allowing
- * (single) capture requests to be issued. This is needed on some devices where issuing a
- * capture request too early might cause it to fail prematurely.
+ * Returns the number of repeating requests frames before capture for quirks.
+ *
+ * This kind of quirk behavior requires waiting for a certain number of repeating requests
+ * to complete before allowing (single) capture requests to be issued. This is needed on
+ * some devices where issuing a capture request too early might cause it to fail prematurely
+ * or cause some other problem. A value of zero is returned when not required.
* - Bug(s): b/287020251, b/289284907
* - Device(s): See [SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP]
* - API levels: Before 34 (U)
*/
- internal fun shouldWaitForRepeatingBeforeCapture(): Boolean {
- return SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP[Build.MANUFACTURER]?.contains(
- Build.DEVICE
- ) == true && Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ internal fun getRepeatingRequestFrameCountForCapture(
+ graphConfigFlags: CameraGraph.Flags
+ ): Int {
+ val requirements = graphConfigFlags.awaitRepeatingRequestBeforeCapture
+
+ var frameCount = 0
+
+ if (
+ SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP[Build.MANUFACTURER]?.contains(Build.DEVICE) ==
+ true && Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ ) {
+ frameCount = max(frameCount, 10)
+ }
+
+ frameCount =
+ when (requirements.completionBehavior) {
+ AT_LEAST -> max(frameCount, requirements.repeatingFramesToComplete.toInt())
+ EXACT -> requirements.repeatingFramesToComplete.toInt()
+ }
+
+ return frameCount
}
/**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
index c6bf514..0c20c01 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt
@@ -305,7 +305,7 @@
//
// [1] b/277310425
// [2] b/277675483
- if (cameraGraphFlags.quirkCloseCaptureSessionOnDisconnect) {
+ if (cameraGraphFlags.closeCaptureSessionOnDisconnect) {
val captureSession = configuredCaptureSession?.session
checkNotNull(captureSession)
Debug.trace("$this CameraCaptureSessionWrapper#close") {
@@ -332,7 +332,7 @@
if (_cameraDevice == null || !hasAttemptedCaptureSession) {
shouldFinalizeSession = true
} else {
- when (cameraGraphFlags.quirkFinalizeSessionOnCloseBehavior) {
+ when (cameraGraphFlags.finalizeSessionOnCloseBehavior) {
FinalizeSessionOnCloseBehavior.IMMEDIATE -> {
shouldFinalizeSession = true
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
index 8039abf..f7532e5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
@@ -55,9 +55,19 @@
} catch (e: Exception) {
Log.warn { "Unexpected error: " + e.message }
when (e) {
+ is CameraAccessException -> {
+ cameraErrorListener.onCameraError(
+ cameraId,
+ CameraError.from(e),
+ // CameraAccessException indicates the task failed because the camera is
+ // unavailable, such as when the camera is in use or disconnected. Such errors
+ // can be recovered when the camera becomes available.
+ willAttemptRetry = true,
+ )
+ return null
+ }
is IllegalArgumentException,
is IllegalStateException,
- is CameraAccessException,
is SecurityException,
is UnsupportedOperationException,
is NullPointerException -> {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
index 7fb4d94..608dc0f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
@@ -278,6 +278,14 @@
request: CaptureRequest
) {}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
val frameNumber = frameQueue.remove()
captureCallback.onCaptureFailed(request, FrameNumber(frameNumber))
@@ -342,6 +350,14 @@
}
}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureSequenceCompleted(session: CameraExtensionSession, sequenceId: Int) {
val frameNumber = extensionSessionMap[session]
captureCallback.onCaptureSequenceCompleted(sequenceId, frameNumber!!)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index c552afc..e89dcf6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -26,7 +26,6 @@
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Token
import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
import kotlinx.atomicfu.atomic
@@ -115,11 +114,16 @@
return controller3A.submit3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
}
- override fun setTorch(torchState: TorchState): Deferred<Result3A> {
- check(!token.released) { "Cannot call setTorch on $this after close." }
+ override fun setTorchOn(): Deferred<Result3A> {
+ check(!token.released) { "Cannot call setTorchOn on $this after close." }
// TODO(sushilnath): First check whether the camera device has a flash unit. Ref:
// https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#FLASH_INFO_AVAILABLE
- return controller3A.setTorch(torchState)
+ return controller3A.setTorchOn()
+ }
+
+ override fun setTorchOff(aeMode: AeMode?): Deferred<Result3A> {
+ check(!token.released) { "Cannot call setTorchOff on $this after close." }
+ return controller3A.setTorchOff(aeMode)
}
override suspend fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 177ce94..8c3c597 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -43,7 +43,6 @@
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.Result3A.Status
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.core.Log.debug
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
@@ -646,14 +645,23 @@
return listener.result
}
- fun setTorch(torchState: TorchState): Deferred<Result3A> {
- // Determine the flash mode based on the torch state.
- val flashMode = if (torchState == TorchState.ON) FlashMode.TORCH else FlashMode.OFF
- // To use the flash control, AE mode must be set to ON or OFF.
+ /**
+ * Enables the torch which may require changing the [AeMode].
+ *
+ * To use [FlashMode.TORCH], either [AeMode.ON] or [AeMode.OFF] needs to be used, otherwise, the
+ * flash mode is a no-op. If the current AE mode is neither of them, this function changes the
+ * AE mode to [AeMode.ON] in order to enable the torch.
+ */
+ fun setTorchOn(): Deferred<Result3A> {
val currAeMode = graphState3A.aeMode
val desiredAeMode =
if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else AeMode.ON
- return update3A(aeMode = desiredAeMode, flashMode = flashMode)
+ return update3A(aeMode = desiredAeMode, flashMode = FlashMode.TORCH)
+ }
+
+ /** Disables the torch and sets a new AE mode if provided. */
+ fun setTorchOff(aeMode: AeMode? = null): Deferred<Result3A> {
+ return update3A(aeMode = aeMode, flashMode = FlashMode.OFF)
}
private fun lock3ANow(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
index 80ce6b0f..cc5d57a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -373,8 +373,8 @@
if (success) {
lastRepeatingRequest = command.request
commands.removeAt(idx)
+ commands.removeUpTo(idx) { it is StartRepeating }
}
- commands.removeUpTo(idx) { it is StartRepeating }
}
is SubmitCapture -> {
if (!_captureProcessingEnabled.value) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 1233c6b..88b8d98 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -18,8 +18,6 @@
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraphId
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
-import androidx.camera.camera2.pipe.FrameInfo
-import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.GraphState
import androidx.camera.camera2.pipe.GraphState.GraphStateError
import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
@@ -27,7 +25,6 @@
import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestMetadata
import androidx.camera.camera2.pipe.compat.Camera2Quirks
import androidx.camera.camera2.pipe.compat.CameraPipeKeys
import androidx.camera.camera2.pipe.config.CameraGraphScope
@@ -35,7 +32,6 @@
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
import androidx.camera.camera2.pipe.core.Threads
-import java.util.concurrent.CountDownLatch
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -128,9 +124,12 @@
}
}
+ val requestsUntilActive =
+ Camera2Quirks.getRepeatingRequestFrameCountForCapture(cameraGraphConfig.flags)
+
val captureLimiter =
- if (Camera2Quirks.shouldWaitForRepeatingBeforeCapture()) {
- CaptureLimiter(10)
+ if (requestsUntilActive != 0) {
+ CaptureLimiter(requestsUntilActive.toLong())
} else {
null
}
@@ -150,27 +149,6 @@
captureLimiter?.graphLoop = graphLoop
}
- // On some devices, we need to wait for 10 frames to complete before we can guarantee the
- // success of single capture requests. This is a quirk identified as part of b/287020251 and
- // reported in b/289284907.
- private var repeatingRequestsCompleted = CountDownLatch(10)
-
- // Graph listener added to repeating requests in order to handle the aforementioned quirk.
- private val graphProcessorRepeatingListeners =
- if (!Camera2Quirks.shouldWaitForRepeatingBeforeCapture()) {
- graphListeners
- } else {
- graphListeners +
- object : Request.Listener {
- override fun onComplete(
- requestMetadata: RequestMetadata,
- frameNumber: FrameNumber,
- result: FrameInfo
- ) {
- repeatingRequestsCompleted.countDown()
- }
- }
- }
private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
override val graphState: StateFlow<GraphState>
get() = _graphState
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
index 12874ff..9333ebc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt
@@ -134,9 +134,8 @@
CameraSurfaceManager(),
SystemTimeSource(),
CameraGraph.Flags(
- quirkFinalizeSessionOnCloseBehavior =
- FinalizeSessionOnCloseBehavior.OFF,
- quirkCloseCaptureSessionOnDisconnect = false,
+ finalizeSessionOnCloseBehavior = FinalizeSessionOnCloseBehavior.OFF,
+ closeCaptureSessionOnDisconnect = false,
),
this
)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
index b6809bc..866bc7b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt
@@ -64,8 +64,8 @@
private val timeSource = SystemTimeSource()
private val cameraGraphFlags =
CameraGraph.Flags(
- quirkFinalizeSessionOnCloseBehavior = FinalizeSessionOnCloseBehavior.OFF,
- quirkCloseCaptureSessionOnDisconnect = false,
+ finalizeSessionOnCloseBehavior = FinalizeSessionOnCloseBehavior.OFF,
+ closeCaptureSessionOnDisconnect = false,
)
private val surface1: Surface = Surface(SurfaceTexture(1))
@@ -256,7 +256,7 @@
cameraSurfaceManager,
timeSource,
CameraGraph.Flags(
- quirkCloseCaptureSessionOnDisconnect = true,
+ closeCaptureSessionOnDisconnect = true,
),
this
)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 4b35f50..a9e597f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -477,14 +478,20 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
- // call. There should be a request to cancel AF and AE precapture metering.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ // call. There should be a request to cancel AF and AE precapture metering
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
}
private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
@@ -509,31 +516,37 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and lock ae.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// Then another request to unlock ae.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val captureSequence2 = captureSequenceProcessor.nextEvent()
+ assertThat(captureSequence2.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
}
- private suspend fun assertCorrectCaptureSequenceInLock3AForCapture(
- isAfTriggered: Boolean = true
- ) {
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
+ private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
if (isAfTriggered) {
isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
} else {
isNotEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
}
}
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
}
companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index 3085cc7..e9be1c9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -27,6 +27,10 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -147,14 +151,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
}
@Test
@@ -199,10 +206,12 @@
// Check the correctness of the requests submitted by lock3A.
// One repeating request was sent to monitor the state of AE to get converged.
- captureSequenceProcessor.nextEvent().requestSequence
- // Once AE is converged, another repeatingrequest is sent to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.isRepeating).isTrue()
+
+ // Once AE is converged, another repeating request is sent to lock AE.
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -228,9 +237,12 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// A single request to lock AF must have been used as well.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -275,8 +287,8 @@
// For a new AE scan we first send a request to unlock AE just in case it was
// previously or internally locked.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -302,14 +314,17 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one more request to lock AE after new scan is done.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -378,14 +393,17 @@
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// One request to lock AE
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -451,21 +469,27 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// There should be one request to monitor lock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -532,29 +556,26 @@
// There should be one request to monitor AF to finish it's scan.
val event = captureSequenceProcessor.nextEvent()
- assertThat(event.requestSequence!!.repeating).isTrue()
- assertThat(event.rejected).isFalse()
- assertThat(event.abort).isFalse()
- assertThat(event.close).isFalse()
- assertThat(event.submit).isTrue()
+ assertThat(event.isRepeating).isTrue()
- // One request to lock AE
+ // One request to lock AE (Repeating)
val request2Event = captureSequenceProcessor.nextEvent()
- assertThat(request2Event.requestSequence!!.repeating).isTrue()
- assertThat(request2Event.submit).isTrue()
- val request2 = request2Event.requestSequence!!
- assertThat(request2).isNotNull()
- assertThat(request2.requiredParameters).isNotEmpty()
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request2Event.isRepeating).isTrue()
+ assertThat(request2Event.requests.size).isEqualTo(1)
+ assertThat(request2Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
val request3Event = captureSequenceProcessor.nextEvent()
- assertThat(request3Event.requestSequence!!.repeating).isFalse()
- assertThat(request3Event.submit).isTrue()
- val request3 = request3Event.requestSequence!!
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request3Event.isCapture).isTrue()
+ assertThat(request3Event.requests.size).isEqualTo(1)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -620,22 +641,29 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+
// There should be one request to unlock AE and monitor the current AF scan to finish.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
// There should be one request to monitor lock AE.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request4 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event4 = captureSequenceProcessor.nextEvent()
+ assertThat(event4.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event4.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -713,14 +741,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
index 7e4ff9a..5490270 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.camera2.pipe.graph
import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
import android.hardware.camera2.CaptureResult
import android.os.Build
import androidx.camera.camera2.pipe.AeMode
@@ -24,14 +25,12 @@
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
-import androidx.camera.camera2.pipe.TorchState
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -39,7 +38,6 @@
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class Controller3ASetTorchTest {
@@ -56,7 +54,7 @@
}
@Test
- fun testSetTorchFailsImmediatelyWithoutRepeatingRequest() = runTest {
+ fun setTorchOn_withoutRepeatingRequest_failsImmediatelyWithNoGraphStateChange() = runTest {
val graphProcessor2 = FakeGraphProcessor()
val controller3A =
Controller3A(
@@ -65,17 +63,42 @@
graphProcessor2.graphState3A,
listener3A
)
- val result = controller3A.setTorch(TorchState.ON)
+ val result = controller3A.setTorchOn()
assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
assertThat(graphProcessor2.graphState3A.flashMode).isEqualTo(FlashMode.TORCH)
}
@Test
- fun testSetTorchOn() = runTest {
- val result = controller3A.setTorch(TorchState.ON)
+ fun setTorchOff_withoutRepeatingRequest_failsImmediatelyWithNoGraphStateChange() = runTest {
+ val graphProcessor2 = FakeGraphProcessor()
+ val controller3A =
+ Controller3A(
+ graphProcessor2,
+ FakeCameraMetadata(),
+ graphProcessor2.graphState3A,
+ listener3A
+ )
+ val result = controller3A.setTorchOff()
+ assertThat(result.await().status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
+ assertThat(graphProcessor2.graphState3A.flashMode).isEqualTo(FlashMode.OFF)
+ }
+
+ @Test
+ fun setTorchOn_updatesGraphStateWithAeModeOnAndFlashModeTorch() = runTest {
+ controller3A.setTorchOn()
assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+ }
+
+ @Test
+ fun setTorchOn_noCaptureResultProvided_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOn()
assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOn_captureResultProvidedWithAeOnAndFlashTorch_returnsOkResult() = runTest {
+ val result = controller3A.setTorchOn()
launch {
listener3A.onRequestSequenceCreated(
@@ -94,17 +117,62 @@
)
)
}
+
val result3A = result.await()
assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@Test
- fun testSetTorchOff() = runTest {
- val result = controller3A.setTorch(TorchState.OFF)
- assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+ fun setTorchOn_captureResultProvidedWithOnlyFlashTorch_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOn()
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)
+ )
+ )
+ }
+ .join()
+
+ assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOff_updatesGraphStateWithFlashModeOff() = runTest {
+ controller3A.setTorchOff()
assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_OFF)
+ }
+
+ @Test
+ fun setTorchOffWithoutAeMode_graphStateAeModeStaysNull() = runTest {
+ controller3A.setTorchOff()
+ assertThat(graphState3A.aeMode?.value).isNull() // null is default value here
+ }
+
+ @Test
+ fun setTorchOffWithAutoFlashAeMode_graphStateAeModeUpdatedToAutoFlash() = runTest {
+ controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
+ assertThat(graphState3A.aeMode?.value).isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH)
+ }
+
+ @Test
+ fun setTorchOff_noCaptureResultWithUpdatedStates_resultIncomplete() = runTest {
+ val result = controller3A.setTorchOff()
assertThat(result.isCompleted).isFalse()
+ }
+
+ @Test
+ fun setTorchOffWithoutAeMode_captureResultProvidedWithFlashOff_returnsOkResult() = runTest {
+ val result = controller3A.setTorchOff()
launch {
listener3A.onRequestSequenceCreated(
@@ -115,11 +183,7 @@
FrameNumber(101L),
FakeFrameMetadata(
frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
- CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
- )
+ resultMetadata = mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF)
)
)
}
@@ -129,33 +193,90 @@
}
@Test
- fun testSetTorchDoesNotChangeAeModeIfNotNeeded() = runTest {
- graphState3A.update(aeMode = AeMode.OFF)
+ fun setTorchOffWithAutoFlashAe_captureResultProvidedWithOnlyFlashOff_resultIncomplete() =
+ runTest {
+ val result = controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
- val result = controller3A.setTorch(TorchState.ON)
- assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
- assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
- assertThat(result.isCompleted).isFalse()
-
- launch {
- listener3A.onRequestSequenceCreated(
- FakeRequestMetadata(requestNumber = RequestNumber(1))
- )
- listener3A.onPartialCaptureResult(
- FakeRequestMetadata(requestNumber = RequestNumber(1)),
- FrameNumber(101L),
- FakeFrameMetadata(
- frameNumber = FrameNumber(101L),
- resultMetadata =
- mapOf(
- CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
- CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF)
)
- )
- )
+ )
+ }
+ .join()
+
+ assertThat(result.isCompleted).isFalse()
}
- val result3A = result.await()
- assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
- assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
- }
+
+ @Test
+ fun setTorchOffWithAutoFlashAe_captureResultProvidedWithAutoAeAndFlashOff_returnsOkResult() =
+ runTest {
+ val result = controller3A.setTorchOff(aeMode = AeMode.ON_AUTO_FLASH)
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(
+ CaptureResult.CONTROL_AE_MODE to
+ CaptureResult.CONTROL_AE_MODE_ON_AUTO_FLASH,
+ CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
+ )
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+ }
+
+ @Test
+ fun setTorchOn_graphStateAlreadyAeOffSoNoChangeNeeded_aeModeUnchangedButFlashChangedToTorch() =
+ runTest {
+ graphState3A.update(aeMode = AeMode.OFF)
+
+ controller3A.setTorchOn()
+ assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
+ assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+ }
+
+ @Test
+ fun setTorchOnWithGraphStateAlreadyAeOff_captureResultProvidedWithFlashTorch_returnsOkResult() =
+ runTest {
+ graphState3A.update(aeMode = AeMode.OFF)
+
+ val result = controller3A.setTorchOn()
+
+ launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(requestNumber = RequestNumber(1))
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata =
+ mapOf(CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH)
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+ }
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index e70b7a7..9305a51 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -25,6 +25,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -104,8 +105,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -164,9 +165,12 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
repeatingJob.cancel()
repeatingJob.join()
@@ -225,8 +229,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AWB.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AWB_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -287,12 +291,15 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// Then request to unlock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
index 728d859..711da09 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -17,21 +17,25 @@
package androidx.camera.camera2.pipe.graph
import android.os.Build
-import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraphId
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CaptureSequence
-import androidx.camera.camera2.pipe.CaptureSequenceProcessor
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.defaultParameters
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isAbort
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isStopRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeMetadata.Companion.TEST_KEY
import androidx.camera.camera2.pipe.testing.FakeSurfaces
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -73,8 +77,10 @@
stream2 to fakeSurfaces.createFakeSurface()
)
- private val csp1 = SimpleCSP(fakeCameraId, surfaceMap)
- private val csp2 = SimpleCSP(fakeCameraId, surfaceMap)
+ private val csp1 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
+ private val csp2 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
private val grp1 = GraphRequestProcessor.from(csp1)
private val grp2 = GraphRequestProcessor.from(csp2)
@@ -823,90 +829,32 @@
assertThat(csp2.events[0].isClose).isTrue()
}
- private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
- get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
+ @Test
+ fun settingRepeatingRequestWhenRequestsAreRejectedDoesNotAttemptMultipleRepeatingRequests() =
+ testScope.runTest {
+ // Arrange
+ csp1.rejectSubmit = true
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.requiredParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.requiredParameters
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- private val SimpleCSP.SimpleCSPEvent.defaultParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.defaultParameters
+ graphLoop.repeatingRequest = request2
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isRepeating: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating ?: false
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
- private val SimpleCSP.SimpleCSPEvent.isCapture: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating == false
+ csp1.rejectSubmit = false
+ graphLoop.invalidate()
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isAbort: Boolean
- get() = this is SimpleCSP.AbortCaptures
-
- private val SimpleCSP.SimpleCSPEvent.isStopRepeating: Boolean
- get() = this is SimpleCSP.StopRepeating
-
- private val SimpleCSP.SimpleCSPEvent.isClose: Boolean
- get() = this is SimpleCSP.Close
-
- internal class SimpleCSP(
- private val cameraId: CameraId,
- private val surfaceMap: Map<StreamId, Surface>
- ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
- val events = mutableListOf<SimpleCSPEvent>()
- var throwOnBuild = false
- private var closed = false
- private val sequenceIds = atomic(0)
-
- override fun build(
- isRepeating: Boolean,
- requests: List<Request>,
- defaultParameters: Map<*, Any?>,
- requiredParameters: Map<*, Any?>,
- listeners: List<Request.Listener>,
- sequenceListener: CaptureSequence.CaptureSequenceListener
- ): FakeCaptureSequence? {
- if (closed) return null
- if (throwOnBuild) throw RuntimeException("Test Exception")
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
}
-
- override fun abortCaptures() {
- events.add(AbortCaptures)
- }
-
- override fun stopRepeating() {
- events.add(StopRepeating)
- }
-
- override suspend fun shutdown() {
- closed = true
- events.add(Close)
- }
-
- override fun submit(captureSequence: FakeCaptureSequence): Int? {
- if (!closed) {
- events.add(Submit(captureSequence))
- return sequenceIds.incrementAndGet()
- }
- return null
- }
-
- sealed class SimpleCSPEvent
-
- object Close : SimpleCSPEvent()
-
- object StopRepeating : SimpleCSPEvent()
-
- object AbortCaptures : SimpleCSPEvent()
-
- data class Submit(val captureSequence: FakeCaptureSequence) : SimpleCSPEvent()
- }
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 13e29b9..c648043 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -28,7 +28,12 @@
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.awaitEvent
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeGraphConfigs
import androidx.camera.camera2.pipe.testing.FakeRequestListener
import androidx.camera.camera2.pipe.testing.FakeThreads
@@ -36,10 +41,9 @@
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
@@ -52,17 +56,20 @@
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class GraphProcessorTest {
+ private val testScope = TestScope()
+ private val fakeThreads = FakeThreads.fromTestScope(testScope)
+
private val globalListener = FakeRequestListener()
private val graphState3A = GraphState3A()
private val graphListener3A = Listener3A()
private val streamId = StreamId(0)
private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
- private val fakeProcessor1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val fakeProcessor2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val graphRequestProcessor1 = GraphRequestProcessor.from(fakeProcessor1)
- private val graphRequestProcessor2 = GraphRequestProcessor.from(fakeProcessor2)
+ private val grp1 = GraphRequestProcessor.from(csp1)
+ private val grp2 = GraphRequestProcessor.from(csp2)
private val requestListener1 = FakeRequestListener()
private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
@@ -70,516 +77,375 @@
private val requestListener2 = FakeRequestListener()
private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
+ private val graphProcessor =
+ GraphProcessorImpl(
+ fakeThreads,
+ CameraGraphId.nextId(),
+ FakeGraphConfigs.graphConfig,
+ graphState3A,
+ graphListener3A,
+ arrayListOf(globalListener)
+ )
+
@After
fun teardown() {
surfaceMap[streamId]?.release()
}
@Test
- fun graphProcessorSubmitsRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
- advanceUntilIdle()
+ fun graphProcessorSubmitsRequests() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Make sure the requests get submitted to the request processor
- val event = fakeProcessor1.nextEvent()
- assertThat(event.requestSequence!!.captureRequestList).containsExactly(request1)
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
+ // Make sure the requests get submitted to the request processor
+ assertThat(csp1.events.size).isEqualTo(1)
- @Test
- fun graphProcessorSubmitsRequestsToMostRecentProcessor() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- graphProcessor.submit(request1)
-
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.close).isTrue()
-
- val event2 = fakeProcessor2.nextEvent()
- assertThat(event2.submit).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList).containsExactly(request1)
- }
-
- @Test
- fun graphProcessorSubmitsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(request1)
- graphProcessor.submit(request2)
-
- // Request1 and 2 should be queued and will be submitted even when the request
- // processor is set after the requests are submitted.
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- val event1 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event1.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event1.requestSequence!!.captureRequestList).contains(request1)
-
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event2.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(listOf(request1, request2))
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- val event = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event.requestSequence!!.captureRequestList).hasSize(2)
- assertThat(event.requestSequence!!.captureRequestList).contains(request1)
- assertThat(event.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.submit(request1)
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.submit(request2)
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request1)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request2)
- }
-
- @Test
- fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- // Note: setting the requestProcessor, and calling submit() can both trigger a call
- // to submit a request.
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
-
- // Check to make sure that submit is called at least once, and that request1 is rejected
- // from the request processor.
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- // Stop rejecting requests
- fakeProcessor1.rejectRequests = false
-
- graphProcessor.submit(request2)
- // Cycle events until we get a submitted event with request1
- val event2 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event2.rejected).isFalse()
-
- // Assert that immediately after we get a successfully submitted request, the
- // next request is also submitted.
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence!!.captureRequestList).contains(request2)
- assertThat(event3.submit).isTrue()
- assertThat(event3.rejected).isFalse()
- }
-
- @Test
- fun graphProcessorSetsRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.repeatingRequest = request2
- advanceUntilIdle()
-
- val event =
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.repeatingRequest = request1
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.repeatingRequest = request2
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- fakeProcessor1.awaitEvent(request = request2) {
- !it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
}
- fakeProcessor1.rejectRequests = false
- graphProcessor.invalidate()
-
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- }
-
@Test
- fun graphProcessorTracksRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsRequestsToMostRecentProcessor() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphStarted(grp2)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
- fakeProcessor1.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
}
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- advanceUntilIdle()
+ @Test
+ fun graphProcessorSubmitsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.submit(request1)
+ graphProcessor.submit(request2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ // Request1 and 2 should be queued and will be submitted even when the request
+ // processor is set after the requests are submitted.
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
}
- }
@Test
- fun graphProcessorTracksRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() =
+ testScope.runTest {
+ graphProcessor.submit(listOf(request1, request2))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1, request2).inOrder()
}
- }
@Test
- fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
- delay(50)
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1) // Re-attempt #1
- var hasRequest1Event = false
- var hasRequest2Event = false
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- // Loop until we see at least one repeating request, and one submit event.
- launch {
- while (!hasRequest1Event && !hasRequest2Event) {
- val event = fakeProcessor1.nextEvent()
- hasRequest1Event =
- hasRequest1Event ||
- event.requestSequence?.captureRequestList?.contains(request1) ?: false
- hasRequest2Event =
- hasRequest2Event ||
- event.requestSequence?.captureRequestList?.contains(request2) ?: false
- }
- }
- .join()
- }
+ // Assert that after a new request processor is set, it receives the queued up requests.
+ assertThat(csp2.events.size).isEqualTo(2)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ assertThat(csp2.events[1].isCapture).isTrue()
+ assertThat(csp2.events[1].requests).containsExactly(request2).inOrder()
+ }
@Test
- fun graphProcessorAbortsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() =
+ testScope.runTest {
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
+ // Note: setting the requestProcessor, and calling submit() can both trigger a call
+ // to submit a request.
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- graphProcessor.abort()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ // Check to make sure that submit is called at least once, and that request1 is rejected
+ // from the request processor.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- val globalAbortEvent = globalListener.onAbortedFlow.first()
+ // Stop rejecting requests
+ csp1.rejectSubmit = false
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
- val nextSequence = fakeProcessor1.nextRequestSequence()
- assertThat(nextSequence.captureRequestList.first()).isSameInstanceAs(request1)
- assertThat(nextSequence.requestMetadata[request1]!!.repeating).isTrue()
- }
+ // Assert that immediately after we get a successfully submitted request, the
+ // next request is also submitted.
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun closingGraphProcessorAbortsSubsequentRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.close()
- advanceUntilIdle()
+ fun graphProcessorSetsRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- // graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
-
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- }
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
+ }
@Test
- fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- advanceUntilIdle()
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- }
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+
+ csp1.rejectSubmit = false
+ graphProcessor.invalidate()
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence?.repeating).isFalse()
- assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isTrue()
- }
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
@Test
- fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- assertThrows<IllegalStateException> {
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun graphProcessorAbortsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ // Abort queued and in-flight requests.
+ graphProcessor.abort()
+ graphProcessor.onGraphStarted(grp1)
+
+ val abortEvent1 = requestListener2.onAbortedFlow.first()
+ val globalAbortEvent = globalListener.onAbortedFlow.first()
+
+ assertThat(abortEvent1.request).isSameInstanceAs(request2)
+ assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun closingGraphProcessorAbortsSubsequentRequests() =
+ testScope.runTest {
+ graphProcessor.close()
+ advanceUntilIdle()
+
+ // Abort queued and in-flight requests.
+ // graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ val abortEvent1 =
+ withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
+ val abortEvent2 = requestListener2.onAbortedFlow.first()
+ assertThat(abortEvent1).isNull()
+ assertThat(abortEvent2.request).isSameInstanceAs(request2)
+ }
+
+ @Test
+ fun graphProcessorResubmitsParametersAfterGraphStarts() =
+ testScope.runTest {
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+ }
+
+ @Test
+ fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() =
+ testScope.runTest {
+
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request1)
+ assertThat(csp1.events[2].requiredParameters).containsEntry(CONTROL_AE_LOCK, true)
}
- }
@Test
- fun graphProcessorChangesGraphStateOnError() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
- }
+ assertThrows<IllegalStateException> {
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ }
+ }
@Test
- fun graphProcessorDropsStaleErrors() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
+ fun graphProcessorChangesGraphStateOnError() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
)
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
+ }
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ @Test
+ fun graphProcessorDropsStaleErrors() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- graphProcessor.onGraphStarting()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- // GraphProcessor should drop errors while the camera graph is stopping.
- graphProcessor.onGraphStopping()
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ graphProcessor.onGraphStarting()
+ graphProcessor.onGraphStarted(grp1)
- // GraphProcessor should also drop errors while the camera graph is stopped.
- graphProcessor.onGraphStopped(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- }
+ // GraphProcessor should drop errors while the camera graph is stopping.
+ graphProcessor.onGraphStopping()
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ // GraphProcessor should also drop errors while the camera graph is stopped.
+ graphProcessor.onGraphStopped(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ }
}
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 0401f79..fe5ef3d 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -63,8 +63,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation(project(":camera:camera-testing")) {
// Ensure camera-testing does not pull in androidx.test dependencies
diff --git a/camera/camera-camera2/lint-baseline.xml b/camera/camera-camera2/lint-baseline.xml
index 8e783a1..cc4e9dd 100644
--- a/camera/camera-camera2/lint-baseline.xml
+++ b/camera/camera-camera2/lint-baseline.xml
@@ -1,5 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+
+ <issue
+ id="MissingClass"
+ message="Class referenced in the manifest, `androidx.camera.core.impl.MetadataHolderService`, was not found in the project or the libraries"
+ errorLine1=" android:name="androidx.camera.core.impl.MetadataHolderService""
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/AndroidManifest.xml"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" captureConfigs.forEach { captureConfig ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" captureConfigs.forEach { captureConfig ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" forEach { config ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" forEach { config ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" captureConfigs.forEach { captureConfig ->"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" cameraInfos.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCasesExpectedSizeMap.keys.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCasesExpectedDynamicRangeMap.keys.forEach {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCases.forEach { put(it, it.currentConfig) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 24, or core library desugaring (current min is 21): `java.lang.Iterable#forEach`"
+ errorLine1=" useCaseConfigs.forEach { put(it, DEFAULT_SUPPORTED_SIZES.toList()) }"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt"/>
+ </issue>
<issue
id="BanThreadSleep"
@@ -29,6 +128,249 @@
</issue>
<issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class AfRegionFlipHorizontallyQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/AfRegionFlipHorizontallyQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class AutoFlashUnderExposedQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/AutoFlashUnderExposedQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CaptureNoResponseQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/CaptureNoResponseQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CaptureSessionOnClosedNotCalledQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/CaptureSessionOnClosedNotCalledQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CaptureSessionStuckQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/CaptureSessionStuckQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CaptureSessionStuckWhenCreatingBeforeClosingCameraQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/CaptureSessionStuckWhenCreatingBeforeClosingCameraQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class ConfigureSurfaceToSecondarySessionFailQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/ConfigureSurfaceToSecondarySessionFailQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CrashWhenTakingPhotoWithAutoFlashAEModeQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class FlashAvailabilityBufferUnderflowQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class ImageCaptureFailWithAutoFlashQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureFailWithAutoFlashQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class ImageCaptureFailedForVideoSnapshotQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureFailedForVideoSnapshotQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class ImageCapturePixelHDRPlusQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCapturePixelHDRPlusQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class IncorrectCaptureStateQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/IncorrectCaptureStateQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class InvalidVideoProfilesQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/InvalidVideoProfilesQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class JpegCaptureDownsizingQuirk implements SoftwareJpegEncodingPreferredQuirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/JpegCaptureDownsizingQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public final class JpegHalCorruptImageQuirk implements SoftwareJpegEncodingPreferredQuirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/JpegHalCorruptImageQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class LegacyCameraOutputConfigNullPointerQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/LegacyCameraOutputConfigNullPointerQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class LegacyCameraSurfaceCleanupQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/LegacyCameraSurfaceCleanupQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class Preview3AThreadCrashQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/Preview3AThreadCrashQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class PreviewOrientationIncorrectQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewOrientationIncorrectQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class PreviewPixelHDRnetQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/PreviewPixelHDRnetQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class RepeatingStreamConstraintForVideoRecordingQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/RepeatingStreamConstraintForVideoRecordingQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class StillCaptureFlashStopRepeatingQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class TextureViewIsClosedQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/TextureViewIsClosedQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class TorchIsClosedAfterImageCapturingQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/TorchIsClosedAfterImageCapturingQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public final class YuvImageOnePixelShiftQuirk implements OnePixelShiftQuirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/YuvImageOnePixelShiftQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class ZslDisablerQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java"/>
+ </issue>
+
+ <issue
id="VisibleForTests"
message="This method should only be accessed from tests or within private scope"
errorLine1=" characteristics = CameraCharacteristicsCompat.toCameraCharacteristicsCompat("
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
index a783bd9..47c76ed 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
@@ -19,14 +19,11 @@
import android.media.CamcorderProfile
import android.media.EncoderProfiles.VideoProfile.HDR_NONE
import android.media.EncoderProfiles.VideoProfile.YUV_420
-import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
import androidx.camera.camera2.internal.compat.quirk.CamcorderProfileResolutionQuirk
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
-import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks
-import androidx.camera.camera2.internal.compat.quirk.InvalidVideoProfilesQuirk
import androidx.camera.core.CameraSelector
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
import androidx.camera.core.impl.Quirks
@@ -35,7 +32,6 @@
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
-import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -139,6 +135,7 @@
val videoProxy = profilesProxy!!.videoProfiles[0]
val audioProxy = profilesProxy.audioProfiles[0]
+ // Don't check video/audio profile, see cts/CamcorderProfileTest.java
assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
assertThat(videoProxy.codec).isEqualTo(video.codec)
@@ -147,7 +144,6 @@
assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
assertThat(videoProxy.width).isEqualTo(video.width)
assertThat(videoProxy.height).isEqualTo(video.height)
- assertThat(videoProxy.profile).isEqualTo(video.profile)
assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
@@ -156,7 +152,6 @@
assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
assertThat(audioProxy.channels).isEqualTo(audio.channels)
- assertThat(audioProxy.profile).isEqualTo(audio.profile)
}
@SdkSuppress(minSdkVersion = 33)
@@ -171,6 +166,7 @@
val videoProxy = profilesProxy!!.videoProfiles[0]
val audioProxy = profilesProxy.audioProfiles[0]
+ // Don't check video/audio profile, see cts/CamcorderProfileTest.java
assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
assertThat(videoProxy.codec).isEqualTo(video.codec)
@@ -179,7 +175,6 @@
assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
assertThat(videoProxy.width).isEqualTo(video.width)
assertThat(videoProxy.height).isEqualTo(video.height)
- assertThat(videoProxy.profile).isEqualTo(video.profile)
assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
@@ -188,17 +183,6 @@
assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
assertThat(audioProxy.channels).isEqualTo(audio.channels)
- assertThat(audioProxy.profile).isEqualTo(audio.profile)
- }
-
- @LabTestRule.LabTestOnly
- @SdkSuppress(minSdkVersion = 31)
- @Test
- fun detectNullVideoProfile() {
- assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
- skipTestOnDevicesWithProblematicBuild()
- val profiles = CamcorderProfile.getAll(cameraId, quality)!!
- assertThat(profiles.videoProfiles[0]).isNotNull()
}
@LabTestRule.LabTestOnly
@@ -211,20 +195,6 @@
assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
}
- private fun skipTestOnDevicesWithProblematicBuild() {
- // Skip test for b/265613005, b/223439995 and b/277174217
- val hasVideoProfilesQuirk = DeviceQuirks.get(InvalidVideoProfilesQuirk::class.java) != null
- assumeFalse(
- "Skip test with null VideoProfile issue. Unable to test.",
- hasVideoProfilesQuirk || isProblematicCuttlefishBuild()
- )
- }
-
- private fun isProblematicCuttlefishBuild(): Boolean {
- return Build.MODEL.contains("Cuttlefish", true) &&
- (Build.ID.startsWith("TP1A", true) || Build.ID.startsWith("TSE4", true))
- }
-
@Suppress("DEPRECATION")
private fun assumeValidCamcorderProfile(quality: Int) {
assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
index ec4cc78..ace8b46 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -45,8 +45,8 @@
// StreamConfigurationMap provided by Robolectric.
try {
return mStreamConfigurationMap.getOutputFormats();
- } catch (NullPointerException e) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e);
return null;
}
}
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 0cc031d..4efb36c 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -71,8 +71,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":camera:camera-testing")) {
// Ensure camera-testing does not pull in androidx.test dependencies
exclude(group:"androidx.test")
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
index 9a5bc12..260965f 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
@@ -143,7 +143,7 @@
private suspend fun processYuvAndVerifyOutputSize(outputFileOptions: OutputFileOptions?) {
// Arrange: create node with JPEG input and grayscale effect.
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(ImageFormat.YUV_420_888, ImageFormat.JPEG)
val imageIn =
createYuvFakeImageProxy(
@@ -162,7 +162,11 @@
private suspend fun processJpegAndVerifyEffectApplied(outputFileOptions: OutputFileOptions?) {
// Arrange: create node with JPEG input and grayscale effect.
val node =
- ProcessingNode(mainThreadExecutor(), InternalImageProcessor(GrayscaleImageEffect()))
+ ProcessingNode(
+ mainThreadExecutor(),
+ null,
+ InternalImageProcessor(GrayscaleImageEffect())
+ )
val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
val imageIn =
createJpegFakeImageProxy(
@@ -215,7 +219,7 @@
outputFileOptions: OutputFileOptions?
) {
// Arrange: create a request with no cropping
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -255,7 +259,7 @@
) {
// Arrange: create a request with no cropping
val format = ImageFormat.JPEG_R
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(format, format)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -301,7 +305,7 @@
private suspend fun inMemoryInputPacket_callbackInvoked(outputFileOptions: OutputFileOptions?) {
// Arrange.
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -341,7 +345,7 @@
) {
// Arrange.
val format = ImageFormat.JPEG_R
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(format, format)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -380,7 +384,7 @@
private suspend fun saveJpegOnDisk_verifyOutput(outputFileOptions: OutputFileOptions?) {
// Arrange: create a on-disk processing request.
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -420,7 +424,7 @@
private suspend fun saveJpegrOnDisk_verifyOutput(outputFileOptions: OutputFileOptions?) {
// Arrange: create a on-disk processing request.
val format = ImageFormat.JPEG_R
- val node = ProcessingNode(mainThreadExecutor())
+ val node = ProcessingNode(mainThreadExecutor(), null)
val nodeIn = ProcessingNode.In.of(format, format)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
@@ -478,7 +482,7 @@
// Arrange.
// Force inject the quirk for the A24 incorrect JPEG metadata problem
val node =
- ProcessingNode(mainThreadExecutor(), Quirks(listOf(IncorrectJpegMetadataQuirk())))
+ ProcessingNode(mainThreadExecutor(), Quirks(listOf(IncorrectJpegMetadataQuirk())), null)
val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
node.transform(nodeIn)
val takePictureCallback = FakeTakePictureCallback()
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 20b2ecd..73d7363 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
@@ -16,7 +16,9 @@
package androidx.camera.core;
+import static android.graphics.ImageFormat.JPEG;
import static android.graphics.ImageFormat.JPEG_R;
+import static android.graphics.ImageFormat.RAW_SENSOR;
import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
@@ -64,6 +66,7 @@
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.graphics.Rect;
+import android.hardware.camera2.CameraCharacteristics;
import android.location.Location;
import android.media.Image;
import android.media.ImageReader;
@@ -310,6 +313,12 @@
public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1;
/**
+ * Captures raw images in the {@link ImageFormat#RAW_SENSOR} image format.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final int OUTPUT_FORMAT_RAW = 2;
+
+ /**
* Provides a static configuration with implementation-agnostic options.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -463,12 +472,14 @@
null);
if (bufferFormat != null) {
Preconditions.checkArgument(!(isSessionProcessorEnabledInCurrentCamera()
- && bufferFormat != ImageFormat.JPEG),
+ && bufferFormat != JPEG),
"Cannot set non-JPEG buffer format with Extensions enabled.");
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
useSoftwareJpeg ? ImageFormat.YUV_420_888 : bufferFormat);
} else {
- if (isOutputFormatUltraHdr(builder.getMutableConfig())) {
+ if (isOutputFormatRaw(builder.getMutableConfig())) {
+ builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ } else if (isOutputFormatUltraHdr(builder.getMutableConfig())) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG_R);
builder.getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE,
DynamicRange.UNSPECIFIED);
@@ -480,12 +491,12 @@
builder.getMutableConfig().retrieveOption(OPTION_SUPPORTED_RESOLUTIONS,
null);
if (supportedSizes == null) {
- builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
+ builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG);
} else {
// Use Jpeg first if supported.
- if (isImageFormatSupported(supportedSizes, ImageFormat.JPEG)) {
+ if (isImageFormatSupported(supportedSizes, JPEG)) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
- ImageFormat.JPEG);
+ JPEG);
} else if (isImageFormatSupported(supportedSizes, ImageFormat.YUV_420_888)) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
ImageFormat.YUV_420_888);
@@ -515,6 +526,11 @@
OUTPUT_FORMAT_JPEG_ULTRA_HDR);
}
+ private static boolean isOutputFormatRaw(@NonNull MutableConfig config) {
+ return Objects.equals(config.retrieveOption(OPTION_OUTPUT_FORMAT, null),
+ OUTPUT_FORMAT_RAW);
+ }
+
/**
* Configures flash mode to CameraControlInternal once it is ready.
*/
@@ -979,6 +995,10 @@
formats.add(OUTPUT_FORMAT_JPEG_ULTRA_HDR);
}
+ if (isRawSupported()) {
+ formats.add(OUTPUT_FORMAT_RAW);
+ }
+
return formats;
}
@@ -990,6 +1010,15 @@
return false;
}
+
+ private boolean isRawSupported() {
+ if (mCameraInfo instanceof CameraInfoInternal) {
+ CameraInfoInternal cameraInfoInternal = (CameraInfoInternal) mCameraInfo;
+ return cameraInfoInternal.getSupportedOutputFormats().contains(RAW_SENSOR);
+ }
+
+ return false;
+ }
}
@NonNull
@@ -1133,7 +1162,7 @@
supported = false;
}
Integer bufferFormat = mutableConfig.retrieveOption(OPTION_BUFFER_FORMAT, null);
- if (bufferFormat != null && bufferFormat != ImageFormat.JPEG) {
+ if (bufferFormat != null && bufferFormat != JPEG) {
Logger.w(TAG, "Software JPEG cannot be used with non-JPEG output buffer format.");
supported = false;
}
@@ -1274,8 +1303,8 @@
// Prefer YUV because it takes less time to decode to bitmap.
List<Size> sizes = map.get(ImageFormat.YUV_420_888);
if (sizes == null || sizes.isEmpty()) {
- sizes = map.get(ImageFormat.JPEG);
- postviewFormat = ImageFormat.JPEG;
+ sizes = map.get(JPEG);
+ postviewFormat = JPEG;
}
if (sizes != null && !sizes.isEmpty()) {
@@ -1307,12 +1336,27 @@
}
}
- mImagePipeline = new ImagePipeline(config, resolution, getEffect(), isVirtualCamera,
+ CameraCharacteristics cameraCharacteristics = null;
+ if (getCamera() != null) {
+ try {
+ Object obj = getCamera().getCameraInfoInternal().getCameraCharacteristics();
+ if (obj instanceof CameraCharacteristics) {
+ cameraCharacteristics = (CameraCharacteristics) obj;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "getCameraCharacteristics failed", e);
+ }
+ }
+
+ mImagePipeline = new ImagePipeline(config, resolution,
+ cameraCharacteristics,
+ getEffect(), isVirtualCamera,
postViewSize, postviewFormat);
if (mTakePictureManager == null) {
// mTakePictureManager is reused when the Surface is reset.
- mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+ mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+ mImageCaptureControl);
}
mTakePictureManager.setImagePipeline(mImagePipeline);
@@ -1597,7 +1641,7 @@
*/
@OptIn(markerClass = androidx.camera.core.ExperimentalImageCaptureOutputFormat.class)
@Target({ElementType.TYPE_USE})
- @IntDef({OUTPUT_FORMAT_JPEG, OUTPUT_FORMAT_JPEG_ULTRA_HDR})
+ @IntDef({OUTPUT_FORMAT_JPEG, OUTPUT_FORMAT_JPEG_ULTRA_HDR, OUTPUT_FORMAT_RAW})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface OutputFormat {
@@ -2319,12 +2363,14 @@
if (bufferFormat != null) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, bufferFormat);
} else {
- if (isOutputFormatUltraHdr(getMutableConfig())) {
+ if (isOutputFormatRaw(getMutableConfig())) {
+ getMutableConfig().insertOption(OPTION_INPUT_FORMAT, RAW_SENSOR);
+ } else if (isOutputFormatUltraHdr(getMutableConfig())) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG_R);
getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE,
DynamicRange.UNSPECIFIED);
} else {
- getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
+ getMutableConfig().insertOption(OPTION_INPUT_FORMAT, JPEG);
}
}
@@ -2829,7 +2875,7 @@
* <p>If not set, the output format will default to {@link #OUTPUT_FORMAT_JPEG}.
*
* @param outputFormat The output image format. Value is {@link #OUTPUT_FORMAT_JPEG} or
- * {@link #OUTPUT_FORMAT_JPEG_ULTRA_HDR}.
+ * {@link #OUTPUT_FORMAT_JPEG_ULTRA_HDR} or {@link #OUTPUT_FORMAT_RAW}.
* @return The current Builder.
*
* @see OutputFormat
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
index b09fe43..b46c236 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureExt.kt
@@ -162,7 +162,7 @@
}
override fun onCaptureSuccess(imageProxy: ImageProxy) {
- delegate?.onCaptureSuccess(imageProxy)
+ delegate?.onCaptureSuccess(imageProxy) ?: run { imageProxy.close() }
}
override fun onError(exception: ImageCaptureException) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/DngImage2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/DngImage2Disk.java
new file mode 100644
index 0000000..c19ed71
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/DngImage2Disk.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_FILE_IO;
+import static androidx.camera.core.imagecapture.FileUtil.createTempFile;
+import static androidx.camera.core.imagecapture.FileUtil.moveFileToTarget;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.DngCreator;
+import android.media.ExifInterface;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.processing.Operation;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class DngImage2Disk implements Operation<DngImage2Disk.In, ImageCapture.OutputFileResults> {
+
+ @NonNull
+ private DngCreator mDngCreator;
+
+ public DngImage2Disk(@NonNull CameraCharacteristics cameraCharacteristics,
+ @NonNull CaptureResult captureResult) {
+ this(new DngCreator(cameraCharacteristics, captureResult));
+ }
+
+ @VisibleForTesting
+ DngImage2Disk(@NonNull DngCreator dngCreator) {
+ mDngCreator = dngCreator;
+ }
+
+ @NonNull
+ @Override
+ public ImageCapture.OutputFileResults apply(@NonNull In in) throws ImageCaptureException {
+ ImageCapture.OutputFileOptions options = in.getOutputFileOptions();
+ File tempFile = createTempFile(options);
+ writeImageToFile(tempFile, in.getImageProxy(), in.getRotationDegrees());
+ Uri uri = moveFileToTarget(tempFile, options);
+ return new ImageCapture.OutputFileResults(uri);
+ }
+
+ /**
+ * Writes byte array to the given {@link File}.
+ */
+ @OptIn(markerClass = ExperimentalGetImage.class)
+ private void writeImageToFile(
+ @NonNull File tempFile,
+ @NonNull ImageProxy imageProxy,
+ int rotationDegrees) throws ImageCaptureException {
+ try (FileOutputStream output = new FileOutputStream(tempFile)) {
+ mDngCreator.setOrientation(computeExifOrientation(rotationDegrees));
+ mDngCreator.writeImage(output, imageProxy.getImage());
+ } catch (IllegalArgumentException e) {
+ throw new ImageCaptureException(ERROR_FILE_IO,
+ "Image with an unsupported format was used", e);
+ } catch (IllegalStateException e) {
+ throw new ImageCaptureException(ERROR_FILE_IO,
+ "Not enough metadata information has been "
+ + "set to write a well-formatted DNG file", e);
+ } catch (IOException e) {
+ throw new ImageCaptureException(ERROR_FILE_IO, "Failed to write to temp file", e);
+ } finally {
+ imageProxy.close();
+ }
+ }
+
+ static int computeExifOrientation(int rotationDegrees) {
+ switch (rotationDegrees) {
+ case 0:
+ return ExifInterface.ORIENTATION_NORMAL;
+ case 90:
+ return ExifInterface.ORIENTATION_ROTATE_90;
+ case 180:
+ return ExifInterface.ORIENTATION_ROTATE_180;
+ case 270:
+ return ExifInterface.ORIENTATION_ROTATE_270;
+ }
+ return ExifInterface.ORIENTATION_UNDEFINED;
+ }
+
+ /**
+ * Input packet.
+ */
+ @AutoValue
+ abstract static class In {
+
+ @NonNull
+ abstract ImageProxy getImageProxy();
+
+ abstract int getRotationDegrees();
+
+ @NonNull
+ abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+
+ @NonNull
+ static DngImage2Disk.In of(
+ @NonNull ImageProxy imageProxy,
+ int rotationDegrees,
+ @NonNull ImageCapture.OutputFileOptions outputFileOptions) {
+ return new AutoValue_DngImage2Disk_In(imageProxy,
+ rotationDegrees, outputFileOptions);
+ }
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/FileUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/FileUtil.java
new file mode 100644
index 0000000..befa01a
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/FileUtil.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_FILE_IO;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.impl.utils.Exif;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+/**
+ * Utility class for file read and write operations.
+ */
+public final class FileUtil {
+
+ private static final String TEMP_FILE_PREFIX = "CameraX";
+ private static final String TEMP_FILE_SUFFIX = ".tmp";
+ private static final int COPY_BUFFER_SIZE = 1024;
+ private static final int PENDING = 1;
+ private static final int NOT_PENDING = 0;
+
+ private FileUtil() {}
+
+ /**
+ * Creates a temporary Dng file.
+ */
+ @NonNull
+ static File createTempFile(@NonNull ImageCapture.OutputFileOptions options)
+ throws ImageCaptureException {
+ try {
+ File appProvidedFile = options.getFile();
+ if (appProvidedFile != null) {
+ // For saving-to-file case, write to the target folder and rename for better
+ // performance. The file extensions must be the same as app provided to avoid the
+ // directory access problem.
+ return new File(appProvidedFile.getParent(),
+ TEMP_FILE_PREFIX + UUID.randomUUID().toString()
+ + getFileExtensionWithDot(appProvidedFile));
+ } else {
+ return File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
+ }
+ } catch (IOException e) {
+ throw new ImageCaptureException(ERROR_FILE_IO, "Failed to create temp file.", e);
+ }
+ }
+
+ /**
+ * Updates exif data.
+ *
+ * @param tempFile
+ * @param originalExif
+ * @param options
+ * @param rotationDegrees
+ * @throws ImageCaptureException
+ */
+ static void updateFileExif(
+ @NonNull File tempFile,
+ @NonNull Exif originalExif,
+ @NonNull ImageCapture.OutputFileOptions options,
+ int rotationDegrees)
+ throws ImageCaptureException {
+ try {
+ // Create new exif based on the original exif.
+ Exif exif = Exif.createFromFile(tempFile);
+ originalExif.copyToCroppedImage(exif);
+
+ if (exif.getRotation() == 0 && rotationDegrees != 0) {
+ // When the HAL does not handle rotation, exif rotation is 0. In which case we
+ // apply the packet rotation.
+ // See: EXIF_ROTATION_AVAILABILITY
+ exif.rotate(rotationDegrees);
+ }
+
+ // Overwrite exif based on metadata.
+ ImageCapture.Metadata metadata = options.getMetadata();
+ if (metadata.isReversedHorizontal()) {
+ exif.flipHorizontally();
+ }
+ if (metadata.isReversedVertical()) {
+ exif.flipVertically();
+ }
+ if (metadata.getLocation() != null) {
+ exif.attachLocation(metadata.getLocation());
+ }
+ exif.save();
+ } catch (IOException e) {
+ throw new ImageCaptureException(ERROR_FILE_IO, "Failed to update Exif data", e);
+ }
+ }
+
+ /**
+ * Copies the file to target, deletes the original file and returns the target's {@link Uri}.
+ *
+ * @return null if the target is {@link OutputStream}.
+ */
+ @Nullable
+ static Uri moveFileToTarget(
+ @NonNull File tempFile, @NonNull ImageCapture.OutputFileOptions options)
+ throws ImageCaptureException {
+ Uri uri = null;
+ try {
+ if (isSaveToMediaStore(options)) {
+ uri = copyFileToMediaStore(tempFile, options);
+ } else if (isSaveToOutputStream(options)) {
+ copyFileToOutputStream(tempFile, requireNonNull(options.getOutputStream()));
+ } else if (isSaveToFile(options)) {
+ uri = copyFileToFile(tempFile, requireNonNull(options.getFile()));
+ }
+ } catch (IOException e) {
+ throw new ImageCaptureException(
+ ERROR_FILE_IO, "Failed to write to OutputStream.", null);
+ } finally {
+ tempFile.delete();
+ }
+ return uri;
+ }
+
+ private static String getFileExtensionWithDot(File file) {
+ String fileName = file.getName();
+ int dotIndex = fileName.lastIndexOf('.');
+ if (dotIndex >= 0) {
+ return fileName.substring(dotIndex);
+ } else {
+ return "";
+ }
+ }
+
+ private static Uri copyFileToMediaStore(
+ @NonNull File file,
+ @NonNull ImageCapture.OutputFileOptions options)
+ throws ImageCaptureException {
+ ContentResolver contentResolver = requireNonNull(options.getContentResolver());
+ ContentValues values = options.getContentValues() != null
+ ? new ContentValues(options.getContentValues())
+ : new ContentValues();
+ setContentValuePendingFlag(values, PENDING);
+ Uri uri = null;
+ try {
+ uri = contentResolver.insert(options.getSaveCollection(), values);
+ if (uri == null) {
+ throw new ImageCaptureException(
+ ERROR_FILE_IO, "Failed to insert a MediaStore URI.", null);
+ }
+ copyTempFileToUri(file, uri, contentResolver);
+ } catch (IOException | SecurityException e) {
+ throw new ImageCaptureException(
+ ERROR_FILE_IO, "Failed to write to MediaStore URI: " + uri, e);
+ } finally {
+ if (uri != null) {
+ updateUriPendingStatus(uri, contentResolver, NOT_PENDING);
+ }
+ }
+ return uri;
+ }
+
+ private static Uri copyFileToFile(@NonNull File source, @NonNull File target)
+ throws ImageCaptureException {
+ // Normally File#renameTo will overwrite the targetFile even if it already exists.
+ // Just in case of unexpected behavior on certain platforms or devices, delete the
+ // target file before renaming.
+ if (target.exists()) {
+ target.delete();
+ }
+ if (!source.renameTo(target)) {
+ throw new ImageCaptureException(
+ ERROR_FILE_IO,
+ "Failed to overwrite the file: " + target.getAbsolutePath(),
+ null);
+ }
+ return Uri.fromFile(target);
+ }
+
+ /**
+ * Copies temp file to {@link Uri}.
+ */
+ private static void copyTempFileToUri(
+ @NonNull File tempFile,
+ @NonNull Uri uri,
+ @NonNull ContentResolver contentResolver) throws IOException {
+ try (OutputStream outputStream = contentResolver.openOutputStream(uri)) {
+ if (outputStream == null) {
+ throw new FileNotFoundException(uri + " cannot be resolved.");
+ }
+ copyFileToOutputStream(tempFile, outputStream);
+ }
+ }
+
+ @SuppressWarnings("IOStreamConstructor")
+ private static void copyFileToOutputStream(@NonNull File file,
+ @NonNull OutputStream outputStream)
+ throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ byte[] buf = new byte[COPY_BUFFER_SIZE];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ outputStream.write(buf, 0, len);
+ }
+ }
+ }
+
+ /**
+ * Removes IS_PENDING flag during the writing to {@link Uri}.
+ */
+ private static void updateUriPendingStatus(@NonNull Uri outputUri,
+ @NonNull ContentResolver contentResolver, int isPending) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ContentValues values = new ContentValues();
+ setContentValuePendingFlag(values, isPending);
+ contentResolver.update(outputUri, values, null, null);
+ }
+ }
+
+ /** Set IS_PENDING flag to {@link ContentValues}. */
+ private static void setContentValuePendingFlag(@NonNull ContentValues values, int isPending) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ values.put(MediaStore.Images.Media.IS_PENDING, isPending);
+ }
+ }
+
+ private static boolean isSaveToMediaStore(ImageCapture.OutputFileOptions outputFileOptions) {
+ return outputFileOptions.getSaveCollection() != null
+ && outputFileOptions.getContentResolver() != null
+ && outputFileOptions.getContentValues() != null;
+ }
+
+ private static boolean isSaveToFile(ImageCapture.OutputFileOptions outputFileOptions) {
+ return outputFileOptions.getFile() != null;
+ }
+
+ private static boolean isSaveToOutputStream(ImageCapture.OutputFileOptions outputFileOptions) {
+ return outputFileOptions.getOutputStream() != null;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index 6f12e60..812be3d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -22,10 +22,12 @@
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.hasCropping;
import static androidx.camera.core.internal.utils.ImageUtil.isJpegFormats;
+import static androidx.camera.core.internal.utils.ImageUtil.isRawFormats;
import static java.util.Objects.requireNonNull;
import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
import android.media.ImageReader;
import android.util.Size;
@@ -87,8 +89,9 @@
@VisibleForTesting
public ImagePipeline(
@NonNull ImageCaptureConfig useCaseConfig,
- @NonNull Size cameraSurfaceSize) {
- this(useCaseConfig, cameraSurfaceSize, /*cameraEffect=*/ null,
+ @NonNull Size cameraSurfaceSize,
+ @NonNull CameraCharacteristics cameraCharacteristics) {
+ this(useCaseConfig, cameraSurfaceSize, cameraCharacteristics, /*cameraEffect=*/ null,
/*isVirtualCamera=*/ false, /* postviewSize */ null, ImageFormat.YUV_420_888);
}
@@ -96,9 +99,10 @@
public ImagePipeline(
@NonNull ImageCaptureConfig useCaseConfig,
@NonNull Size cameraSurfaceSize,
+ @NonNull CameraCharacteristics cameraCharacteristics,
@Nullable CameraEffect cameraEffect,
boolean isVirtualCamera) {
- this(useCaseConfig, cameraSurfaceSize, cameraEffect, isVirtualCamera,
+ this(useCaseConfig, cameraSurfaceSize, cameraCharacteristics, cameraEffect, isVirtualCamera,
null, ImageFormat.YUV_420_888);
}
@@ -106,6 +110,7 @@
public ImagePipeline(
@NonNull ImageCaptureConfig useCaseConfig,
@NonNull Size cameraSurfaceSize,
+ @Nullable CameraCharacteristics cameraCharacteristics,
@Nullable CameraEffect cameraEffect,
boolean isVirtualCamera,
@Nullable Size postviewSize,
@@ -118,6 +123,7 @@
mCaptureNode = new CaptureNode();
mProcessingNode = new ProcessingNode(
requireNonNull(mUseCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor())),
+ cameraCharacteristics,
cameraEffect != null ? new InternalImageProcessor(cameraEffect) : null);
// Connect nodes
@@ -246,6 +252,9 @@
if (inputFormat != null && inputFormat == ImageFormat.JPEG_R) {
return ImageFormat.JPEG_R;
}
+ if (inputFormat != null && inputFormat == ImageFormat.RAW_SENSOR) {
+ return ImageFormat.RAW_SENSOR;
+ }
// By default, use JPEG format.
return ImageFormat.JPEG;
@@ -303,9 +312,10 @@
builder.addSurface(mPipelineIn.getSurface());
builder.setPostviewEnabled(shouldEnablePostview());
- // Only sets the JPEG rotation and quality for JPEG formats. Some devices do not
+ // Sets the JPEG rotation and quality for JPEG and RAW formats. Some devices do not
// handle these configs for non-JPEG images. See b/204375890.
- if (isJpegFormats(mPipelineIn.getInputFormat())) {
+ if (isJpegFormats(mPipelineIn.getInputFormat())
+ || isRawFormats(mPipelineIn.getInputFormat())) {
if (EXIF_ROTATION_AVAILABILITY.isRotationOptionSupported()) {
builder.addImplementationOption(CaptureConfig.OPTION_ROTATION,
takePictureRequest.getRotationDegrees());
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..26085ba 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -16,20 +16,17 @@
package androidx.camera.core.imagecapture;
import static androidx.camera.core.ImageCapture.ERROR_FILE_IO;
+import static androidx.camera.core.imagecapture.FileUtil.createTempFile;
+import static androidx.camera.core.imagecapture.FileUtil.moveFileToTarget;
+import static androidx.camera.core.imagecapture.FileUtil.updateFileExif;
import static java.util.Objects.requireNonNull;
-import android.content.ContentResolver;
-import android.content.ContentValues;
import android.net.Uri;
-import android.os.Build;
-import android.provider.MediaStore;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.impl.utils.Exif;
import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -37,25 +34,14 @@
import com.google.auto.value.AutoValue;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.UUID;
/**
* Saves JPEG bytes to disk.
*/
class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
- private static final String TEMP_FILE_PREFIX = "CameraX";
- private static final String TEMP_FILE_SUFFIX = ".tmp";
- private static final int COPY_BUFFER_SIZE = 1024;
- private static final int PENDING = 1;
- private static final int NOT_PENDING = 0;
-
@NonNull
@Override
public ImageCapture.OutputFileResults apply(@NonNull In in) throws ImageCaptureException {
@@ -70,42 +56,9 @@
}
/**
- * Creates a temporary JPEG file.
- */
- @NonNull
- private static File createTempFile(@NonNull ImageCapture.OutputFileOptions options)
- throws ImageCaptureException {
- try {
- File appProvidedFile = options.getFile();
- if (appProvidedFile != null) {
- // For saving-to-file case, write to the target folder and rename for better
- // performance. The file extensions must be the same as app provided to avoid the
- // directory access problem.
- return new File(appProvidedFile.getParent(),
- TEMP_FILE_PREFIX + UUID.randomUUID().toString()
- + getFileExtensionWithDot(appProvidedFile));
- } else {
- return File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
- }
- } catch (IOException e) {
- throw new ImageCaptureException(ERROR_FILE_IO, "Failed to create temp file.", e);
- }
- }
-
- private static String getFileExtensionWithDot(File file) {
- String fileName = file.getName();
- int dotIndex = fileName.lastIndexOf('.');
- if (dotIndex >= 0) {
- return fileName.substring(dotIndex);
- } else {
- return "";
- }
- }
-
- /**
* Writes byte array to the given {@link File}.
*/
- private static void writeBytesToFile(
+ static void writeBytesToFile(
@NonNull File tempFile, @NonNull byte[] bytes) throws ImageCaptureException {
try (FileOutputStream output = new FileOutputStream(tempFile)) {
InvalidJpegDataParser invalidJpegDataParser = new InvalidJpegDataParser();
@@ -115,174 +68,6 @@
}
}
- private static void updateFileExif(
- @NonNull File tempFile,
- @NonNull Exif originalExif,
- @NonNull ImageCapture.OutputFileOptions options,
- int rotationDegrees)
- throws ImageCaptureException {
- try {
- // Create new exif based on the original exif.
- Exif exif = Exif.createFromFile(tempFile);
- originalExif.copyToCroppedImage(exif);
-
- if (exif.getRotation() == 0 && rotationDegrees != 0) {
- // When the HAL does not handle rotation, exif rotation is 0. In which case we
- // apply the packet rotation.
- // See: EXIF_ROTATION_AVAILABILITY
- exif.rotate(rotationDegrees);
- }
-
- // Overwrite exif based on metadata.
- ImageCapture.Metadata metadata = options.getMetadata();
- if (metadata.isReversedHorizontal()) {
- exif.flipHorizontally();
- }
- if (metadata.isReversedVertical()) {
- exif.flipVertically();
- }
- if (metadata.getLocation() != null) {
- exif.attachLocation(metadata.getLocation());
- }
- exif.save();
- } catch (IOException e) {
- throw new ImageCaptureException(ERROR_FILE_IO, "Failed to update Exif data", e);
- }
- }
-
- /**
- * Copies the file to target, deletes the original file and returns the target's {@link Uri}.
- *
- * @return null if the target is {@link OutputStream}.
- */
- @Nullable
- static Uri moveFileToTarget(
- @NonNull File tempFile, @NonNull ImageCapture.OutputFileOptions options)
- throws ImageCaptureException {
- Uri uri = null;
- try {
- if (isSaveToMediaStore(options)) {
- uri = copyFileToMediaStore(tempFile, options);
- } else if (isSaveToOutputStream(options)) {
- copyFileToOutputStream(tempFile, requireNonNull(options.getOutputStream()));
- } else if (isSaveToFile(options)) {
- uri = copyFileToFile(tempFile, requireNonNull(options.getFile()));
- }
- } catch (IOException e) {
- throw new ImageCaptureException(
- ERROR_FILE_IO, "Failed to write to OutputStream.", null);
- } finally {
- tempFile.delete();
- }
- return uri;
- }
-
- private static Uri copyFileToMediaStore(
- @NonNull File file,
- @NonNull ImageCapture.OutputFileOptions options)
- throws ImageCaptureException {
- ContentResolver contentResolver = requireNonNull(options.getContentResolver());
- ContentValues values = options.getContentValues() != null
- ? new ContentValues(options.getContentValues())
- : new ContentValues();
- setContentValuePendingFlag(values, PENDING);
- Uri uri = null;
- try {
- uri = contentResolver.insert(options.getSaveCollection(), values);
- if (uri == null) {
- throw new ImageCaptureException(
- ERROR_FILE_IO, "Failed to insert a MediaStore URI.", null);
- }
- copyTempFileToUri(file, uri, contentResolver);
- } catch (IOException | SecurityException e) {
- throw new ImageCaptureException(
- ERROR_FILE_IO, "Failed to write to MediaStore URI: " + uri, e);
- } finally {
- if (uri != null) {
- updateUriPendingStatus(uri, contentResolver, NOT_PENDING);
- }
- }
- return uri;
- }
-
- private static Uri copyFileToFile(@NonNull File source, @NonNull File target)
- throws ImageCaptureException {
- // Normally File#renameTo will overwrite the targetFile even if it already exists.
- // Just in case of unexpected behavior on certain platforms or devices, delete the
- // target file before renaming.
- if (target.exists()) {
- target.delete();
- }
- if (!source.renameTo(target)) {
- throw new ImageCaptureException(
- ERROR_FILE_IO,
- "Failed to overwrite the file: " + target.getAbsolutePath(),
- null);
- }
- return Uri.fromFile(target);
- }
-
- /**
- * Copies temp file to {@link Uri}.
- */
- private static void copyTempFileToUri(
- @NonNull File tempFile,
- @NonNull Uri uri,
- @NonNull ContentResolver contentResolver) throws IOException {
- try (OutputStream outputStream = contentResolver.openOutputStream(uri)) {
- if (outputStream == null) {
- throw new FileNotFoundException(uri + " cannot be resolved.");
- }
- copyFileToOutputStream(tempFile, outputStream);
- }
- }
-
- @SuppressWarnings("IOStreamConstructor")
- private static void copyFileToOutputStream(@NonNull File file,
- @NonNull OutputStream outputStream)
- throws IOException {
- try (InputStream in = new FileInputStream(file)) {
- byte[] buf = new byte[COPY_BUFFER_SIZE];
- int len;
- while ((len = in.read(buf)) > 0) {
- outputStream.write(buf, 0, len);
- }
- }
- }
-
- /**
- * Removes IS_PENDING flag during the writing to {@link Uri}.
- */
- private static void updateUriPendingStatus(@NonNull Uri outputUri,
- @NonNull ContentResolver contentResolver, int isPending) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- ContentValues values = new ContentValues();
- setContentValuePendingFlag(values, isPending);
- contentResolver.update(outputUri, values, null, null);
- }
- }
-
- /** Set IS_PENDING flag to {@link ContentValues}. */
- private static void setContentValuePendingFlag(@NonNull ContentValues values, int isPending) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- values.put(MediaStore.Images.Media.IS_PENDING, isPending);
- }
- }
-
- private static boolean isSaveToMediaStore(ImageCapture.OutputFileOptions outputFileOptions) {
- return outputFileOptions.getSaveCollection() != null
- && outputFileOptions.getContentResolver() != null
- && outputFileOptions.getContentValues() != null;
- }
-
- private static boolean isSaveToFile(ImageCapture.OutputFileOptions outputFileOptions) {
- return outputFileOptions.getFile() != null;
- }
-
- private static boolean isSaveToOutputStream(ImageCapture.OutputFileOptions outputFileOptions) {
- return outputFileOptions.getOutputStream() != null;
- }
-
/**
* Input packet.
*/
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index 5243d9c8..fddd9b7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -17,11 +17,13 @@
package androidx.camera.core.imagecapture;
import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.RAW_SENSOR;
import static android.graphics.ImageFormat.YUV_420_888;
import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
import static androidx.camera.core.internal.utils.ImageUtil.isJpegFormats;
+import static androidx.camera.core.internal.utils.ImageUtil.isRawFormats;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
@@ -29,6 +31,7 @@
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -66,6 +69,9 @@
@Nullable
final InternalImageProcessor mImageProcessor;
+ @Nullable
+ private final CameraCharacteristics mCameraCharacteristics;
+
private ProcessingNode.In mInputEdge;
private Operation<InputPacket, Packet<ImageProxy>> mInput2Packet;
private Operation<Image2JpegBytes.In, Packet<byte[]>> mImage2JpegBytes;
@@ -84,18 +90,23 @@
* {@link CameraXExecutors#ioExecutor()}
*/
@VisibleForTesting
- ProcessingNode(@NonNull Executor blockingExecutor) {
- this(blockingExecutor, /*imageProcessor=*/null, DeviceQuirks.getAll());
+ ProcessingNode(@NonNull Executor blockingExecutor,
+ @Nullable CameraCharacteristics cameraCharacteristics) {
+ this(blockingExecutor, cameraCharacteristics,
+ /*imageProcessor=*/null, DeviceQuirks.getAll());
}
@VisibleForTesting
- ProcessingNode(@NonNull Executor blockingExecutor, @NonNull Quirks quirks) {
- this(blockingExecutor, /*imageProcessor=*/null, quirks);
+ ProcessingNode(@NonNull Executor blockingExecutor,
+ @NonNull Quirks quirks,
+ @Nullable CameraCharacteristics cameraCharacteristics) {
+ this(blockingExecutor, cameraCharacteristics, /*imageProcessor=*/null, quirks);
}
ProcessingNode(@NonNull Executor blockingExecutor,
+ @Nullable CameraCharacteristics cameraCharacteristics,
@Nullable InternalImageProcessor imageProcessor) {
- this(blockingExecutor, imageProcessor, DeviceQuirks.getAll());
+ this(blockingExecutor, cameraCharacteristics, imageProcessor, DeviceQuirks.getAll());
}
/**
@@ -104,6 +115,7 @@
* @param imageProcessor external effect for post-processing.
*/
ProcessingNode(@NonNull Executor blockingExecutor,
+ @Nullable CameraCharacteristics cameraCharacteristics,
@Nullable InternalImageProcessor imageProcessor,
@NonNull Quirks quirks) {
boolean isLowMemoryDevice = DeviceQuirks.get(LowMemoryQuirk.class) != null;
@@ -113,6 +125,7 @@
mBlockingExecutor = blockingExecutor;
}
mImageProcessor = imageProcessor;
+ mCameraCharacteristics = cameraCharacteristics;
mQuirks = quirks;
mHasIncorrectJpegMetadataQuirk = quirks.contains(IncorrectJpegMetadataQuirk.class);
}
@@ -216,17 +229,33 @@
ImageCapture.OutputFileResults processOnDiskCapture(@NonNull InputPacket inputPacket)
throws ImageCaptureException {
int format = mInputEdge.getOutputFormat();
- checkArgument(isJpegFormats(format), String.format("On-disk capture only support JPEG and"
- + " JPEG/R output formats. Output format: %s", format));
+ checkArgument(isJpegFormats(format)
+ || isRawFormats(format),
+ String.format("On-disk capture only support JPEG and"
+ + " JPEG/R and RAW output formats. Output format: %s", format));
ProcessingRequest request = inputPacket.getProcessingRequest();
Packet<ImageProxy> originalImage = mInput2Packet.apply(inputPacket);
- Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
- Image2JpegBytes.In.of(originalImage, request.getJpegQuality()));
- if (jpegBytes.hasCropping() || mBitmapEffect != null) {
- jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
+
+ switch (format) {
+ case RAW_SENSOR:
+ DngImage2Disk dngImage2Disk = new DngImage2Disk(
+ requireNonNull(mCameraCharacteristics),
+ originalImage.getCameraCaptureResult().getCaptureResult());
+ return dngImage2Disk.apply(DngImage2Disk.In.of(
+ originalImage.getData(),
+ originalImage.getRotationDegrees(),
+ requireNonNull(request.getOutputFileOptions())));
+ case JPEG:
+ default:
+ Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
+ Image2JpegBytes.In.of(originalImage, request.getJpegQuality()));
+ if (jpegBytes.hasCropping() || mBitmapEffect != null) {
+ jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
+ }
+ return mJpegBytes2Disk.apply(
+ JpegBytes2Disk.In.of(jpegBytes,
+ requireNonNull(request.getOutputFileOptions())));
}
- return mJpegBytes2Disk.apply(
- JpegBytes2Disk.In.of(jpegBytes, requireNonNull(request.getOutputFileOptions())));
}
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
package androidx.camera.core.imagecapture;
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
import java.util.List;
/**
@@ -62,46 +41,13 @@
*
* <p>The thread safety is guaranteed by using the main thread.
*/
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
- private static final String TAG = "TakePictureManager";
-
- // Queue of new requests that have not been sent to the pipeline/camera.
- @VisibleForTesting
- final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
- final ImageCaptureControl mImageCaptureControl;
- ImagePipeline mImagePipeline;
-
- // The current request being processed by the camera. Only one request can be processed by
- // the camera at the same time. Null if the camera is idle.
- @Nullable
- private RequestWithCallback mCapturingRequest;
- // The current requests that have not received a result or an error.
- private final List<RequestWithCallback> mIncompleteRequests;
-
- // Once paused, the class waits until the class is resumed to handle new requests.
- boolean mPaused = false;
-
- /**
- * @param imageCaptureControl for controlling {@link ImageCapture}
- */
- @MainThread
- public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
- checkMainThread();
- mImageCaptureControl = imageCaptureControl;
- mIncompleteRequests = new ArrayList<>();
- }
-
+public interface TakePictureManager {
/**
* Sets the {@link ImagePipeline} for building capture requests and post-processing camera
* output.
*/
@MainThread
- public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
- checkMainThread();
- mImagePipeline = imagePipeline;
- mImagePipeline.setOnImageCloseListener(this);
- }
+ void setImagePipeline(@NonNull ImagePipeline imagePipeline);
/**
* Adds requests to the queue.
@@ -109,201 +55,52 @@
* <p>The requests in the queue will be executed based on the order being added.
*/
@MainThread
- public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
- checkMainThread();
- mNewRequests.offer(takePictureRequest);
- issueNextRequest();
- }
-
- @MainThread
- @Override
- public void retryRequest(@NonNull TakePictureRequest request) {
- checkMainThread();
- Logger.d(TAG, "Add a new request for retrying.");
- // Insert the request to the front of the queue.
- mNewRequests.addFirst(request);
- // Try to issue the newly added request in case condition allows.
- issueNextRequest();
- }
+ void offerRequest(@NonNull TakePictureRequest takePictureRequest);
/**
* Pauses sending request to camera.
*/
@MainThread
- public void pause() {
- checkMainThread();
- mPaused = true;
-
- // Always retry because the camera may not send an error callback during the reset.
- if (mCapturingRequest != null) {
- mCapturingRequest.abortSilentlyAndRetry();
- }
- }
+ void pause();
/**
* Resumes sending request to camera.
*/
@MainThread
- public void resume() {
- checkMainThread();
- mPaused = false;
- issueNextRequest();
- }
+ void resume();
/**
* Clears the requests queue.
*/
@MainThread
- public void abortRequests() {
- checkMainThread();
- ImageCaptureException exception =
- new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
- // Clear pending request first so aborting in-flight request won't trigger another capture.
- for (TakePictureRequest request : mNewRequests) {
- request.onError(exception);
- }
- mNewRequests.clear();
-
- // Abort the in-flight request after clearing the pending requests.
- // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
- List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
- for (RequestWithCallback request : requestsSnapshot) {
- // TODO: optimize the performance by not processing aborted requests.
- request.abortAndSendErrorToApp(exception);
- }
- }
+ void abortRequests();
/**
- * Issues the next request if conditions allow.
+ * Returns whether any capture request is being processed currently.
*/
- @MainThread
- void issueNextRequest() {
- checkMainThread();
- Log.d(TAG, "Issue the next TakePictureRequest.");
- if (hasCapturingRequest()) {
- Log.d(TAG, "There is already a request in-flight.");
- return;
- }
- if (mPaused) {
- Log.d(TAG, "The class is paused.");
- return;
- }
- if (mImagePipeline.getCapacity() == 0) {
- Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
- return;
- }
- TakePictureRequest request = mNewRequests.poll();
- if (request == null) {
- Log.d(TAG, "No new request.");
- return;
- }
-
- RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
- trackCurrentRequests(requestWithCallback);
-
- // Send requests.
- Pair<CameraRequest, ProcessingRequest> requests =
- mImagePipeline.createRequests(request, requestWithCallback,
- requestWithCallback.getCaptureFuture());
- CameraRequest cameraRequest = requireNonNull(requests.first);
- ProcessingRequest processingRequest = requireNonNull(requests.second);
- mImagePipeline.submitProcessingRequest(processingRequest);
- ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
- requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
- }
-
- /**
- * Waits for the request to finish before issuing the next.
- */
- private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
- checkState(!hasCapturingRequest());
- mCapturingRequest = requestWithCallback;
-
- // Waits for the capture to finish before issuing the next.
- mCapturingRequest.getCaptureFuture().addListener(() -> {
- mCapturingRequest = null;
- issueNextRequest();
- }, directExecutor());
-
- // Track all incomplete requests so we can abort them when UseCase is detached.
- mIncompleteRequests.add(requestWithCallback);
- requestWithCallback.getCompleteFuture().addListener(() -> {
- mIncompleteRequests.remove(requestWithCallback);
- }, directExecutor());
- }
-
- /**
- * Submit a request to camera and post-processing pipeline.
- *
- * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
- */
- @MainThread
- private ListenableFuture<Void> submitCameraRequest(
- @NonNull CameraRequest cameraRequest) {
- checkMainThread();
- mImageCaptureControl.lockFlashMode();
- ListenableFuture<Void> captureRequestFuture =
- mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
- Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- mImageCaptureControl.unlockFlashMode();
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
- if (cameraRequest.isAborted()) {
- // When the pipeline is recreated, the in-flight request is aborted and
- // retried. On legacy devices, the camera may return CancellationException
- // for the aborted request which causes the retried request to fail. Return
- // early if the request has been aborted.
- return;
- } else {
- int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
- if (throwable instanceof ImageCaptureException) {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, (ImageCaptureException) throwable));
- } else {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, new ImageCaptureException(
- ERROR_CAPTURE_FAILED,
- "Failed to submit capture request",
- throwable)));
- }
- }
- mImageCaptureControl.unlockFlashMode();
- }
- }, mainThreadExecutor());
- return captureRequestFuture;
- }
-
@VisibleForTesting
- boolean hasCapturingRequest() {
- return mCapturingRequest != null;
- }
+ boolean hasCapturingRequest();
+ /**
+ * Returns the capture request being processed currently.
+ */
@VisibleForTesting
@Nullable
- public RequestWithCallback getCapturingRequest() {
- return mCapturingRequest;
- }
+ RequestWithCallback getCapturingRequest();
+ /**
+ * Returns the requests that have not received a result or an error yet.
+ */
+ @NonNull
@VisibleForTesting
- List<RequestWithCallback> getIncompleteRequests() {
- return mIncompleteRequests;
- }
+ List<RequestWithCallback> getIncompleteRequests();
+ /**
+ * Returns the {@link ImagePipeline} instance used under the hood.
+ */
@VisibleForTesting
@NonNull
- public ImagePipeline getImagePipeline() {
- return mImagePipeline;
- }
-
- @Override
- public void onImageClose(@NonNull ImageProxy image) {
- mainThreadExecutor().execute(this::issueNextRequest);
- }
+ ImagePipeline getImagePipeline();
@AutoValue
abstract static class CaptureError {
@@ -318,4 +115,18 @@
}
}
+ /**
+ * Interface for deferring creation of a {@link TakePictureManager}.
+ */
+ interface Provider {
+ /**
+ * Creates a new, initialized instance of a {@link TakePictureManager}.
+ *
+ * @param imageCaptureControl Used by TakePictureManager to control an
+ * {@link ImageCapture} instance.
+ * @return The {@code TakePictureManager} instance.
+ */
+ @NonNull
+ TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+ TakePictureRequest.RetryControl {
+
+ private static final String TAG = "TakePictureManagerImpl";
+
+ // Queue of new requests that have not been sent to the pipeline/camera.
+ @VisibleForTesting
+ final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+ final ImageCaptureControl mImageCaptureControl;
+ ImagePipeline mImagePipeline;
+
+ // The current request being processed by the camera. Only one request can be processed by
+ // the camera at the same time. Null if the camera is idle.
+ @Nullable
+ private RequestWithCallback mCapturingRequest;
+ // The current requests that have not received a result or an error.
+ private final List<RequestWithCallback> mIncompleteRequests;
+
+ // Once paused, the class waits until the class is resumed to handle new requests.
+ boolean mPaused = false;
+
+ /**
+ * @param imageCaptureControl for controlling {@link ImageCapture}
+ */
+ @MainThread
+ public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+ checkMainThread();
+ mImageCaptureControl = imageCaptureControl;
+ mIncompleteRequests = new ArrayList<>();
+ }
+
+ /**
+ * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+ * output.
+ */
+ @MainThread
+ @Override
+ public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+ checkMainThread();
+ mImagePipeline = imagePipeline;
+ mImagePipeline.setOnImageCloseListener(this);
+ }
+
+ /**
+ * Adds requests to the queue.
+ *
+ * <p>The requests in the queue will be executed based on the order being added.
+ */
+ @MainThread
+ @Override
+ public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+ checkMainThread();
+ mNewRequests.offer(takePictureRequest);
+ issueNextRequest();
+ }
+
+ @MainThread
+ @Override
+ public void retryRequest(@NonNull TakePictureRequest request) {
+ checkMainThread();
+ Logger.d(TAG, "Add a new request for retrying.");
+ // Insert the request to the front of the queue.
+ mNewRequests.addFirst(request);
+ // Try to issue the newly added request in case condition allows.
+ issueNextRequest();
+ }
+
+ /**
+ * Pauses sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void pause() {
+ checkMainThread();
+ mPaused = true;
+
+ // Always retry because the camera may not send an error callback during the reset.
+ if (mCapturingRequest != null) {
+ mCapturingRequest.abortSilentlyAndRetry();
+ }
+ }
+
+ /**
+ * Resumes sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void resume() {
+ checkMainThread();
+ mPaused = false;
+ issueNextRequest();
+ }
+
+ /**
+ * Clears the requests queue.
+ */
+ @MainThread
+ @Override
+ public void abortRequests() {
+ checkMainThread();
+ ImageCaptureException exception =
+ new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+ // Clear pending request first so aborting in-flight request won't trigger another capture.
+ for (TakePictureRequest request : mNewRequests) {
+ request.onError(exception);
+ }
+ mNewRequests.clear();
+
+ // Abort the in-flight request after clearing the pending requests.
+ // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+ List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+ for (RequestWithCallback request : requestsSnapshot) {
+ // TODO: optimize the performance by not processing aborted requests.
+ request.abortAndSendErrorToApp(exception);
+ }
+ }
+
+ /**
+ * Issues the next request if conditions allow.
+ */
+ @MainThread
+ void issueNextRequest() {
+ checkMainThread();
+ Log.d(TAG, "Issue the next TakePictureRequest.");
+ if (hasCapturingRequest()) {
+ Log.d(TAG, "There is already a request in-flight.");
+ return;
+ }
+ if (mPaused) {
+ Log.d(TAG, "The class is paused.");
+ return;
+ }
+ if (mImagePipeline.getCapacity() == 0) {
+ Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+ return;
+ }
+ TakePictureRequest request = mNewRequests.poll();
+ if (request == null) {
+ Log.d(TAG, "No new request.");
+ return;
+ }
+
+ RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+ trackCurrentRequests(requestWithCallback);
+
+ // Send requests.
+ Pair<CameraRequest, ProcessingRequest> requests =
+ mImagePipeline.createRequests(request, requestWithCallback,
+ requestWithCallback.getCaptureFuture());
+ CameraRequest cameraRequest = requireNonNull(requests.first);
+ ProcessingRequest processingRequest = requireNonNull(requests.second);
+ mImagePipeline.submitProcessingRequest(processingRequest);
+ ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+ requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+ }
+
+ /**
+ * Waits for the request to finish before issuing the next.
+ */
+ private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+ checkState(!hasCapturingRequest());
+ mCapturingRequest = requestWithCallback;
+
+ // Waits for the capture to finish before issuing the next.
+ mCapturingRequest.getCaptureFuture().addListener(() -> {
+ mCapturingRequest = null;
+ issueNextRequest();
+ }, directExecutor());
+
+ // Track all incomplete requests so we can abort them when UseCase is detached.
+ mIncompleteRequests.add(requestWithCallback);
+ requestWithCallback.getCompleteFuture().addListener(() -> {
+ mIncompleteRequests.remove(requestWithCallback);
+ }, directExecutor());
+ }
+
+ /**
+ * Submit a request to camera and post-processing pipeline.
+ *
+ * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+ */
+ @MainThread
+ private ListenableFuture<Void> submitCameraRequest(
+ @NonNull CameraRequest cameraRequest) {
+ checkMainThread();
+ mImageCaptureControl.lockFlashMode();
+ ListenableFuture<Void> captureRequestFuture =
+ mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+ Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ mImageCaptureControl.unlockFlashMode();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (cameraRequest.isAborted()) {
+ // When the pipeline is recreated, the in-flight request is aborted and
+ // retried. On legacy devices, the camera may return CancellationException
+ // for the aborted request which causes the retried request to fail. Return
+ // early if the request has been aborted.
+ return;
+ } else {
+ int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+ if (throwable instanceof ImageCaptureException) {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, (ImageCaptureException) throwable));
+ } else {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, new ImageCaptureException(
+ ERROR_CAPTURE_FAILED,
+ "Failed to submit capture request",
+ throwable)));
+ }
+ }
+ mImageCaptureControl.unlockFlashMode();
+ }
+ }, mainThreadExecutor());
+ return captureRequestFuture;
+ }
+
+ @VisibleForTesting
+ @Override
+ public boolean hasCapturingRequest() {
+ return mCapturingRequest != null;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ @Override
+ public RequestWithCallback getCapturingRequest() {
+ return mCapturingRequest;
+ }
+
+ @NonNull
+ @VisibleForTesting
+ @Override
+ public List<RequestWithCallback> getIncompleteRequests() {
+ return mIncompleteRequests;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ @Override
+ public ImagePipeline getImagePipeline() {
+ return mImagePipeline;
+ }
+
+ @Override
+ public void onImageClose(@NonNull ImageProxy image) {
+ mainThreadExecutor().execute(this::issueNextRequest);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 347a25c..9d6f159 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -21,10 +21,16 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.internal.TargetConfig;
+import java.util.Objects;
+
/**
* Configuration containing options for use cases.
*
@@ -108,6 +114,10 @@
Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
+ Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+ Option.create("camerax.core.useCase.takePictureManagerProvider",
+ TakePictureManager.Provider.class);
+
// *********************************************************************************************
/**
@@ -329,6 +339,22 @@
}
/**
+ * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+ */
+ @NonNull
+ default TakePictureManager.Provider getTakePictureManagerProvider() {
+ return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ return new TakePictureManagerImpl(imageCaptureControl);
+ }
+ }));
+ }
+
+ /**
* Builder for a {@link UseCase}.
*
* @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
index 0c062c8..c756a27 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -124,6 +124,7 @@
import androidx.exifinterface.media.ExifInterface;
import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
@@ -286,6 +287,8 @@
TAG_F_NUMBER, TAG_EXPOSURE_TIME, TAG_GPS_TIMESTAMP));
private static final int MM_IN_MICRONS = 1000;
+ private static final String COMPONENTS_CONFIGURATION_YCBCR = new String(new byte[]{1, 2, 3, 0},
+ StandardCharsets.UTF_8);
private final List<Map<String, ExifAttribute>> mAttributes;
private final ByteOrder mByteOrder;
@@ -876,7 +879,8 @@
String.valueOf(EXPOSURE_PROGRAM_NOT_DEFINED), attributes);
setAttributeIfMissing(TAG_EXIF_VERSION, "0230", attributes);
// Default is for YCbCr components
- setAttributeIfMissing(TAG_COMPONENTS_CONFIGURATION, "1,2,3,0", attributes);
+ setAttributeIfMissing(TAG_COMPONENTS_CONFIGURATION, COMPONENTS_CONFIGURATION_YCBCR,
+ attributes);
setAttributeIfMissing(TAG_METERING_MODE, String.valueOf(METERING_MODE_UNKNOWN),
attributes);
setAttributeIfMissing(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_UNKNOWN),
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 dc922ee..498abdf 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
@@ -23,6 +23,7 @@
import static androidx.camera.core.DynamicRange.ENCODING_SDR;
import static androidx.camera.core.DynamicRange.ENCODING_UNSPECIFIED;
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR;
+import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_OUTPUT_FORMAT;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
@@ -1041,14 +1042,20 @@
throw new IllegalArgumentException("Extensions are not supported for use with "
+ "Ultra HDR image capture.");
}
+
+ if (hasRawImageCapture(useCases)) {
+ throw new IllegalArgumentException("Extensions are not supported for use with "
+ + "Raw image capture.");
+ }
}
// TODO(b/322311893): throw exception to block feature combination of effect with Ultra
// HDR, until ImageProcessor and SurfaceProcessor can support JPEG/R format.
synchronized (mLock) {
- if (!mEffects.isEmpty() && hasUltraHdrImageCapture(useCases)) {
- throw new IllegalArgumentException("Ultra HDR image capture does not support for "
- + "use with CameraEffect.");
+ if (!mEffects.isEmpty() && (hasUltraHdrImageCapture(useCases)
+ || hasRawImageCapture(useCases))) {
+ throw new IllegalArgumentException("Ultra HDR image and Raw capture does not "
+ + "support for use with CameraEffect.");
}
}
}
@@ -1088,6 +1095,23 @@
return false;
}
+ private static boolean hasRawImageCapture(@NonNull Collection<UseCase> useCases) {
+ for (UseCase useCase : useCases) {
+ if (!isImageCapture(useCase)) {
+ continue;
+ }
+
+ UseCaseConfig<?> config = useCase.getCurrentConfig();
+ if (config.containsOption(OPTION_OUTPUT_FORMAT)
+ && (checkNotNull(config.retrieveOption(OPTION_OUTPUT_FORMAT))
+ == OUTPUT_FORMAT_RAW)) {
+ return true;
+ }
+
+ }
+ return false;
+ }
+
/**
* An identifier for a {@link CameraUseCaseAdapter}.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 86948ad..44fa074 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -67,6 +67,10 @@
ImageCaptureFailedForSpecificCombinationQuirk.load())) {
quirks.add(new ImageCaptureFailedForSpecificCombinationQuirk());
}
+ if (quirkSettings.shouldEnableQuirk(PreviewGreenTintQuirk.class,
+ PreviewGreenTintQuirk.load())) {
+ quirks.add(PreviewGreenTintQuirk.INSTANCE);
+ }
return quirks;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncorrectJpegMetadataQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncorrectJpegMetadataQuirk.java
index 98e547c..217fca5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncorrectJpegMetadataQuirk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/IncorrectJpegMetadataQuirk.java
@@ -30,16 +30,19 @@
/**
* <p>QuirkSummary
- * Bug Id: 309005680
+ * Bug Id: 309005680, 356428987
* Description: Quirk required to check whether the captured JPEG image has incorrect metadata.
* For example, Samsung A24 device has the problem and result in the captured
- * image can't be parsed and saved successfully.
- * Device(s): Samsung Galaxy A24 device.
+ * image can't be parsed and saved successfully. Samsung S10e and S10+ devices are
+ * also reported to have the similar issue.
+ * Device(s): Samsung Galaxy A24, S10e, S10+ device.
*/
public final class IncorrectJpegMetadataQuirk implements Quirk {
private static final Set<String> SAMSUNG_DEVICES = new HashSet<>(Arrays.asList(
- "A24" // Samsung Galaxy A24 series devices
+ "A24", // Samsung Galaxy A24 series devices
+ "BEYOND0", // Samsung Galaxy S10e series devices
+ "BEYOND2" // Samsung Galaxy S10+ series devices
));
static boolean load() {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewGreenTintQuirk.kt b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewGreenTintQuirk.kt
new file mode 100644
index 0000000..2a4a61d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewGreenTintQuirk.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory
+
+/**
+ * QuirkSummary
+ * - Bug Id: 361488335
+ * - Description: Quirk indicates the preview contains green tint.
+ * - Device(s): Motorola E20
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+public object PreviewGreenTintQuirk : Quirk {
+
+ private val isMotoE20
+ get() =
+ "motorola".equals(Build.BRAND, ignoreCase = true) &&
+ "moto e20".equals(Build.MODEL, ignoreCase = true)
+
+ @JvmStatic public fun load(): Boolean = isMotoE20
+
+ /** Returns whether stream sharing should be forced enabled. */
+ @JvmStatic
+ public fun shouldForceEnableStreamSharing(
+ cameraId: String,
+ appUseCases: Collection<UseCase>
+ ): Boolean {
+ if (isMotoE20) {
+ return shouldForceEnableStreamSharingForMotoE20(cameraId, appUseCases)
+ }
+ return false
+ }
+
+ private fun shouldForceEnableStreamSharingForMotoE20(
+ cameraId: String,
+ appUseCases: Collection<UseCase>
+ ): Boolean {
+ if (cameraId != "0" || appUseCases.size != 2) return false
+
+ val hasPreview = appUseCases.any { it is Preview }
+ val hasVideoCapture =
+ appUseCases.any {
+ it.currentConfig.containsOption(UseCaseConfig.OPTION_CAPTURE_TYPE) &&
+ it.currentConfig.captureType == UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE
+ }
+ return hasPreview && hasVideoCapture
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java
index 21209cb..0b6eb62 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnabler.java
@@ -21,6 +21,7 @@
import androidx.camera.core.UseCase;
import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
import androidx.camera.core.internal.compat.quirk.ImageCaptureFailedForSpecificCombinationQuirk;
+import androidx.camera.core.internal.compat.quirk.PreviewGreenTintQuirk;
import java.util.Collection;
@@ -28,12 +29,17 @@
* Workaround to check whether stream sharing should be forced enabled.
*
* @see ImageCaptureFailedForSpecificCombinationQuirk
+ * @see PreviewGreenTintQuirk
*/
public class StreamSharingForceEnabler {
@Nullable
private final ImageCaptureFailedForSpecificCombinationQuirk mSpecificCombinationQuirk =
DeviceQuirks.get(ImageCaptureFailedForSpecificCombinationQuirk.class);
+ @Nullable
+ private final PreviewGreenTintQuirk mPreviewGreenTintQuirk =
+ DeviceQuirks.get(PreviewGreenTintQuirk.class);
+
/**
* Returns whether stream sharing should be forced enabled.
*/
@@ -42,6 +48,9 @@
if (mSpecificCombinationQuirk != null) {
return mSpecificCombinationQuirk.shouldForceEnableStreamSharing(cameraId, appUseCases);
}
+ if (mPreviewGreenTintQuirk != null) {
+ return mPreviewGreenTintQuirk.shouldForceEnableStreamSharing(cameraId, appUseCases);
+ }
return false;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index 88dea73..c5c01ec 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -340,6 +340,11 @@
return imageFormat == ImageFormat.JPEG || imageFormat == ImageFormat.JPEG_R;
}
+ /** True if the given image format is RAW_SENSOR. */
+ public static boolean isRawFormats(int imageFormat) {
+ return imageFormat == ImageFormat.RAW_SENSOR;
+ }
+
/** True if the given aspect ratio is meaningful and has effect on the given size. */
public static boolean isAspectRatioValid(@NonNull Size sourceSize,
@Nullable Rational aspectRatio) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..af73043 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -27,10 +27,12 @@
import androidx.camera.testing.impl.fakes.FakeImageProxy
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
+import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runTest
@@ -101,6 +103,23 @@
}
@Test
+ fun takePicture_inMemory_imageProxyIsNotDeliveredClosed(): Unit = runTest {
+ // Arrange
+ val imageProxy = FakeImageProxy(FakeImageInfo())
+
+ // Arrange & Act.
+ val takePictureAsync = MainScope().async { imageCapture.takePicture() }
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
+ imageCaptureCallback?.onCaptureSuccess(imageProxy)
+
+ // Assert.
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ assertThat(takePictureAsync.await()).isEqualTo(imageProxy)
+ assertThat(imageProxy.isClosed).isFalse()
+ }
+
+ @Test
fun takePicture_inMemory_canCancel(): Unit = runTest {
// Arrange & Act.
val takePictureAsync = MainScope().async { imageCapture.takePicture() }
@@ -110,6 +129,24 @@
}
@Test
+ fun takePicture_inMemory_cancelClosesUndeliveredImage(): Unit = runTest {
+ // Arrange
+ val imageProxy = FakeImageProxy(FakeImageInfo())
+
+ // Arrange & Act.
+ val takePictureAsync = MainScope().async { imageCapture.takePicture() }
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
+ takePictureAsync.cancel()
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
+ imageCaptureCallback?.onCaptureSuccess(imageProxy)
+
+ // Assert.
+ assertThrows<CancellationException> { takePictureAsync.await() }
+ assertThat(imageProxy.isClosed).isTrue()
+ }
+
+ @Test
fun takePicture_inMemory_canPropagateCaptureStarted(): Unit = runTest {
// Arrange.
var callbackCalled = false
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/DngImage2DiskTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/DngImage2DiskTest.kt
new file mode 100644
index 0000000..ff91a2c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/DngImage2DiskTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture
+
+import android.graphics.ImageFormat
+import android.hardware.camera2.DngCreator
+import android.media.ExifInterface
+import android.media.Image
+import android.os.Build
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.imagecapture.FileUtil.moveFileToTarget
+import androidx.camera.core.imagecapture.Utils.ROTATION_DEGREES
+import androidx.camera.core.imagecapture.Utils.TEMP_FILE
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.io.OutputStream
+import java.util.UUID
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/** Unit tests for [DngImage2Disk] */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DngImage2DiskTest {
+
+ private val dngCreator = mock(DngCreator::class.java)
+ private val operation = DngImage2Disk(dngCreator)
+
+ @Test
+ fun copyToDestination_tempFileDeleted() {
+ // Arrange: create a file with a string.
+ val fileContent = "fileContent"
+ TEMP_FILE.writeText(fileContent, Charsets.UTF_8)
+ val destination =
+ File.createTempFile("unit_test_" + UUID.randomUUID().toString(), ".temp").also {
+ it.deleteOnExit()
+ }
+ // Act: move the file to the destination.
+ moveFileToTarget(TEMP_FILE, OutputFileOptions.Builder(destination).build())
+ // Assert: the temp file is deleted and the destination file has the same content.
+ assertThat(File(TEMP_FILE.absolutePath).exists()).isFalse()
+ assertThat(File(destination.absolutePath).readText(Charsets.UTF_8)).isEqualTo(fileContent)
+ }
+
+ @Test
+ fun writeImageToFile_dngCreatorCalled() {
+ val options = OutputFileOptions.Builder(TEMP_FILE).build()
+ val imageProxy = FakeImageProxy(FakeImageInfo())
+ imageProxy.format = ImageFormat.RAW_SENSOR
+ imageProxy.image = mock(Image::class.java)
+ val input = DngImage2Disk.In.of(imageProxy, ROTATION_DEGREES, options)
+
+ val result = operation.apply(input)
+ assertThat(result.savedUri).isNotNull()
+ assertThat(result.savedUri?.path).isEqualTo(TEMP_FILE.absolutePath)
+ verify(dngCreator).setOrientation(ExifInterface.ORIENTATION_ROTATE_180)
+ verify(dngCreator).writeImage(any(OutputStream::class.java), eq(imageProxy.image!!))
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
index 637dee6..54de138 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
@@ -16,6 +16,7 @@
package androidx.camera.core.imagecapture
+import android.hardware.camera2.CameraCharacteristics
import android.util.Size
import androidx.annotation.MainThread
import androidx.camera.core.ImageCaptureException
@@ -25,10 +26,14 @@
import androidx.camera.core.impl.ImageCaptureConfig
import androidx.core.util.Pair
import com.google.common.util.concurrent.ListenableFuture
+import org.mockito.Mockito.mock
/** Fake [ImagePipeline] class for testing. */
-class FakeImagePipeline(config: ImageCaptureConfig, cameraSurfaceSize: Size) :
- ImagePipeline(config, cameraSurfaceSize) {
+class FakeImagePipeline(
+ config: ImageCaptureConfig,
+ cameraSurfaceSize: Size,
+ cameraCharacteristics: CameraCharacteristics
+) : ImagePipeline(config, cameraSurfaceSize, cameraCharacteristics) {
private var currentProcessingRequest: ProcessingRequest? = null
private var receivedProcessingRequest: MutableSet<ProcessingRequest> = mutableSetOf()
@@ -43,7 +48,12 @@
var sNextRequestId = 0
}
- constructor() : this(createEmptyImageCaptureConfig(), Size(640, 480))
+ constructor() :
+ this(
+ createEmptyImageCaptureConfig(),
+ Size(640, 480),
+ mock(CameraCharacteristics::class.java)
+ )
@MainThread
internal override fun createRequests(
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index bcfa85b..8ede6e5 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -18,6 +18,7 @@
import android.graphics.ImageFormat
import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.os.Build
import android.os.Looper.getMainLooper
@@ -59,6 +60,7 @@
import androidx.camera.testing.impl.TestImageUtil.createJpegFakeImageProxy
import androidx.camera.testing.impl.TestImageUtil.createJpegrBytes
import androidx.camera.testing.impl.TestImageUtil.createJpegrFakeImageProxy
+import androidx.camera.testing.impl.TestImageUtil.createRawFakeImageProxy
import androidx.camera.testing.impl.TestImageUtil.createYuvFakeImageProxy
import androidx.camera.testing.impl.fakes.FakeImageInfo
import androidx.camera.testing.impl.fakes.FakeImageReaderProxy
@@ -69,6 +71,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@@ -90,11 +93,13 @@
private lateinit var imagePipeline: ImagePipeline
private lateinit var imageCaptureConfig: ImageCaptureConfig
+ private lateinit var cameraCharacteristics: CameraCharacteristics
@Before
fun setUp() {
imageCaptureConfig = createImageCaptureConfig()
- imagePipeline = ImagePipeline(imageCaptureConfig, SIZE)
+ cameraCharacteristics = mock(CameraCharacteristics::class.java)
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
}
@After
@@ -114,7 +119,7 @@
.setCaptureOptionUnpacker { _, builder -> builder.templateType = TEMPLATE_TYPE }
builder.mutableConfig.insertOption(ImageInputConfig.OPTION_INPUT_FORMAT, ImageFormat.JPEG)
// Act.
- val pipeline = ImagePipeline(builder.useCaseConfig, SIZE)
+ val pipeline = ImagePipeline(builder.useCaseConfig, SIZE, cameraCharacteristics)
// Assert.
assertThat(pipeline.captureNode.inputEdge.imageReaderProxyProvider)
.isEqualTo(imageReaderProxyProvider)
@@ -133,6 +138,7 @@
ImagePipeline(
imageCaptureConfig,
SIZE,
+ cameraCharacteristics,
/*cameraEffect=*/ null,
/*isVirtualCamera=*/ true
)
@@ -149,7 +155,13 @@
@Test
fun createPipelineWithEffect_processingNodeContainsEffect() {
assertThat(
- ImagePipeline(imageCaptureConfig, SIZE, GrayscaleImageEffect(), false)
+ ImagePipeline(
+ imageCaptureConfig,
+ SIZE,
+ cameraCharacteristics,
+ GrayscaleImageEffect(),
+ false
+ )
.processingNode
.mImageProcessor
)
@@ -174,7 +186,22 @@
fun createRequests_verifyCameraRequest_whenFormatIsJpegr() {
// Arrange.
imageCaptureConfig = createImageCaptureConfig(inputFormat = ImageFormat.JPEG_R)
- imagePipeline = ImagePipeline(imageCaptureConfig, SIZE)
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
+ val captureInput = imagePipeline.captureNode.inputEdge
+
+ // Act: create requests
+ val result =
+ imagePipeline.createRequests(IN_MEMORY_REQUEST, CALLBACK, Futures.immediateFuture(null))
+
+ // Assert: CameraRequest is constructed correctly.
+ verifyCaptureRequest(captureInput, result)
+ }
+
+ @Test
+ fun createRequests_verifyCameraRequest_whenFormatIsRAw() {
+ // Arrange.
+ imageCaptureConfig = createImageCaptureConfig(inputFormat = ImageFormat.RAW_SENSOR)
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
val captureInput = imagePipeline.captureNode.inputEdge
// Act: create requests
@@ -245,6 +272,7 @@
ImagePipeline(
imageCaptureConfig,
SIZE,
+ cameraCharacteristics,
null,
false,
postviewSize,
@@ -267,7 +295,15 @@
// Arrange.
val postviewSize = Size(640, 480)
imagePipeline =
- ImagePipeline(imageCaptureConfig, SIZE, null, false, postviewSize, ImageFormat.JPEG)
+ ImagePipeline(
+ imageCaptureConfig,
+ SIZE,
+ cameraCharacteristics,
+ null,
+ false,
+ postviewSize,
+ ImageFormat.JPEG
+ )
// Act: create SessionConfig
val sessionConfig = imagePipeline.createSessionConfigBuilder(SIZE).build()
@@ -288,6 +324,7 @@
ImagePipeline(
imageCaptureConfig,
SIZE,
+ cameraCharacteristics,
null,
false,
postviewSize,
@@ -356,7 +393,7 @@
builder.mutableConfig.insertOption(OPTION_BUFFER_FORMAT, ImageFormat.YUV_420_888)
builder.mutableConfig.insertOption(OPTION_IO_EXECUTOR, mainThreadExecutor())
builder.mutableConfig.insertOption(ImageInputConfig.OPTION_INPUT_FORMAT, ImageFormat.JPEG)
- val pipeline = ImagePipeline(builder.useCaseConfig, SIZE)
+ val pipeline = ImagePipeline(builder.useCaseConfig, SIZE, cameraCharacteristics)
// Arrange & act.
sendInMemoryRequest(pipeline, ImageFormat.YUV_420_888)
@@ -380,7 +417,7 @@
fun sendInMemoryRequest_receivesImageProxy_whenFormatIsJpegr() {
// Arrange & act.
imageCaptureConfig = createImageCaptureConfig(inputFormat = ImageFormat.JPEG_R)
- imagePipeline = ImagePipeline(imageCaptureConfig, SIZE)
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
val image = sendInMemoryRequest(imagePipeline, ImageFormat.JPEG_R)
// Assert: the image is received by TakePictureCallback.
@@ -389,6 +426,19 @@
assertThat(CALLBACK.inMemoryResult!!.planes).isEqualTo(image.planes)
}
+ @Test
+ fun sendInMemoryRequest_receivesImageProxy_whenFormatIsRaw() {
+ // Arrange & act.
+ imageCaptureConfig = createImageCaptureConfig(inputFormat = ImageFormat.RAW_SENSOR)
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE, cameraCharacteristics)
+ val image = sendInMemoryRequest(imagePipeline, ImageFormat.RAW_SENSOR)
+
+ // Assert: the image is received by TakePictureCallback.
+ assertThat(image.format).isEqualTo(ImageFormat.RAW_SENSOR)
+ assertThat(CALLBACK.inMemoryResult!!.format).isEqualTo(ImageFormat.RAW_SENSOR)
+ assertThat(CALLBACK.inMemoryResult!!.planes).isEqualTo(image.planes)
+ }
+
/** Creates a ImageProxy and sends it to the pipeline. */
private fun sendInMemoryRequest(
pipeline: ImagePipeline,
@@ -416,6 +466,9 @@
val jpegBytes = createJpegrBytes(WIDTH, HEIGHT)
createJpegrFakeImageProxy(imageInfo, jpegBytes)
}
+ ImageFormat.RAW_SENSOR -> {
+ createRawFakeImageProxy(imageInfo, WIDTH, HEIGHT)
+ }
else -> {
val jpegBytes = createJpegBytes(WIDTH, HEIGHT)
createJpegFakeImageProxy(imageInfo, jpegBytes)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/JpegBytes2DiskTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/JpegBytes2DiskTest.kt
index c479676..b4738db 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/JpegBytes2DiskTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/JpegBytes2DiskTest.kt
@@ -24,7 +24,7 @@
import android.util.Size
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OutputFileOptions
-import androidx.camera.core.imagecapture.JpegBytes2Disk.moveFileToTarget
+import androidx.camera.core.imagecapture.FileUtil.moveFileToTarget
import androidx.camera.core.imagecapture.Utils.ALTITUDE
import androidx.camera.core.imagecapture.Utils.CAMERA_CAPTURE_RESULT
import androidx.camera.core.imagecapture.Utils.EXIF_DESCRIPTION
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
index d6139bf..b6ab8c8 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
@@ -18,6 +18,7 @@
import android.graphics.ImageFormat
import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
import android.os.Build
import android.os.Looper.getMainLooper
import androidx.camera.core.ImageCaptureException
@@ -43,6 +44,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@@ -56,8 +58,10 @@
class ProcessingNodeTest {
private lateinit var processingNodeIn: ProcessingNode.In
+ private var cameraCharacteristics: CameraCharacteristics =
+ mock(CameraCharacteristics::class.java)
- private var node = ProcessingNode(mainThreadExecutor())
+ private var node = ProcessingNode(mainThreadExecutor(), cameraCharacteristics)
@Before
fun setUp() {
@@ -219,7 +223,12 @@
fun singleExecutorForLowMemoryQuirkEnabled() {
listOf("sm-a520w", "motog3").forEach { model ->
setStaticField(Build::class.java, "MODEL", model)
- assertThat(isSequentialExecutor(ProcessingNode(mainThreadExecutor()).mBlockingExecutor))
+ assertThat(
+ isSequentialExecutor(
+ ProcessingNode(mainThreadExecutor(), cameraCharacteristics)
+ .mBlockingExecutor
+ )
+ )
.isTrue()
}
}
@@ -230,7 +239,7 @@
setStaticField(Build::class.java, "DEVICE", "a24")
// Creates the ProcessingNode after updating the device name to load the correct quirks
- node = ProcessingNode(mainThreadExecutor())
+ node = ProcessingNode(mainThreadExecutor(), cameraCharacteristics)
processingNodeIn = ProcessingNode.In.of(ImageFormat.JPEG, ImageFormat.JPEG)
node.transform(processingNodeIn)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..62f161b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.core.imagecapture
+import android.hardware.camera2.CameraCharacteristics
import android.os.Build
import android.os.Looper.getMainLooper
import android.util.Size
@@ -35,6 +36,7 @@
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@@ -50,8 +52,9 @@
private val imagePipeline = FakeImagePipeline()
private val imageCaptureControl = FakeImageCaptureControl()
private val takePictureManager =
- TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+ TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
+ private val cameraCharacteristics = mock(CameraCharacteristics::class.java)
@After
fun tearDown() {
@@ -426,7 +429,11 @@
// Arrange.
// Uses the real ImagePipeline implementation to do the test
takePictureManager.mImagePipeline =
- ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+ ImagePipeline(
+ Utils.createEmptyImageCaptureConfig(),
+ Size(640, 480),
+ cameraCharacteristics
+ )
val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
@@ -448,7 +455,11 @@
// Arrange.
// Uses the real ImagePipeline implementation to do the test
takePictureManager.mImagePipeline =
- ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+ ImagePipeline(
+ Utils.createEmptyImageCaptureConfig(),
+ Size(640, 480),
+ cameraCharacteristics
+ )
val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
@@ -465,7 +476,11 @@
fun requestFailure_failureReportedIfQuirkDisabled() {
// Arrange: use the real ImagePipeline implementation to do the test
takePictureManager.mImagePipeline =
- ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+ ImagePipeline(
+ Utils.createEmptyImageCaptureConfig(),
+ Size(640, 480),
+ cameraCharacteristics
+ )
// Create a request and offer it to the manager.
imageCaptureControl.shouldUsePendingResult = true
@@ -500,7 +515,11 @@
// Use the real ImagePipeline implementation to do the test
takePictureManager.mImagePipeline =
- ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+ ImagePipeline(
+ Utils.createEmptyImageCaptureConfig(),
+ Size(640, 480),
+ cameraCharacteristics
+ )
// Create a request and offer it to the manager.
imageCaptureControl.shouldUsePendingResult = true
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 42652ae..d402847 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
@@ -18,6 +18,7 @@
import android.graphics.ImageFormat.JPEG
import android.graphics.ImageFormat.JPEG_R
+import android.graphics.ImageFormat.RAW_SENSOR
import android.graphics.Matrix
import android.graphics.Rect
import android.os.Build
@@ -332,6 +333,36 @@
adapter.addUseCases(setOf(imageCapture))
}
+ @RequiresApi(23)
+ @Test(expected = CameraException::class)
+ fun useRawWithExtensions_throwsException() {
+ // Arrange: enable extensions.
+ val extensionsConfig = createCoexistingRequiredRuleCameraConfig(FakeSessionProcessor())
+ val cameraId = "fakeCameraId"
+ val fakeManager = FakeCameraDeviceSurfaceManager()
+ fakeManager.setValidSurfaceCombos(
+ setOf(listOf(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, RAW_SENSOR))
+ )
+ val fakeCamera = FakeCamera(cameraId)
+ val adapter =
+ CameraUseCaseAdapter(
+ fakeCamera,
+ null,
+ RestrictedCameraInfo(fakeCamera.cameraInfoInternal, extensionsConfig),
+ null,
+ CompositionSettings.DEFAULT,
+ CompositionSettings.DEFAULT,
+ FakeCameraCoordinator(),
+ fakeManager,
+ FakeUseCaseConfigFactory(),
+ )
+
+ // Act: add ImageCapture that sets Ultra HDR.
+ val imageCapture =
+ ImageCapture.Builder().setOutputFormat(ImageCapture.OUTPUT_FORMAT_RAW).build()
+ adapter.addUseCases(setOf(imageCapture))
+ }
+
@RequiresApi(34) // Ultra HDR only supported on API 34+
@Test(expected = CameraException::class)
fun useUltraHdrWithCameraEffect_throwsException() {
@@ -359,6 +390,29 @@
}
@Test(expected = CameraException::class)
+ fun useRawWithCameraEffect_throwsException() {
+ // Arrange: add an image effect.
+ val cameraId = "fakeCameraId"
+ val fakeManager = FakeCameraDeviceSurfaceManager()
+ fakeManager.setValidSurfaceCombos(
+ setOf(listOf(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, RAW_SENSOR))
+ )
+ val adapter =
+ CameraUseCaseAdapter(
+ FakeCamera(cameraId),
+ FakeCameraCoordinator(),
+ fakeManager,
+ FakeUseCaseConfigFactory(),
+ )
+ adapter.setEffects(listOf(imageEffect))
+
+ // Act: add ImageCapture that sets Ultra HDR.
+ val imageCapture =
+ ImageCapture.Builder().setOutputFormat(ImageCapture.OUTPUT_FORMAT_RAW).build()
+ adapter.addUseCases(setOf(imageCapture))
+ }
+
+ @Test(expected = CameraException::class)
fun addStreamSharing_throwsException() {
val streamSharing =
StreamSharing(
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/JpegMetadataCorrectorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/JpegMetadataCorrectorTest.kt
index 9737c77..d1fb879 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/JpegMetadataCorrectorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/JpegMetadataCorrectorTest.kt
@@ -47,6 +47,20 @@
}
@Test
+ fun needCorrectJpegMetadataOnSamsungS10e() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "SAMSUNG")
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", "beyond0")
+ assertThat(JpegMetadataCorrector(DeviceQuirks.getAll()).needCorrectJpegMetadata()).isTrue()
+ }
+
+ @Test
+ fun needCorrectJpegMetadataOnSamsungS10Plus() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "SAMSUNG")
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", "beyond2")
+ assertThat(JpegMetadataCorrector(DeviceQuirks.getAll()).needCorrectJpegMetadata()).isTrue()
+ }
+
+ @Test
fun doesNotNeedCorrectJpegMetadataOnSamsungA23() {
ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "SAMSUNG")
ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", "a23")
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt
index e7e0ee7..8d9be0c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/StreamSharingForceEnablerTest.kt
@@ -81,6 +81,17 @@
)
)
add(arrayOf("", "", "1", PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE, false))
+ add(arrayOf("Motorola", "Moto E20", "0", PREVIEW or VIDEO_CAPTURE, true))
+ add(arrayOf("Motorola", "Moto E20", "1", PREVIEW or VIDEO_CAPTURE, false))
+ add(
+ arrayOf(
+ "Motorola",
+ "Moto E20",
+ "0",
+ PREVIEW or IMAGE_CAPTURE or VIDEO_CAPTURE,
+ false
+ )
+ )
}
}
diff --git a/camera/camera-effects/lint-baseline.xml b/camera/camera-effects/lint-baseline.xml
new file mode 100644
index 0000000..348f450
--- /dev/null
+++ b/camera/camera-effects/lint-baseline.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" val inputSurface = surfaceRequest.deferrableSurface.surface.get()"
+ errorLine2=" ~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" val inputSurface = surfaceRequest.deferrableSurface.surface.get()"
+ errorLine2=" ~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 26 (current min is 21): `java.util.regex.Matcher#start` (called from `kotlin.text.MatchGroupCollection#get(String)`)"
+ errorLine1=" val inputSurface = surfaceRequest.deferrableSurface.surface.get()"
+ errorLine2=" ~~~">
+ <location
+ file="src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt"/>
+ </issue>
+
+</issues>
diff --git a/camera/camera-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 034920a..c7e4120 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -53,10 +53,10 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation(libs.kotlinStdlib)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":camera:camera-camera2"))
diff --git a/camera/camera-extensions/lint-baseline.xml b/camera/camera-extensions/lint-baseline.xml
index b529f11..5a613f6 100644
--- a/camera/camera-extensions/lint-baseline.xml
+++ b/camera/camera-extensions/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.5.0-alpha06" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.0-alpha06)" variant="all" version="8.5.0-alpha06">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="BanThreadSleep"
@@ -10,4 +10,31 @@
file="src/main/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheck.java"/>
</issue>
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CaptureOutputSurfaceOccupiedQuirk implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/extensions/internal/compat/quirk/CaptureOutputSurfaceOccupiedQuirk.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class CrashWhenOnDisableTooSoon implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/extensions/internal/compat/quirk/CrashWhenOnDisableTooSoon.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class GetAvailableKeysNeedsOnInit implements Quirk {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/camera/extensions/internal/compat/quirk/GetAvailableKeysNeedsOnInit.java"/>
+ </issue>
+
</issues>
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 4ca154c..0f7bfdb 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -194,7 +194,11 @@
@Test
fun correctAvailability_whenExtensionIsNotAvailable() {
// Skips the test if extensions availability is disabled by quirk.
- assumeFalse(ExtensionsTestUtil.extensionsDisabledByQuirk())
+ assumeFalse(
+ ExtensionsTestUtil.extensionsDisabledByQuirk(
+ CameraUtil.getCameraIdWithLensFacing(lensFacing)!!
+ )
+ )
extensionsManager =
ExtensionsManager.getInstanceAsync(context, cameraProvider)[
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
index b64e407..82a6ac3 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
@@ -384,8 +384,8 @@
/**
* Returns whether extensions is disabled by quirk.
*/
- public static boolean extensionsDisabledByQuirk() {
- return new ExtensionDisabledValidator().shouldDisableExtension();
+ public static boolean extensionsDisabledByQuirk(@NonNull String cameraId) {
+ return new ExtensionDisabledValidator().shouldDisableExtension(cameraId);
}
/**
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
index c651f3e..9e16f66 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
@@ -109,7 +109,7 @@
public boolean isExtensionAvailable(@NonNull String cameraId,
@NonNull Map<String, CameraCharacteristics> characteristicsMap) {
- if (mExtensionDisabledValidator.shouldDisableExtension()) {
+ if (mExtensionDisabledValidator.shouldDisableExtension(cameraId)) {
return false;
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index 5f908f9..08b17bc 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -135,7 +135,7 @@
public boolean isExtensionAvailable(@NonNull String cameraId,
@NonNull Map<String, CameraCharacteristics> characteristicsMap) {
- if (mExtensionDisabledValidator.shouldDisableExtension()) {
+ if (mExtensionDisabledValidator.shouldDisableExtension(cameraId)) {
return false;
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
index d934a6f..25eb47a 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
@@ -18,6 +18,7 @@
import android.os.Build;
+import androidx.annotation.NonNull;
import androidx.camera.core.impl.Quirk;
import androidx.camera.extensions.internal.ExtensionVersion;
import androidx.camera.extensions.internal.Version;
@@ -25,7 +26,7 @@
/**
* <p>QuirkSummary
- * Bug Id: b/199408131, b/214130117, b/255956506
+ * Bug Id: b/199408131, b/214130117, b/255956506, b/364152642
* Description: Quirk required to disable extension for some devices. An example is that
* Pixel 5's availability check result of the basic extension interface should
* be false, but it actually returns true. Therefore, force disable Basic
@@ -33,20 +34,20 @@
* minimum quality requirements for camera extensions support. Common issues encountered with
* Motorola extensions include: Bokeh not supported on some devices, SurfaceView not supported,
* Image doesn't appear after taking a picture, Preview is pauses after resuming.
- * Device(s): Pixel 5, Motorola
+ * Device(s): Pixel 5, Motorola, Samsung A52s 5G
*
* @see androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator
*/
public class ExtensionDisabledQuirk implements Quirk {
static boolean load() {
- return isPixel5() || isMoto() || isRealme();
+ return isPixel5() || isMoto() || isRealme() || isSamsungA52s5g();
}
/**
* Checks whether extension should be disabled.
*/
- public boolean shouldDisableExtension() {
+ public boolean shouldDisableExtension(@NonNull String cameraId) {
if (isPixel5() && !isAdvancedExtenderSupported()) {
// 1. Disables Pixel 5's Basic Extender capability.
return true;
@@ -58,6 +59,8 @@
// implementation only set the specific effect mode and have one critical bug that the
// the output image's timestamp doesn't match the timestamp in onCaptureStarted.
return true;
+ } else if (isSamsungA52s5g()) {
+ return shouldDisableForSamsungA52s5g(cameraId);
}
return false;
@@ -75,6 +78,14 @@
return "realme".equalsIgnoreCase(Build.BRAND);
}
+ private static boolean isSamsungA52s5g() {
+ return "samsung".equalsIgnoreCase(Build.BRAND) && "a52sxq".equalsIgnoreCase(Build.DEVICE);
+ }
+
+ private static boolean shouldDisableForSamsungA52s5g(@NonNull String cameraId) {
+ return cameraId.equals("0");
+ }
+
private static boolean isAdvancedExtenderSupported() {
return ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_2)
&& ExtensionVersion.isAdvancedExtenderSupported();
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
index eb6b08f..7ff1825 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.internal.compat.workaround;
+import androidx.annotation.NonNull;
import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
import androidx.camera.extensions.internal.compat.quirk.ExtensionDisabledQuirk;
@@ -36,7 +37,7 @@
/**
* Checks whether extension should be disabled.
*/
- public boolean shouldDisableExtension() {
- return mQuirk != null && mQuirk.shouldDisableExtension();
+ public boolean shouldDisableExtension(@NonNull String cameraId) {
+ return mQuirk != null && mQuirk.shouldDisableExtension(cameraId);
}
}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
index d34b6c6..0d82f78 100644
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
@@ -55,7 +55,8 @@
ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
val validator = ExtensionDisabledValidator()
- assertThat(validator.shouldDisableExtension()).isEqualTo(config.shouldDisableExtension)
+ assertThat(validator.shouldDisableExtension(config.cameraId))
+ .isEqualTo(config.shouldDisableExtension)
}
class TestConfig(
@@ -63,33 +64,41 @@
val device: String,
val version: String,
val isAdvancedInterface: Boolean,
+ val cameraId: String,
val shouldDisableExtension: Boolean
)
companion object {
+ private const val DEFAULT_BACK_CAMERA_ID = "0"
+ private const val DEFAULT_FRONT_CAMERA_ID = "1"
+
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun createTestSet(): List<TestConfig> {
return listOf(
// Pixel 5 extension capability is disabled on basic extender
- TestConfig("Google", "Redfin", "1.2.0", false, true),
+ TestConfig("Google", "Redfin", "1.2.0", false, DEFAULT_BACK_CAMERA_ID, true),
// Pixel 5 extension capability is enabled on advanced extender
- TestConfig("Google", "Redfin", "1.2.0", true, false),
+ TestConfig("Google", "Redfin", "1.2.0", true, DEFAULT_BACK_CAMERA_ID, false),
// All Motorola devices should be disabled for version 1.1.0 and older.
- TestConfig("Motorola", "Smith", "1.1.0", false, true),
- TestConfig("Motorola", "Hawaii P", "1.1.0", false, true),
+ TestConfig("Motorola", "Smith", "1.1.0", false, DEFAULT_BACK_CAMERA_ID, true),
+ TestConfig("Motorola", "Hawaii P", "1.1.0", false, DEFAULT_BACK_CAMERA_ID, true),
// Make sure Motorola device would still be enabled for newer versions
// Motorola doesn't support this today but making sure there is a path to enable
- TestConfig("Motorola", "Hawaii P", "1.2.0", false, false),
+ TestConfig("Motorola", "Hawaii P", "1.2.0", false, DEFAULT_BACK_CAMERA_ID, false),
+
+ // Samsung A52s 5G devices should be disabled for the back camera.
+ TestConfig("Samsung", "a52sxq", "1.2.0", true, DEFAULT_BACK_CAMERA_ID, true),
+ TestConfig("Samsung", "a52sxq", "1.2.0", true, DEFAULT_FRONT_CAMERA_ID, false),
// Other cases should be kept normal.
- TestConfig("", "", "1.2.0", false, false),
+ TestConfig("", "", "1.2.0", false, DEFAULT_BACK_CAMERA_ID, false),
// Advanced extender is enabled for all devices
- TestConfig("", "", "1.2.0", true, false),
+ TestConfig("", "", "1.2.0", true, DEFAULT_BACK_CAMERA_ID, false),
)
}
}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
index 667a60f..be34019 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
@@ -70,7 +70,7 @@
@GuardedBy("mLock") private var cameraXConfigProvider: CameraXConfig.Provider? = null
@GuardedBy("mLock") private var cameraXInitializeFuture: ListenableFuture<Void>? = null
@GuardedBy("mLock") private var cameraXShutdownFuture = Futures.immediateFuture<Void>(null)
- private val lifecycleCameraRepository = LifecycleCameraRepository()
+ private val lifecycleCameraRepository = LifecycleCameraRepository.getInstance()
private var cameraX: CameraX? = null
private var context: Context? = null
@GuardedBy("mLock")
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
index 244c443..9346ab1 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
@@ -19,6 +19,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraEffect;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
@@ -69,6 +70,10 @@
* LifecycleCamera will be released.
*/
final class LifecycleCameraRepository {
+ private static final Object INSTANCE_LOCK = new Object();
+ @GuardedBy("INSTANCE_LOCK")
+ private static LifecycleCameraRepository sInstance = null;
+
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -84,6 +89,22 @@
@GuardedBy("mLock")
@Nullable CameraCoordinator mCameraCoordinator;
+ @VisibleForTesting
+ LifecycleCameraRepository() {
+ // LifecycleCameraRepository is designed to be used as a singleton and the constructor
+ // should only be called for testing purpose.
+ }
+
+ @NonNull
+ static LifecycleCameraRepository getInstance() {
+ synchronized (INSTANCE_LOCK) {
+ if (sInstance == null) {
+ sInstance = new LifecycleCameraRepository();
+ }
+ return sInstance;
+ }
+ }
+
/**
* Create a new {@link LifecycleCamera} associated with the given {@link LifecycleOwner}.
*
diff --git a/camera/camera-media3-effect/api/current.txt b/camera/camera-media3-effect/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-media3-effect/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/biometric/biometric-ktx/api/res-current.txt b/camera/camera-media3-effect/api/res-current.txt
similarity index 100%
copy from biometric/biometric-ktx/api/res-current.txt
copy to camera/camera-media3-effect/api/res-current.txt
diff --git a/camera/camera-media3-effect/api/restricted_current.txt b/camera/camera-media3-effect/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-media3-effect/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-media3-effect/build.gradle b/camera/camera-media3-effect/build.gradle
new file mode 100644
index 0000000..4383949
--- /dev/null
+++ b/camera/camera-media3-effect/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+dependencies {
+ api(project(":camera:camera-core"))
+ implementation(libs.media3Common)
+ implementation(libs.media3Effect)
+
+ testImplementation(libs.kotlinCoroutinesAndroid)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+ testImplementation(libs.robolectric)
+
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.kotlinStdlib)
+ androidTestImplementation(libs.kotlinCoroutinesAndroid)
+}
+android {
+ testOptions.unitTests.includeAndroidResources = true
+ namespace "androidx.camera.media3.effect"
+}
+androidx {
+ name = "Camera Media3 Effect"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Media3 effect components for the Jetpack Camera Library, a library providing a" +
+ " seamless integration that enables media3 effect in CameraX."
+}
\ No newline at end of file
diff --git a/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
new file mode 100644
index 0000000..4840bb7
--- /dev/null
+++ b/camera/camera-media3-effect/src/androidTest/java/androidx/camera/media3/effect/Media3EffectDeviceTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.media3.effect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Instrumented tests for [Media3Effect]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class Media3EffectDeviceTest {
+
+ @Test
+ fun smokeTest() {
+ assertThat(true).isTrue()
+ }
+}
diff --git a/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
new file mode 100644
index 0000000..7ecd11a
--- /dev/null
+++ b/camera/camera-media3-effect/src/main/java/androidx/camera/media3/effect/Media3Effect.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.media3.effect
+
+import androidx.annotation.RestrictTo
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.SurfaceProcessor
+import androidx.core.util.Consumer
+import java.util.concurrent.Executor
+
+/** A CameraEffect that inserts media3 effect into CameraX pipeline */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Media3Effect(
+ targets: Int,
+ executor: Executor,
+ surfaceProcessor: SurfaceProcessor,
+ errorListener: Consumer<Throwable>
+) : CameraEffect(targets, executor, surfaceProcessor, errorListener) {}
diff --git a/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt b/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
new file mode 100644
index 0000000..0f3365e
--- /dev/null
+++ b/camera/camera-media3-effect/src/test/java/androidx/camera/media3/effect/Media3EffectTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.media3.effect
+
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/** Unit tests for [Media3Effect]. */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Media3EffectTest {
+
+ // TODO: replace this with a real test.
+ @Test
+ fun smokeTest() {
+ assertThat(true).isTrue()
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 92063b0..03b2840 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -50,6 +50,7 @@
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import androidx.test.core.app.ApplicationProvider;
import java.util.ArrayList;
import java.util.Arrays;
@@ -118,34 +119,36 @@
}
public FakeCameraInfoInternal(@NonNull String cameraId) {
- this(cameraId, 0, CameraSelector.LENS_FACING_BACK, null);
+ this(cameraId, 0, CameraSelector.LENS_FACING_BACK,
+ ApplicationProvider.getApplicationContext());
}
public FakeCameraInfoInternal(@NonNull String cameraId,
@CameraSelector.LensFacing int lensFacing) {
- this(cameraId, 0, lensFacing, null);
+ this(cameraId, 0, lensFacing,
+ ApplicationProvider.getApplicationContext());
}
public FakeCameraInfoInternal(int sensorRotation, @CameraSelector.LensFacing int lensFacing) {
- this("0", sensorRotation, lensFacing, null);
+ this("0", sensorRotation, lensFacing,
+ ApplicationProvider.getApplicationContext());
}
public FakeCameraInfoInternal(@NonNull String cameraId, int sensorRotation,
@CameraSelector.LensFacing int lensFacing) {
- this(cameraId, sensorRotation, lensFacing, null);
+ this(cameraId, sensorRotation, lensFacing,
+ ApplicationProvider.getApplicationContext());
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public FakeCameraInfoInternal(@NonNull String cameraId, int sensorRotation,
@CameraSelector.LensFacing int lensFacing,
- @Nullable Context context) {
+ @NonNull Context context) {
mCameraId = cameraId;
mSensorRotation = sensorRotation;
mLensFacing = lensFacing;
mZoomLiveData = new MutableLiveData<>(ImmutableZoomState.create(1.0f, 4.0f, 1.0f, 0.0f));
- if (context != null) {
- mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
- }
+ mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
}
/**
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/ImageProxyUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/ImageProxyUtil.java
index 00869ea4..db3c92a 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/ImageProxyUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/ImageProxyUtil.java
@@ -69,6 +69,27 @@
return planes;
}
+ /**
+ * Creates {@link android.graphics.ImageFormat.RAW_SENSOR} image planes.
+ *
+ * @param width image width.
+ * @param height image height.
+ * @param incrementValue true if the data value will increment by position, e.g. 1, 2, 3, etc,.
+ * @return image planes in image proxy.
+ */
+ @NonNull
+ public static ImageProxy.PlaneProxy[] createRawImagePlanes(
+ final int width,
+ final int height,
+ final int pixelStride,
+ final boolean incrementValue) {
+ ImageProxy.PlaneProxy[] planes = new ImageProxy.PlaneProxy[1];
+
+ planes[0] =
+ createPlane(width, height, pixelStride, /*dataValue=*/ 1, incrementValue);
+ return planes;
+ }
+
@NonNull
private static ImageProxy.PlaneProxy createPlane(
final int width,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
index 91d37ce..1914e96 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/TestImageUtil.java
@@ -19,8 +19,10 @@
import static android.graphics.BitmapFactory.decodeByteArray;
import static android.graphics.ImageFormat.JPEG;
import static android.graphics.ImageFormat.JPEG_R;
+import static android.graphics.ImageFormat.RAW_SENSOR;
import static android.graphics.ImageFormat.YUV_420_888;
+import static androidx.camera.testing.impl.ImageProxyUtil.createRawImagePlanes;
import static androidx.camera.testing.impl.ImageProxyUtil.createYUV420ImagePlanes;
import static androidx.core.util.Preconditions.checkState;
@@ -105,6 +107,20 @@
}
/**
+ * Creates a [FakeImageProxy] with [RAW_SENSOR] format.
+ */
+ @NonNull
+ public static FakeImageProxy createRawFakeImageProxy(@NonNull ImageInfo imageInfo,
+ int width, int height) {
+ FakeImageProxy image = new FakeImageProxy(imageInfo);
+ image.setFormat(RAW_SENSOR);
+ image.setPlanes(createRawImagePlanes(width, height, 2, false));
+ image.setWidth(width);
+ image.setHeight(height);
+ return image;
+ }
+
+ /**
* Creates a {@link FakeImageProxy} from JPEG bytes.
*/
@NonNull
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt
new file mode 100644
index 0000000..e1b2f99
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeTakePictureManagerImpl.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl.fakes
+
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureRequest
+
+private const val TAG = "FakeTakePictureManager"
+
+internal class FakeTakePictureManagerImpl : TakePictureManager {
+ override fun setImagePipeline(imagePipeline: ImagePipeline) {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ override fun offerRequest(takePictureRequest: TakePictureRequest) {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ override fun pause() {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ override fun resume() {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ override fun abortRequests() {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ @VisibleForTesting
+ override fun hasCapturingRequest(): Boolean {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ @VisibleForTesting
+ override fun getCapturingRequest(): RequestWithCallback? {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ @VisibleForTesting
+ override fun getIncompleteRequests(): List<RequestWithCallback> {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+
+ @VisibleForTesting
+ override fun getImagePipeline(): ImagePipeline {
+ throw UnsupportedOperationException("Not implemented yet")
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/CameraTestActivityScenarioRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/CameraTestActivityScenarioRule.kt
new file mode 100644
index 0000000..9c93ed9
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/testrule/CameraTestActivityScenarioRule.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl.testrule
+
+import android.app.Activity
+import android.content.Intent
+import androidx.camera.testing.impl.InternalTestConvenience.useInCameraTest
+import androidx.test.core.app.ActivityScenario
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A [TestRule] to use [ActivityScenario] in a safer way for internal camera tests.
+ *
+ * See [useInCameraTest] for details.
+ */
+public class CameraTestActivityScenarioRule<A : Activity>
+private constructor(private val activityScenarioLazy: Lazy<ActivityScenario<A>>) : TestRule {
+ public constructor(
+ activityClass: Class<A>
+ ) : this(lazy { ActivityScenario.launch(activityClass) })
+
+ public constructor(intent: Intent) : this(lazy { ActivityScenario.launch(intent) })
+
+ public val scenario: ActivityScenario<A>
+ get() = activityScenarioLazy.value
+
+ override fun apply(base: Statement, description: Description): Statement =
+ object : Statement() {
+ override fun evaluate() {
+ scenario.useInCameraTest { base.evaluate() }
+ }
+ }
+}
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index ed18c77..f992c47 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -53,8 +53,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.autoValueAnnotations)
androidTestImplementation(project(":camera:camera-lifecycle"))
androidTestImplementation(project(":camera:camera-testing")) {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 19fa0d2..9753edb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -713,6 +713,14 @@
checkAndBindUseCases(preview, videoCapture)
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ // Bypass stream sharing if it's enforced on the device. Like quirks in
+ // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+ assumeFalse(
+ "The test is temporarily ignored when stream sharing is enabled.",
+ isStreamSharingEnabled(videoCapture)
+ )
+
val recording =
recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
@@ -737,6 +745,14 @@
checkAndBindUseCases(preview, videoCapture)
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ // Bypass stream sharing if it's enforced on the device. Like quirks in
+ // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+ assumeFalse(
+ "The test is temporarily ignored when stream sharing is enabled.",
+ isStreamSharingEnabled(videoCapture)
+ )
+
val recording =
recordingSession
.createRecording(asPersistentRecording = true)
@@ -928,6 +944,7 @@
)
}
+ @Ignore("TODO: b/353113961 - Temporarily ignored for persistent recording.")
@Test
fun updateVideoUsage_whenLifecycleStoppedBeforeCompletingPersistentRecording() = runBlocking {
assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
index a8e4f6d..1d9f872 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
@@ -46,6 +46,7 @@
import java.util.concurrent.TimeUnit
import org.junit.After
import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeNotNull
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -156,6 +157,9 @@
assumeTrue(baseProvider.hasProfile(quality))
val encoderProfiles = baseProvider.getAll(quality)
val baseVideoProfile = encoderProfiles!!.videoProfiles[0]
+ // Due to a known issue where VideoProfile might be null, see InvalidVideoProfilesQuirk,
+ // skip the test if VideoProfile is null.
+ assumeNotNull(baseVideoProfile)
// Act.
val resultVideoProfile = validateOrAdapt(baseVideoProfile, VideoEncoderInfoImpl.FINDER)
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index 251128f..fe349d6 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -70,8 +70,8 @@
}
androidTestImplementation(project(":camera:camera-camera2-pipe-integration"))
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
}
android {
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
index 0da6eab..e62c82a 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
@@ -32,9 +32,9 @@
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.camera.testing.impl.fakes.FakeActivity
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.testrule.CameraTestActivityScenarioRule
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -55,7 +55,7 @@
@RunWith(Parameterized::class)
@SdkSuppress(minSdkVersion = 21)
class PreviewViewBitmapTest(private val implName: String, private val cameraConfig: CameraXConfig) {
- @get:Rule val activityRule = ActivityScenarioRule(FakeActivity::class.java)
+ @get:Rule val activityRule = CameraTestActivityScenarioRule(FakeActivity::class.java)
@get:Rule
var useCamera =
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index c361fe0..3defd64b 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -70,8 +70,6 @@
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
@@ -273,6 +271,8 @@
instrumentation.runOnMainSync {
val previewView = PreviewView(context)
+ // Specifies the content description and uses it to find the view to click
+ previewView.contentDescription = previewView.hashCode().toString()
clickEventHelper = ClickEventHelper(previewView)
previewView.setOnTouchListener(clickEventHelper)
previewView.controller = fakeController
@@ -311,7 +311,6 @@
private var uiDevice: UiDevice? = null
private var limitedRetryCount = 0
private var retriedCounter = 0
- private var executor: ExecutorService? = null
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (view != targetView) {
@@ -337,7 +336,6 @@
uiDevice = null
limitedRetryCount = 0
retriedCounter = 0
- executor?.shutdown()
synchronized(lock) { isPerformingClick = false }
}
@@ -359,34 +357,16 @@
}
}
- executor = Executors.newSingleThreadExecutor()
limitedRetryCount = retryCount
retriedCounter = 0
this.uiDevice = uiDevice
performSingleClickInternal()
}
- private fun performSingleClickInternal() {
- executor!!.execute {
- var needClearContentDescription = false
- val originalContentDescription = targetView.contentDescription
-
- if (originalContentDescription == null || originalContentDescription.isEmpty()) {
- needClearContentDescription = true
- targetView.contentDescription = targetView.hashCode().toString()
- }
-
- uiDevice!!
- .findObject(
- UiSelector().descriptionContains(targetView.contentDescription.toString())
- )
- .click()
-
- if (needClearContentDescription) {
- targetView.contentDescription = originalContentDescription
- }
- }
- }
+ private fun performSingleClickInternal() =
+ uiDevice!!
+ .findObject(UiSelector().descriptionContains(targetView.hashCode().toString()))
+ .click()
}
@Test
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
index 2467886..e9f52ea 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewStreamStateTest.kt
@@ -30,10 +30,10 @@
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.camera.testing.impl.fakes.FakeActivity
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.testrule.CameraTestActivityScenarioRule
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -69,8 +69,8 @@
CameraUtil.grantCameraPermissionAndPreTestAndPostTest(PreTestCameraIdList(cameraConfig))
@get:Rule
- val activityRule: ActivityScenarioRule<FakeActivity> =
- ActivityScenarioRule(FakeActivity::class.java)
+ val activityRule: CameraTestActivityScenarioRule<FakeActivity> =
+ CameraTestActivityScenarioRule(FakeActivity::class.java)
@get:Rule
val cameraPipeConfigTestRule =
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
new file mode 100644
index 0000000..9e9562c
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
@@ -0,0 +1,692 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+import android.util.Log
+import android.util.Range
+import android.util.Rational
+import android.util.Size
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.AspectRatio.RATIO_16_9
+import androidx.camera.core.AspectRatio.RATIO_4_3
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.AspectRatioStrategy.FALLBACK_RULE_AUTO
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * ResolutionSelector related test on the real device.
+ *
+ * Make the ResolutionSelectorDeviceTest focus on the generic ResolutionSelector selection results
+ * for all the normal devices. Skips the tests when the devices have any of the quirks that might
+ * affect the selected resolution.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class ResolutionSelectorDeviceTest(
+ private val implName: String,
+ private var cameraSelector: CameraSelector,
+ private val cameraConfig: CameraXConfig,
+) {
+ @get:Rule
+ val cameraPipeConfigTestRule =
+ CameraPipeConfigTestRule(
+ active = implName.contains(CameraPipeConfig::class.simpleName!!),
+ )
+
+ @get:Rule
+ val cameraRule =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+ CameraUtil.PreTestCameraIdList(cameraConfig)
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+ private val useCaseFormatMap =
+ mapOf(
+ Pair(Preview::class.java, ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE),
+ Pair(ImageCapture::class.java, ImageFormat.JPEG),
+ Pair(ImageAnalysis::class.java, ImageFormat.YUV_420_888)
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() =
+ listOf(
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ Camera2Config.defaultConfig(),
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ arrayOf(
+ "front+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ ),
+ )
+ }
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var lifecycleOwner: FakeLifecycleOwner
+ private lateinit var camera: Camera
+ private lateinit var cameraInfoInternal: CameraInfoInternal
+
+ @Before
+ fun initializeCameraX() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ ProcessCameraProvider.configureInstance(cameraConfig)
+ cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+
+ instrumentation.runOnMainSync {
+ lifecycleOwner = FakeLifecycleOwner()
+ lifecycleOwner.startAndResume()
+
+ camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+ cameraInfoInternal = camera.cameraInfo as CameraInfoInternal
+ }
+
+ assumeNotAspectRatioQuirkDevice()
+ assumeNotOutputSizeQuirkDevice()
+ }
+
+ @After
+ fun shutdownCameraX() {
+ if (::cameraProvider.isInitialized) {
+ cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+ }
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_4_3)
+ }
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewImageCaptureAndImageAnalysis() {
+ canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_16_9)
+ }
+
+ private fun canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(
+ targetAspectRatio: Int
+ ) {
+ val preview = createUseCaseWithResolutionSelector(Preview::class.java, targetAspectRatio)
+ val imageCapture =
+ createUseCaseWithResolutionSelector(ImageCapture::class.java, targetAspectRatio)
+ val imageAnalysis =
+ createUseCaseWithResolutionSelector(ImageAnalysis::class.java, targetAspectRatio)
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ imageCapture,
+ imageAnalysis
+ )
+ }
+ assertThat(isResolutionAspectRatioBestMatched(preview, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageCapture, targetAspectRatio)).isTrue()
+ assertThat(isResolutionAspectRatioBestMatched(imageAnalysis, targetAspectRatio)).isTrue()
+ }
+
+ private fun isResolutionAspectRatioBestMatched(
+ useCase: UseCase,
+ targetAspectRatio: Int
+ ): Boolean {
+ val isMatched =
+ hasMatchingAspectRatio(
+ useCase.attachedSurfaceResolution!!,
+ aspectRatioToRational(targetAspectRatio)
+ )
+
+ if (isMatched) {
+ return true
+ }
+
+ // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM will be used to select resolutions for the
+ // combination of Preview + ImageAnalysis + ImageCapture
+ val closestAspectRatioSizes =
+ if (useCase is Preview || useCase is ImageAnalysis) {
+ getClosestAspectRatioSizesUnderPreviewSize(targetAspectRatio, useCase.javaClass)
+ } else {
+ getClosestAspectRatioSizes(targetAspectRatio, useCase.javaClass)
+ }
+
+ Log.d(
+ "ResolutionSelectorDeviceTest",
+ "The selected resolution (${useCase.attachedSurfaceResolution!!}) does not exactly" +
+ " match the target aspect ratio. It is selected from the closest aspect ratio" +
+ " sizes: $closestAspectRatioSizes"
+ )
+
+ return closestAspectRatioSizes.contains(useCase.attachedSurfaceResolution!!)
+ }
+
+ @Test
+ fun canSelect4x3ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForPreviewByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageCaptureByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_16_9)
+
+ @Test
+ fun canSelect4x3ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_4_3)
+
+ @Test
+ fun canSelect16x9ResolutionForImageAnalysisByResolutionStrategy() =
+ canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_16_9)
+
+ private fun <T : UseCase> canSelectResolutionByResolutionStrategy(
+ useCaseClass: Class<T>,
+ ratio: Int
+ ) {
+ // Filters the output sizes matching the target aspect ratio
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ .filter { size -> hasMatchingAspectRatio(size, aspectRatioToRational(ratio)) }
+ .let {
+ // Picks the item in the middle of the list to run the test
+ it.elementAtOrNull(it.size / 2)?.let { boundSize ->
+ {
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ aspectRatio = ratio,
+ aspectRatioStrategyFallbackRule = FALLBACK_RULE_AUTO,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule =
+ FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(boundSize)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun canSelectAnyResolutionForPreviewByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyHighResolutionForPreviewByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(
+ Preview::class.java,
+ // For Preview, need to override resolution strategy so that the output sizes larger
+ // than PREVIEW size can be selected.
+ cameraInfoInternal
+ .getSupportedHighResolutions(useCaseFormatMap[Preview::class.java]!!)
+ .maxWithOrNull(CompareSizesByArea())
+ )
+
+ @Test
+ fun canSelectAnyResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageCaptureByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageCapture::class.java)
+
+ @Test
+ fun canSelectAnyResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ @Test
+ fun canSelectAnyHighResolutionForImageAnalysisByResolutionFilter() =
+ canSelectAnyHighResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule
+ )
+
+ private fun <T : UseCase> canSelectAnyHighResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ ) =
+ canSelectAnyResolutionByResolutionFilter(
+ useCaseClass,
+ cameraInfoInternal.getSupportedHighResolutions(useCaseFormatMap[useCaseClass]!!),
+ boundSize,
+ resolutionStrategyFallbackRule,
+ PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+ )
+
+ private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+ useCaseClass: Class<T>,
+ outputSizes: List<Size>,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) {
+ outputSizes.forEach { targetResolution ->
+ val useCase =
+ createUseCaseWithResolutionSelector(
+ useCaseClass,
+ boundSize = boundSize,
+ resolutionStrategyFallbackRule = resolutionStrategyFallbackRule,
+ resolutionFilter = { _, _ -> mutableListOf(targetResolution) },
+ allowedResolutionMode = allowedResolutionMode
+ )
+ instrumentation.runOnMainSync {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+ }
+ assertThat(useCase.attachedSurfaceResolution).isEqualTo(targetResolution)
+ }
+ }
+
+ @Test
+ fun canSelectResolutionForSixtyFpsPreview() {
+ assumeTrue(isSixtyFpsSupported())
+
+ val preview = Preview.Builder().setTargetFrameRate(Range.create(60, 60)).build()
+ val imageCapture = ImageCapture.Builder().build()
+
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
+ }
+
+ assertThat(getMaxFrameRate(preview.attachedSurfaceResolution!!)).isEqualTo(60)
+ }
+
+ private fun isSixtyFpsSupported() =
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+ ?.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
+ ?.any { range -> range.upper == 60 } ?: false
+
+ private fun getMaxFrameRate(size: Size) =
+ (1_000_000_000.0 /
+ CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)!!.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )!!
+ .getOutputMinFrameDuration(SurfaceTexture::class.java, size) + 0.5)
+ .toInt()
+
+ private fun <T : UseCase> createUseCaseWithResolutionSelector(
+ useCaseClass: Class<T>,
+ aspectRatio: Int? = null,
+ aspectRatioStrategyFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ): UseCase {
+ val builder =
+ when (useCaseClass) {
+ Preview::class.java -> Preview.Builder()
+ ImageCapture::class.java -> ImageCapture.Builder()
+ ImageAnalysis::class.java -> ImageAnalysis.Builder()
+ else -> throw IllegalArgumentException("Unsupported class type!!")
+ }
+
+ (builder as ImageOutputConfig.Builder<*>).setResolutionSelector(
+ createResolutionSelector(
+ aspectRatio,
+ aspectRatioStrategyFallbackRule,
+ boundSize,
+ resolutionStrategyFallbackRule,
+ resolutionFilter,
+ allowedResolutionMode
+ )
+ )
+
+ return builder.build()
+ }
+
+ private fun createResolutionSelector(
+ aspectRatio: Int? = null,
+ aspectRatioFallbackRule: Int = FALLBACK_RULE_AUTO,
+ boundSize: Size? = null,
+ resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+ ) =
+ ResolutionSelector.Builder()
+ .apply {
+ aspectRatio?.let {
+ setAspectRatioStrategy(
+ AspectRatioStrategy(aspectRatio, aspectRatioFallbackRule)
+ )
+ }
+ boundSize?.let {
+ setResolutionStrategy(ResolutionStrategy(boundSize, resolutionFallbackRule))
+ }
+ resolutionFilter?.let { setResolutionFilter(resolutionFilter) }
+ setAllowedResolutionMode(allowedResolutionMode)
+ }
+ .build()
+
+ private fun aspectRatioToRational(ratio: Int) =
+ if (ratio == RATIO_16_9) {
+ ASPECT_RATIO_16_9
+ } else {
+ ASPECT_RATIO_4_3
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizesUnderPreviewSize(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes
+ .getSmallerThanOrEqualToPreviewScaleSizeSublist()
+ .getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun <T : UseCase> getClosestAspectRatioSizes(
+ targetAspectRatio: Int,
+ useCaseClass: Class<T>
+ ): List<Size> {
+ val outputSizes =
+ cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+ return outputSizes.getClosestAspectRatioSublist(targetAspectRatio)
+ }
+
+ private fun List<Size>.getSmallerThanOrEqualToPreviewScaleSizeSublist() = filter { size ->
+ SizeUtil.getArea(size) <= SizeUtil.getArea(getPreviewScaleSize())
+ }
+
+ @Suppress("DEPRECATION")
+ private fun getPreviewScaleSize(): Size {
+ val point = Point()
+ DisplayInfoManager.getInstance(context).getMaxSizeDisplay(false).getRealSize(point)
+ val displaySize = Size(point.x, point.y)
+ return if (SizeUtil.isSmallerByArea(RESOLUTION_1080P, displaySize)) {
+ RESOLUTION_1080P
+ } else {
+ displaySize
+ }
+ }
+
+ private fun List<Size>.getClosestAspectRatioSublist(targetAspectRatio: Int): List<Size> {
+ val sensorRect = (camera.cameraControl as RestrictedCameraControl).sensorRect
+ val aspectRatios = getResolutionListGroupingAspectRatioKeys(this)
+ val sortedAspectRatios =
+ aspectRatios.sortedWith(
+ AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatioToRational(targetAspectRatio),
+ Rational(sensorRect.width(), sensorRect.height())
+ )
+ )
+ val groupedRatioToSizesMap = groupSizesByAspectRatio(this)
+
+ for (ratio in sortedAspectRatios) {
+ groupedRatioToSizesMap[ratio]?.let {
+ if (it.isNotEmpty()) {
+ return it
+ }
+ }
+ }
+
+ fail("There should have one non-empty size list returned.")
+ }
+
+ /**
+ * Returns the grouping aspect ratio keys of the input resolution list.
+ *
+ * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an existing
+ * aspect ratio group if the aspect ratio can match by the mod16 rule.
+ */
+ private fun getResolutionListGroupingAspectRatioKeys(
+ resolutionCandidateList: List<Size>
+ ): List<Rational> {
+ val aspectRatios = mutableListOf<Rational>()
+
+ // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+ // additional items.
+ aspectRatios.add(ASPECT_RATIO_4_3)
+ aspectRatios.add(ASPECT_RATIO_16_9)
+
+ // Tries to find the aspect ratio which the target size belongs to.
+ for (size in resolutionCandidateList) {
+ val newRatio = Rational(size.width, size.height)
+ val aspectRatioFound = aspectRatios.contains(newRatio)
+
+ // The checking size might be a mod16 size which can be mapped to an existing aspect
+ // ratio group.
+ if (!aspectRatioFound) {
+ var hasMatchingAspectRatio = false
+ for (aspectRatio in aspectRatios) {
+ if (hasMatchingAspectRatio(size, aspectRatio)) {
+ hasMatchingAspectRatio = true
+ break
+ }
+ }
+ if (!hasMatchingAspectRatio) {
+ aspectRatios.add(newRatio)
+ }
+ }
+ }
+
+ return aspectRatios
+ }
+
+ /** Groups the input sizes into an aspect ratio to size list map. */
+ private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
+ val aspectRatioSizeListMap = mutableMapOf<Rational, MutableList<Size>>()
+ val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+ for (aspectRatio in aspectRatioKeys) {
+ aspectRatioSizeListMap[aspectRatio] = mutableListOf()
+ }
+
+ for (outputSize in sizes) {
+ for (key in aspectRatioSizeListMap.keys) {
+ // Put the size into all groups that is matched in mod16 condition since a size
+ // may match multiple aspect ratio in mod16 algorithm.
+ if (hasMatchingAspectRatio(outputSize, key)) {
+ aspectRatioSizeListMap[key]!!.add(outputSize)
+ }
+ }
+ }
+
+ return aspectRatioSizeListMap
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotAspectRatioQuirkDevice() {
+ assumeFalse(hasAspectRatioLegacyApi21Quirk())
+ assumeFalse(hasNexus4AndroidLTargetAspectRatioQuirk())
+ assumeFalse(hasExtraCroppingQuirk())
+ }
+
+ // Checks whether it is the device for AspectRatioLegacyApi21Quirk
+ private fun hasAspectRatioLegacyApi21Quirk(): Boolean {
+ val quirks = cameraInfoInternal.cameraQuirks
+
+ return if (implName == CameraPipeConfig::class.simpleName) {
+ quirks.contains(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ } else {
+ quirks.contains(
+ androidx.camera.camera2.internal.compat.quirk.AspectRatioLegacyApi21Quirk::class
+ .java
+ )
+ }
+ }
+
+ // Checks whether it is the device for Nexus4AndroidLTargetAspectRatioQuirk
+ private fun hasNexus4AndroidLTargetAspectRatioQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk
+ .Nexus4AndroidLTargetAspectRatioQuirk::class
+ .java
+ )
+ }
+
+ // Checks whether it is the device for ExtraCroppingQuirk
+ private fun hasExtraCroppingQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraCroppingQuirk::class.java
+ )
+ }
+
+ // Skips the tests when the devices have any of the quirks that might affect the selected
+ // resolution.
+ private fun assumeNotOutputSizeQuirkDevice() {
+ assumeFalse(hasExcludedSupportedSizesQuirk())
+ assumeFalse(hasExtraSupportedOutputSizeQuirk())
+ }
+
+ private fun hasExcludedSupportedSizesQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExcludedSupportedSizesQuirk::class
+ .java
+ )
+ }
+
+ private fun hasExtraSupportedOutputSizeQuirk() =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ hasDeviceQuirk(
+ androidx.camera.camera2.pipe.integration.compat.quirk
+ .ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ } else {
+ hasDeviceQuirk(
+ androidx.camera.camera2.internal.compat.quirk.ExtraSupportedOutputSizeQuirk::class
+ .java
+ )
+ }
+
+ private fun <T : Quirk?> hasDeviceQuirk(quirkClass: Class<T>) =
+ if (implName == CameraPipeConfig::class.simpleName) {
+ androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks.get(quirkClass)
+ } else {
+ androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.get(quirkClass)
+ } != null
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index c8427ff..aec2319 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
@@ -29,6 +29,7 @@
import static androidx.camera.core.ImageCapture.FLASH_MODE_SCREEN;
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG;
import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR;
+import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW;
import static androidx.camera.core.ImageCapture.getImageCaptureCapabilities;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.integration.core.CameraXViewModel.getConfiguredCameraXCameraImplementation;
@@ -983,19 +984,9 @@
public void onClick(View view) {
mImageSavedIdlingResource.increment();
mStartCaptureTime = SystemClock.elapsedRealtime();
- createDefaultPictureFolderIfNotExist();
- Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS",
- Locale.US);
- String fileName = "CoreTestApp-" + formatter.format(
- Calendar.getInstance().getTime()) + ".jpg";
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+
ImageCapture.OutputFileOptions outputFileOptions =
- new ImageCapture.OutputFileOptions.Builder(
- getContentResolver(),
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues).build();
+ createOutputFileOptions(mImageOutputFormat);
getImageCapture().takePicture(outputFileOptions,
mImageCaptureExecutorService,
new ImageCapture.OnImageSavedCallback() {
@@ -1039,6 +1030,40 @@
});
}
+ @SuppressLint("RestrictedApiAndroidX")
+ @NonNull
+ private ImageCapture.OutputFileOptions createOutputFileOptions(
+ @ImageCapture.OutputFormat int imageOutputFormat) {
+ createDefaultPictureFolderIfNotExist();
+ Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS",
+ Locale.US);
+
+ String suffix = "";
+ String mimetype = "";
+ switch (imageOutputFormat) {
+ case OUTPUT_FORMAT_RAW:
+ suffix = ".dng";
+ mimetype = "image/x-adobe-dng";
+ break;
+ case OUTPUT_FORMAT_JPEG_ULTRA_HDR:
+ case OUTPUT_FORMAT_JPEG:
+ suffix = ".jpg";
+ mimetype = "image/jpeg";
+ break;
+ }
+ String fileName = "CoreTestApp-" + formatter.format(
+ Calendar.getInstance().getTime()) + suffix;
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimetype);
+ return new ImageCapture.OutputFileOptions.Builder(
+ getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues).build();
+ }
+
+
private String getImageCaptureErrorMessage(@NonNull ImageCaptureException exception) {
String errorCodeString;
int errorCode = exception.getImageCaptureError();
@@ -2620,36 +2645,46 @@
return DYNAMIC_RANGE_UI_DATA.get(itemId).mDynamicRange;
}
+ @SuppressLint("RestrictedApiAndroidX")
@NonNull
private static String getImageOutputFormatIconName(@ImageCapture.OutputFormat int format) {
if (format == OUTPUT_FORMAT_JPEG) {
return "Jpeg";
} else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {
return "Ultra HDR";
+ } else if (format == OUTPUT_FORMAT_RAW) {
+ return "Raw";
}
return "?";
}
+ @SuppressLint("RestrictedApiAndroidX")
@NonNull
private static String getImageOutputFormatMenuItemName(@ImageCapture.OutputFormat int format) {
if (format == OUTPUT_FORMAT_JPEG) {
return "Jpeg";
} else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {
return "Ultra HDR";
+ } else if (format == OUTPUT_FORMAT_RAW) {
+ return "Raw";
}
return "Unknown format";
}
+ @SuppressLint("RestrictedApiAndroidX")
private static int imageOutputFormatToItemId(@ImageCapture.OutputFormat int format) {
if (format == OUTPUT_FORMAT_JPEG) {
return 0;
} else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) {
return 1;
+ } else if (format == OUTPUT_FORMAT_RAW) {
+ return 2;
} else {
throw new IllegalArgumentException("Undefined output format: " + format);
}
}
+ @SuppressLint("RestrictedApiAndroidX")
@ImageCapture.OutputFormat
private static int itemIdToImageOutputFormat(int itemId) {
switch (itemId) {
@@ -2657,6 +2692,8 @@
return OUTPUT_FORMAT_JPEG;
case 1:
return OUTPUT_FORMAT_JPEG_ULTRA_HDR;
+ case 2:
+ return OUTPUT_FORMAT_RAW;
default:
throw new IllegalArgumentException("Undefined item id: " + itemId);
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
index 9e7449f..0f56828 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
@@ -32,6 +32,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Logger;
@@ -77,6 +79,13 @@
private static final String VIDEO_FILE_PREFIX = "video";
private static final String INFO_FILE_PREFIX = "video_camera_switching_test_info";
private static final String KEY_DEVICE_ORIENTATION = "device_orientation";
+ private static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation";
+ // Camera2 implementation.
+ private static final String CAMERA2_IMPLEMENTATION_OPTION = "camera2";
+ // Camera-pipe implementation.
+ private static final String CAMERA_PIPE_IMPLEMENTATION_OPTION = "camera_pipe";
+
+ private static String sCameraImplementationType;
@NonNull
private CameraSelector mCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
@@ -103,6 +112,7 @@
private OrientationEventListener mOrientationEventListener;
private int mMirrorMode = MIRROR_MODE_OFF;
+ @OptIn(markerClass = androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -130,6 +140,21 @@
} else {
mMirrorMode = MIRROR_MODE_OFF;
}
+
+ String cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
+ if (cameraImplementation != null && sCameraImplementationType == null) {
+ if (cameraImplementation.equals(CAMERA2_IMPLEMENTATION_OPTION)) {
+ ProcessCameraProvider.configureInstance(Camera2Config.defaultConfig());
+ sCameraImplementationType = cameraImplementation;
+ } else if (cameraImplementation.equals(CAMERA_PIPE_IMPLEMENTATION_OPTION)) {
+ ProcessCameraProvider.configureInstance(
+ CameraPipeConfig.defaultConfig());
+ sCameraImplementationType = cameraImplementation;
+ } else {
+ throw new IllegalArgumentException("Failed to configure the CameraProvider "
+ + "using unknown " + cameraImplementation + " implementation option.");
+ }
+ }
}
mOrientationEventListener = new OrientationEventListener(this) {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
index 4dad639..355947d 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
@@ -21,8 +21,8 @@
import android.hardware.camera2.CameraCharacteristics
import android.os.Build
import android.util.Rational
-import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.utils.AspectRatioUtil
import androidx.camera.core.internal.utils.SizeUtil
import androidx.camera.extensions.ExtensionsManager
@@ -97,7 +97,8 @@
cameraProvider.bindToLifecycle(FakeLifecycleOwner(), extensionCameraSelector)
}
- cameraCharacteristics = Camera2CameraInfo.extractCameraCharacteristics(camera.cameraInfo)
+ cameraCharacteristics =
+ (camera.cameraInfo as CameraInfoInternal).cameraCharacteristics as CameraCharacteristics
}
@After
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
index b7321cb..da1eb32 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
@@ -19,8 +19,8 @@
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.os.Build
-import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.extensions.impl.PreviewExtenderImpl.ProcessorType
import androidx.camera.extensions.impl.PreviewImageProcessorImpl
@@ -95,7 +95,8 @@
cameraProvider.bindToLifecycle(FakeLifecycleOwner(), extensionCameraSelector)
}
- cameraCharacteristics = Camera2CameraInfo.extractCameraCharacteristics(camera.cameraInfo)
+ cameraCharacteristics =
+ (camera.cameraInfo as CameraInfoInternal).cameraCharacteristics as CameraCharacteristics
}
@After
diff --git a/camera/integration-tests/testingtestapp/build.gradle.kts b/camera/integration-tests/testingtestapp/build.gradle.kts
new file mode 100644
index 0000000..e2c430a
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/build.gradle.kts
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.application")
+ id("kotlin-android")
+ id("com.google.dagger.hilt.android")
+ id("com.google.devtools.ksp")
+}
+
+android {
+ namespace = "androidx.camera.integration.testingtestapp"
+
+ defaultConfig {
+ testInstrumentationRunner = "androidx.camera.integration.testingtestapp.utils.HiltTestRunner"
+
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+}
+
+dependencies {
+
+ // Core Android dependencies
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.activity:activity-compose:1.9.1")
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
+
+ // Hilt Dependency Injection
+ implementation(libs.hiltAndroid)
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.8")
+ debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.8")
+ ksp(libs.hiltCompiler)
+
+ // Arch Components
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.4")
+
+ // Compose
+ implementation("androidx.compose.ui:ui:1.6.8")
+ implementation("androidx.compose.ui:ui-tooling-preview:1.6.8")
+ implementation("androidx.compose.material3:material3:1.2.1")
+ implementation("androidx.compose.material:material-icons-core:1.6.8")
+
+ // Camera
+ implementation(project(":camera:camera-extensions"))
+ implementation(project(":camera:camera-view"))
+ implementation(project(":camera:camera-camera2"))
+ implementation(project(":camera:camera-lifecycle"))
+
+ // Testing
+ androidTestImplementation(libs.androidx.core)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ implementation(libs.testRunner)
+ implementation(libs.hiltAndroidTesting)
+ implementation(libs.testCore)
+}
diff --git a/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/TestCameraModule.kt b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/TestCameraModule.kt
new file mode 100644
index 0000000..813b3c7
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/TestCameraModule.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.testingtestapp
+
+import android.content.Context
+import androidx.camera.integration.testingtestapp.camerax.CameraModule
+import androidx.camera.view.LifecycleCameraController
+import androidx.core.content.ContextCompat
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.testing.TestInstallIn
+import java.util.concurrent.Executor
+import javax.inject.Named
+
+@Module
+@TestInstallIn(components = [ViewModelComponent::class], replaces = [CameraModule::class])
+class TestCameraModule() {
+
+ @Provides
+ fun provideLifecycleCameraController(
+ @ApplicationContext context: Context
+ ): LifecycleCameraController {
+ // TODO: Replace with fake
+ return LifecycleCameraController(context)
+ }
+
+ @Provides
+ @Named("MainExecutor")
+ fun provideMainExecutor(@ApplicationContext context: Context): Executor {
+ // TODO: Replace if necessary
+ return ContextCompat.getMainExecutor(context)
+ }
+}
diff --git a/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/ui/CameraTest.kt b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/ui/CameraTest.kt
new file mode 100644
index 0000000..4e3b950
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/ui/CameraTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.testingtestapp.ui
+
+import androidx.camera.integration.testingtestapp.R
+import androidx.camera.integration.testingtestapp.testing.HiltComponentActivity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Rule
+import org.junit.Test
+
+@HiltAndroidTest
+@SdkSuppress(minSdkVersion = 31) // TODO: b/360115093 - Enable tests only all APIs once fixed
+class CameraTest {
+
+ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule(order = 1)
+ val cameraPermissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(android.Manifest.permission.CAMERA)
+
+ @get:Rule(order = 2) var composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
+
+ @Test
+ fun test1() {
+
+ composeTestRule.setContent { Camera() }
+ composeTestRule
+ .onNodeWithContentDescription(composeTestRule.activity.getString(R.string.swap_cameras))
+ .assertExists()
+ }
+}
diff --git a/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/utils/HiltTestRunner.kt b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/utils/HiltTestRunner.kt
new file mode 100644
index 0000000..ddac7e0
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/androidTest/java/androidx/camera/integration/testingtestapp/utils/HiltTestRunner.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.testingtestapp.utils
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+/** A custom runner to set up the instrumented application class for tests. */
+class HiltTestRunner : AndroidJUnitRunner() {
+
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
+ return super.newApplication(cl, HiltTestApplication::class.java.name, context)
+ }
+}
diff --git a/camera/integration-tests/testingtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/testingtestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..64cbc34
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-feature android:name="android.hardware.camera.any" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ android:maxSdkVersion="28" />
+
+ <application
+ android:name=".CameraXTestingApp"
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.CameraXTesting"
+ tools:targetApi="31">
+ <activity
+ android:name=".ui.ComposeCameraActivity"
+ android:exported="true"
+ android:label="@string/title_activity_compose_camera"
+ android:theme="@style/Theme.CameraXTesting" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".testing.HiltComponentActivity" />
+ </application>
+
+</manifest>
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/CameraXTestingApp.kt
similarity index 67%
rename from biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
rename to camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/CameraXTestingApp.kt
index 233dbe6..5279237 100644
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/CameraXTestingApp.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package androidx.biometric.auth
+package androidx.camera.integration.testingtestapp
-/**
- * Thrown when an authentication attempt by the user has been rejected, e.g., the user's biometrics
- * were not recognized.
- */
-public class AuthPromptFailureException : Exception()
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp class CameraXTestingApp : Application()
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/CameraModule.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/CameraModule.kt
new file mode 100644
index 0000000..1d63ffa
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/CameraModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.testingtestapp.camerax
+
+import android.content.Context
+import androidx.camera.view.LifecycleCameraController
+import androidx.core.content.ContextCompat
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.util.concurrent.Executor
+import javax.inject.Named
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class CameraModule() {
+
+ @Provides
+ fun provideLifecycleCameraController(
+ @ApplicationContext context: Context
+ ): LifecycleCameraController {
+ return LifecycleCameraController(context)
+ }
+
+ @Provides
+ @Named("MainExecutor")
+ fun provideMainExecutor(@ApplicationContext context: Context): Executor {
+ return ContextCompat.getMainExecutor(context)
+ }
+}
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/OutputOptionsProvider.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/OutputOptionsProvider.kt
new file mode 100644
index 0000000..3475bfd
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/camerax/OutputOptionsProvider.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.testingtestapp.camerax
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import androidx.camera.core.ImageCapture
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.text.SimpleDateFormat
+import java.util.Locale
+import javax.inject.Inject
+
+class OutputOptionsProvider @Inject constructor(@ApplicationContext val context: Context) {
+ fun getOutputOptions(filename: String): ImageCapture.OutputFileOptions {
+ // Create time stamped name and MediaStore entry.
+ val name =
+ filename +
+ SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
+ val contentValues =
+ ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
+ }
+ }
+
+ // Create output options object which contains file + metadata
+ return ImageCapture.OutputFileOptions.Builder(
+ context.contentResolver,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ .build()
+ }
+}
+
+private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/testing/HiltComponentActivity.kt
similarity index 61%
copy from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
copy to camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/testing/HiltComponentActivity.kt
index c273ad3..2917cf0 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/testing/HiltComponentActivity.kt
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.camera.integration.testingtestapp.testing
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+import androidx.activity.ComponentActivity
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * A [ComponentActivity] annotated with [AndroidEntryPoint] for use in tests, as a workaround for
+ * https://github.com/google/dagger/issues/3394
+ */
+@AndroidEntryPoint class HiltComponentActivity : ComponentActivity()
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/Camera.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/Camera.kt
new file mode 100644
index 0000000..35dca0c
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/Camera.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.testingtestapp.ui
+
+import androidx.camera.integration.testingtestapp.R
+import androidx.camera.view.PreviewView
+import androidx.camera.view.PreviewView.ScaleType.FIT_CENTER
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.AddCircle
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+
+/** Shows a preview and buttons to take photos. */
+@Composable
+fun Camera(viewModel: CameraViewModel = viewModel()) {
+
+ val context = LocalContext.current
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ val cameraController =
+ remember(lifecycleOwner) { viewModel.initCameraController(lifecycleOwner) }
+
+ DisposableEffect(key1 = lifecycleOwner) { onDispose { viewModel.disposeCameraController() } }
+
+ val errors = viewModel.errorState.collectAsStateWithLifecycle()
+ if (errors.value.isNotEmpty()) {
+ Text(errors.value)
+ } else {
+ Camera(
+ toggleCamera = { viewModel.toggleCamera() },
+ takePhoto = { viewModel.takePhoto() },
+ frontCamera = !viewModel.isUsingBackLens.collectAsStateWithLifecycle().value
+ ) {
+ AndroidView(
+ factory = {
+ PreviewView(context).apply {
+ try {
+ controller = cameraController
+ } catch (e: IllegalArgumentException) {
+ viewModel.setViewError(e)
+ }
+ scaleType = FIT_CENTER
+ }
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+}
+
+/** Stateless composable for the Camera's UI. */
+@Composable
+fun Camera(
+ toggleCamera: () -> Unit,
+ takePhoto: () -> Unit,
+ frontCamera: Boolean,
+ content: @Composable () -> Unit
+) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ content()
+ IconButton(
+ modifier = Modifier.align(Alignment.BottomStart).padding(16.dp),
+ onClick = takePhoto
+ ) {
+ Icon(
+ imageVector = Icons.Filled.AddCircle,
+ contentDescription = stringResource(R.string.take_photo),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(54.dp)
+ )
+ }
+ IconButton(
+ modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
+ onClick = toggleCamera
+ ) {
+ Icon(
+ imageVector =
+ if (frontCamera) Icons.AutoMirrored.Filled.ArrowForward
+ else Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.swap_cameras),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(54.dp)
+ )
+ }
+ }
+}
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/CameraViewModel.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/CameraViewModel.kt
new file mode 100644
index 0000000..9dec81e
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/CameraViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.testingtestapp.ui
+
+import android.util.Log
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture.OnImageSavedCallback
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.integration.testingtestapp.camerax.OutputOptionsProvider
+import androidx.camera.view.LifecycleCameraController
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+@HiltViewModel
+class CameraViewModel
+@Inject
+constructor(
+ private val outputOptionsProvider: OutputOptionsProvider,
+ private val cameraController: LifecycleCameraController,
+ @Named("MainExecutor") private val mainExecutor: Executor,
+) : ViewModel() {
+
+ val errorState = MutableStateFlow("")
+
+ private val _isUsingBackLens = MutableStateFlow(false)
+
+ // Source of truth for whether we're using the back camera
+ val isUsingBackLens: StateFlow<Boolean>
+ get() = _isUsingBackLens
+
+ fun toggleCamera() {
+ _isUsingBackLens.value = !_isUsingBackLens.value
+ val newCamera =
+ if (isUsingBackLens.value) {
+ CameraSelector.DEFAULT_BACK_CAMERA
+ } else {
+ CameraSelector.DEFAULT_FRONT_CAMERA
+ }
+ if (cameraController.hasCamera(newCamera)) {
+ cameraController.cameraSelector = newCamera
+ }
+ }
+
+ fun takePhoto() {
+ try {
+ cameraController.takePicture(
+ outputOptionsProvider.getOutputOptions("myPhoto"),
+ mainExecutor,
+ object : OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: OutputFileResults) {
+ Log.d(LOG_TAG, "Saved in ${outputFileResults.savedUri}")
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ Log.d(LOG_TAG, "Errors! ${exception.message}")
+ }
+ }
+ )
+ } catch (e: IllegalStateException) {
+ errorState.value = "Camera not ready"
+ }
+ }
+
+ fun initCameraController(lifecycleOwner: LifecycleOwner): LifecycleCameraController {
+ with(cameraController) {
+ bindToLifecycle(lifecycleOwner)
+ cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+ initializationFuture.addListener(
+ {
+ if (isUsingBackLens.value && hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) {
+ cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+ } else if (
+ !isUsingBackLens.value && hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
+ ) {
+ cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+ } else {
+ Log.d(LOG_TAG, "Error, no camera supported")
+ errorState.value = "No camera supported"
+ }
+ },
+ mainExecutor
+ )
+ }
+ return cameraController
+ }
+
+ fun disposeCameraController() {
+ cameraController.unbind()
+ }
+
+ fun setViewError(e: IllegalArgumentException) {
+ errorState.value = e.message ?: "Error binding to view"
+ }
+}
+
+private val LOG_TAG = "CameraViewModel"
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/ComposeCameraActivity.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/ComposeCameraActivity.kt
new file mode 100644
index 0000000..6fd752e
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/ComposeCameraActivity.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.testingtestapp.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.camera.integration.testingtestapp.ui.theme.MultimoduleTemplateTheme
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class ComposeCameraActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MultimoduleTemplateTheme { Camera() }
+ // } else {
+ // Column {
+ // val textToShow = if
+ // (cameraPermissionState.status.shouldShowRationale) {
+ // // If the user has denied the permission but the rationale can
+ // be shown,
+ // // then gently explain why the app requires this permission
+ // "The camera is important for this app. Please grant the
+ // permission."
+ // } else {
+ // // If it's the first time the user lands on this feature, or
+ // the user
+ // // doesn't want to be asked again for this permission, explain
+ // that the
+ // // permission is required
+ // "Camera permission required for this feature to be available.
+ // " +
+ // "Please grant the permission"
+ // }
+ // Text(textToShow)
+ // Button(onClick = { cameraPermissionState.launchPermissionRequest()
+ // }) {
+ // Text("Request permission")
+ // }
+ // }
+ // }
+ }
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Color.kt
similarity index 60%
copy from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
copy to camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Color.kt
index c273ad3..599d3ae 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Color.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package androidx.camera.integration.testingtestapp.ui.theme
-package androidx.compose.foundation.layout
+import androidx.compose.ui.graphics.Color
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt
new file mode 100644
index 0000000..933cedc
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Theme.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.testingtestapp.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme =
+ darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80)
+
+private val LightColorScheme =
+ lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+ )
+
+@Composable
+fun MultimoduleTemplateTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme =
+ when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
+}
diff --git a/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Type.kt b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Type.kt
new file mode 100644
index 0000000..4391ace
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/java/androidx/camera/integration/testingtestapp/ui/theme/Type.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.testingtestapp.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+ )
diff --git a/camera/integration-tests/testingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/camera/integration-tests/testingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..0a38769
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,46 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+</vector>
diff --git a/camera/integration-tests/testingtestapp/src/main/res/drawable/ic_launcher_background.xml b/camera/integration-tests/testingtestapp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..8677364
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+</vector>
diff --git a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml b/camera/integration-tests/testingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 62%
copy from biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
copy to camera/integration-tests/testingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eda0e80..d5c96b2 100644
--- a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/testingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright (C) 2020 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -12,5 +13,9 @@
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
+ -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
diff --git a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml b/camera/integration-tests/testingtestapp/src/main/res/values/colors.xml
similarity index 75%
copy from biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
copy to camera/integration-tests/testingtestapp/src/main/res/values/colors.xml
index eda0e80..0440a40 100644
--- a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/testingtestapp/src/main/res/values/colors.xml
@@ -1,5 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright (C) 2020 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -12,5 +13,7 @@
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
+ -->
+
+<resources>
+</resources>
diff --git a/camera/integration-tests/testingtestapp/src/main/res/values/strings.xml b/camera/integration-tests/testingtestapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3edbaa1
--- /dev/null
+++ b/camera/integration-tests/testingtestapp/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_name">CameraXTesting</string>
+ <string name="title_activity_compose_camera">ComposeCamera</string>
+ <string name="take_photo">Take Photo</string>
+ <string name="start_capture">Start Capture</string>
+ <string name="stop_capture">Stop Capture</string>
+ <string name="swap_cameras">Swap cameras</string>
+</resources>
diff --git a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml b/camera/integration-tests/testingtestapp/src/main/res/values/themes.xml
similarity index 76%
rename from biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
rename to camera/integration-tests/testingtestapp/src/main/res/values/themes.xml
index eda0e80..d89f463 100644
--- a/biometric/biometric-ktx/samples/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/testingtestapp/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright (C) 2020 The Android Open Source Project
+ ~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -12,5 +12,9 @@
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
+ -->
+
+<resources>
+
+ <style name="Theme.CameraXTesting" parent="android:Theme.Material.Light.NoActionBar" />
+</resources>
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
index 8e320c7..e8c033c 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/compose/ComposeCameraAppTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
import androidx.testutils.RepeatRule
import com.google.common.truth.Truth
@@ -69,6 +70,7 @@
// Activity launch will render ImageCaptureScreen
// Ensure that ImageCapture screen's PreviewView is streaming properly
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
@Test
@RepeatRule.Repeat(times = 10)
fun testPreviewViewStreamStateOnActivityLaunch() {
@@ -84,6 +86,7 @@
@Test
@LabTestRule.LabTestOnly
@RepeatRule.Repeat(times = 10)
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewStreamStateOnNavigation() {
// Get VideoCapture Navigation Tab (Node)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisLockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisLockedOrientationTest.kt
index 3e07be1..a86f56e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisLockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisLockedOrientationTest.kt
@@ -18,6 +18,7 @@
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -60,6 +61,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<LockedOrientationActivity>(lensFacing, cameraXConfig) {
rotate(rotationDegrees)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisOrientationConfigChangesTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisOrientationConfigChangesTest.kt
index 2295f9d..e78f757 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisOrientationConfigChangesTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisOrientationConfigChangesTest.kt
@@ -21,6 +21,7 @@
import android.view.View
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
@@ -87,6 +88,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<OrientationConfigChangesOverriddenActivity>(lensFacing, cameraXConfig) {
if (rotate(rotation)) {
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisUnlockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisUnlockedOrientationTest.kt
index 731580e..b4e3ba9 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisUnlockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageAnalysisUnlockedOrientationTest.kt
@@ -23,6 +23,7 @@
import androidx.camera.integration.uiwidgets.rotations.RotationUnlocked.Natural
import androidx.camera.integration.uiwidgets.rotations.RotationUnlocked.Right
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
@@ -66,6 +67,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<UnlockedOrientationActivity>(lensFacing, cameraXConfig) {
if (rotation.shouldRotate) {
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
index 915afae..3949f07 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
@@ -19,6 +19,7 @@
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -67,6 +68,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<LockedOrientationActivity>(lensFacing, captureMode, cameraXConfig) {
rotate(rotationDegrees)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureOrientationConfigChangesTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureOrientationConfigChangesTest.kt
index bcaa1eae..c412145 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureOrientationConfigChangesTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureOrientationConfigChangesTest.kt
@@ -22,6 +22,7 @@
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
@@ -93,6 +94,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<OrientationConfigChangesOverriddenActivity>(
lensFacing,
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
index 6883234..fad2968 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
@@ -24,6 +24,7 @@
import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.IMAGE_CAPTURE_MODE_OUTPUT_STREAM
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
@@ -110,6 +111,7 @@
}
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun verifyRotation() {
verifyRotation<UnlockedOrientationActivity>(lensFacing, captureMode, cameraXConfig) {
if (rotation.shouldRotate) {
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
index 886d1b5..d7059fe 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
@@ -44,6 +44,7 @@
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.common.truth.Truth.assertThat
@@ -149,6 +150,7 @@
// The test makes sure the camera PreviewView is in the streaming state.
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewUpdateAfterStopResume() {
launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
@@ -166,6 +168,7 @@
// The test makes sure the TextureView surface texture keeps the same after switch.
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewUpdateAfterSwitch() {
assumeFalse(shouldSkipTest()) // b/331933633
@@ -198,6 +201,7 @@
implementationMode == PERFORMANCE_MODE
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewUpdateAfterSwitchAndStop_ResumeAndSwitchBack() {
launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
index b7f221e..43d41b5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPagerActivityTest.kt
@@ -41,6 +41,7 @@
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.common.truth.Truth.assertThat
@@ -135,6 +136,7 @@
// The test makes sure the camera PreviewView is in the streaming state.
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewUpdateAfterStopResume() {
launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
@@ -152,6 +154,7 @@
// The test makes sure the TextureView surface texture keeps the same after switch.
@Test
+ @SdkSuppress(maxSdkVersion = 33) // b/360867144: Module crashes on API34
fun testPreviewViewUpdateAfterSwitch() {
launchActivity(lensFacing, cameraXConfig).useInCameraTest { scenario ->
// At first, check Preview in stream state
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 4cff2d8..6a72f25 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -56,6 +56,7 @@
implementation(project(":camera:camera-view"))
implementation(project(":camera:camera-video"))
implementation(project(":camera:camera-effects"))
+ implementation(project(":camera:camera-media3-effect"))
implementation(libs.guavaAndroid)
implementation('com.google.mlkit:barcode-scanning:17.0.2')
implementation("androidx.exifinterface:exifinterface:1.3.2")
@@ -78,6 +79,8 @@
implementation("androidx.compose.material:material:1.4.0")
implementation("androidx.compose.ui:ui:1.4.0")
implementation("androidx.compose.foundation:foundation:1.4.0")
+ implementation(libs.media3Common)
+ implementation(libs.media3Effect)
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.1")
diff --git a/camera/viewfinder/viewfinder-view/build.gradle b/camera/viewfinder/viewfinder-view/build.gradle
index 4225bde..b2854e8 100644
--- a/camera/viewfinder/viewfinder-view/build.gradle
+++ b/camera/viewfinder/viewfinder-view/build.gradle
@@ -62,8 +62,8 @@
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
android {
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
index 98ab821..5c3544f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zh-rCN/strings.xml
@@ -165,7 +165,7 @@
<string name="gas_station" msgid="1203313937444666161">"加油站"</string>
<string name="short_route" msgid="4831864276538141265">"短路线"</string>
<string name="less_busy" msgid="310625272281710983">"不忙"</string>
- <string name="hov_friendly" msgid="6956152104754594971">"高承载率车辆友好"</string>
+ <string name="hov_friendly" msgid="6956152104754594971">"适合多乘员车辆"</string>
<string name="long_route" msgid="4737969235741057506">"长路线"</string>
<string name="continue_start_nav" msgid="6231797535084469163">"继续开始导航"</string>
<string name="continue_route" msgid="5172258139245088080">"继续规划路线"</string>
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/current.ignore
+++ b/car/app/app/api/current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/restricted_current.ignore
+++ b/car/app/app/api/restricted_current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
/**
* {@link Bundle} key used in the rootHints bundle passed to
+ * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+ * to indicate the version of the caller. Note that this should only be used for analytics and
+ * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+ *
+ * <p>TYPE: string - the version info.
+ */
+ public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+ "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+ /**
+ * {@link Bundle} key used in the rootHints bundle passed to
* {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
* which version of the media api is used by the caller
*
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 1f975fd2..c94e4a0 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -340,13 +340,12 @@
* <p>The host creates indexed lists to help users navigate through long lists more easily
* by sorting, filtering, or some other means.
*
- * <p>For example, a media app may, by default, show a user's playlists sorted by date
- * created. If the app provides these playlists via the {@code SectionedItemTemplate} and
- * enables {@code #isAlphabeticalIndexingAllowed}, the user will be able to jump to their
- * playlists that start with the letter "H". When this happens, the list is reconstructed
- * and sorted alphabetically, then shown to the user, jumping down to the letter "H". If
- * the item is set to {@code #setIndexable(false)}, the item will not show up in this newly
- * sorted list.
+ * <p>For example, a messaging app may show conversations by last message received. If the
+ * app provides these conversations via the {@code SectionedItemTemplate} and enables
+ * {@code #isAlphabeticalIndexingAllowed}, the user will be able to jump to their
+ * conversations that start with a given letter they chose. The messaging app can choose
+ * to hide, for example, service messages from this filtered list by setting this {@code
+ * #setIndexable(false)}.
*
* <p>Individual items can be set to be included or excluded from filtered lists, but it's
* also possible to enable/disable the creation of filtered lists as a whole via the
diff --git a/car/app/app/src/main/java/androidx/car/app/serialization/ListDelegate.kt b/car/app/app/src/main/java/androidx/car/app/serialization/ListDelegate.kt
index 4e99e87..355023f 100644
--- a/car/app/app/src/main/java/androidx/car/app/serialization/ListDelegate.kt
+++ b/car/app/app/src/main/java/androidx/car/app/serialization/ListDelegate.kt
@@ -32,7 +32,8 @@
/**
* Host-side interface for requesting items in range `[startIndex, endIndex]` (both inclusive).
*
- * The sublist is returned to the host as a [List], via [OnDoneCallback.onSuccess]
+ * The sublist is returned to the host as a [List], via [OnDoneCallback.onSuccess] on the main
+ * thread.
*/
@SuppressLint("ExecutorRegistration")
fun requestItemRange(startIndex: Int, endIndex: Int, callback: OnDoneCallback)
diff --git a/collection/collection/bcv/native/current.txt b/collection/collection/bcv/native/current.txt
index 215b18d..38ae0962 100644
--- a/collection/collection/bcv/native/current.txt
+++ b/collection/collection/bcv/native/current.txt
@@ -1,5 +1,5 @@
// Klib ABI Dump
-// Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64]
+// Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 7833819..8e38f60 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -38,6 +38,7 @@
mac()
linux()
ios()
+ watchosDeviceArm64()
watchos()
tvos()
mingwX64()
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
index 16ba576..e0b4519 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/internal/LockExt.kt
@@ -19,6 +19,9 @@
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
+// Workaround for applying callsInPlace to expect fun no longer works.
+// See: https://youtrack.jetbrains.com/issue/KT-29963
+@Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
internal inline fun <T> Lock.synchronized(block: () -> T): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return synchronizedImpl(block)
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
index 964b216..e0728603 100644
--- a/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/internal/Lock.native.kt
@@ -38,7 +38,9 @@
private val lockImpl = LockImpl()
- @Suppress("unused") // The returned Cleaner must be assigned to a property
+ // unused - The returned Cleaner must be assigned to a property
+ // TODO(/365786168) Replace with kotlin.native.ref.createCleaner, after kotlin bump to 1.9+
+ @Suppress("unused", "DEPRECATION")
@OptIn(ExperimentalStdlibApi::class)
private val cleaner = createCleaner(lockImpl, LockImpl::destroy)
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 595aef5..a0f829e 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -54,6 +54,9 @@
commonTest {
dependencies {
+ implementation(kotlin("test"))
+ implementation(libs.kotlinCoroutinesTest)
+ implementation(project(":kruth:kruth"))
}
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
deleted file mode 100644
index c09ec81..0000000
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.core
-
-import androidx.compose.runtime.MonotonicFrameClock
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.unit.IntSize
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
-import kotlin.math.abs
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class AnimatableTest {
- @Test
- fun animateDecayTest() {
- runBlocking {
- val from = 9f
- val initialVelocity = 20f
- val decaySpec = FloatExponentialDecaySpec()
- val anim =
- DecayAnimation(decaySpec, initialValue = from, initialVelocity = initialVelocity)
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- withContext(clock) {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..5000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- }
- var playTimeMillis = 0L
- val animatable = Animatable(9f)
- val result =
- animatable.animateDecay(20f, animationSpec = exponentialDecay()) {
- assertTrue(isRunning)
- assertEquals(anim.targetValue, targetValue)
- assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
- assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
- playTimeMillis += interval
- assertEquals(value, animatable.value, 0.0001f)
- assertEquals(velocity, animatable.velocity, 0.0001f)
- }
- // After animation
- assertEquals(anim.targetValue, animatable.value)
- assertEquals(false, animatable.isRunning)
- assertEquals(0f, animatable.velocity)
- assertEquals(AnimationEndReason.Finished, result.endReason)
- assertTrue(abs(result.endState.velocity) <= decaySpec.absVelocityThreshold)
- }
- }
- }
-
- @Test
- fun animateToTest() {
- runBlocking {
- val anim =
- TargetBasedAnimation(
- spring(dampingRatio = Spring.DampingRatioMediumBouncy),
- Float.VectorConverter,
- initialValue = 0f,
- targetValue = 1f
- )
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- val animatable = Animatable(0f)
- withContext(clock) {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..5000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- }
- var playTimeMillis = 0L
- val result =
- animatable.animateTo(
- 1f,
- spring(dampingRatio = Spring.DampingRatioMediumBouncy)
- ) {
- assertTrue(isRunning)
- assertEquals(1f, targetValue)
- assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
- assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
- playTimeMillis += interval
- }
- // After animation
- assertEquals(anim.targetValue, animatable.value)
- assertEquals(0f, animatable.velocity)
- assertEquals(false, animatable.isRunning)
- assertEquals(AnimationEndReason.Finished, result.endReason)
- }
- }
- }
-
- @Test
- fun animateToGenericTypeTest() =
- runBlocking<Unit> {
- val from = Offset(666f, 321f)
- val to = Offset(919f, 864f)
- val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
- TwoWayConverter(
- convertToVector = { AnimationVector2D(it.x, it.y) },
- convertFromVector = { Offset(it.v1, it.v2) }
- )
- val anim =
- TargetBasedAnimation(
- tween(500),
- offsetToVector,
- initialValue = from,
- targetValue = to
- )
- val clock = SuspendAnimationTest.TestFrameClock()
- val interval = 50
- val animatable = Animatable(initialValue = from, typeConverter = offsetToVector)
- coroutineScope {
- withContext(clock) {
- launch {
- // Put in a bunch of frames 50 milliseconds apart
- for (frameTimeMillis in 0..1000 step interval) {
- clock.frame(frameTimeMillis * 1_000_000L)
- delay(5)
- }
- }
- launch {
- // The first frame should start at 100ms
- var playTimeMillis = 0L
- animatable.animateTo(to, animationSpec = tween(500)) {
- assertTrue("PlayTime Millis: $playTimeMillis", isRunning)
- assertEquals(to, targetValue)
- val expectedValue = anim.getValueFromMillis(playTimeMillis)
- assertEquals(
- "PlayTime Millis: $playTimeMillis",
- expectedValue.x,
- value.x,
- 0.001f
- )
- assertEquals(
- "PlayTime Millis: $playTimeMillis",
- expectedValue.y,
- value.y,
- 0.001f
- )
- playTimeMillis += interval
-
- if (playTimeMillis == 300L) {
- // Prematurely cancel the animation and check corresponding states
- [email protected] {
- stop()
- assertFalse(isRunning)
- assertEquals(playTimeMillis, 300L)
- assertEquals(to, animatable.targetValue)
- assertEquals(AnimationVector(0f, 0f), animatable.velocityVector)
- }
- }
- }
- }
- }
- }
- }
-
- @Test
- fun animateToWithInterruption() {
- runBlocking {
- val anim1 =
- TargetBasedAnimation(
- tween(200, easing = LinearEasing),
- Float.VectorConverter,
- 0f,
- 200f
- )
- val clock = MyTestFrameClock()
- val interval = 50
- coroutineScope {
- withContext(clock) {
- val animatable = Animatable(0f)
- var playTimeMillis by mutableStateOf(0L)
-
- suspend fun createInterruption() {
- val anim2 =
- TargetBasedAnimation(
- spring(),
- Float.VectorConverter,
- animatable.value,
- 300f,
- animatable.velocity
- )
- assertEquals(100L, playTimeMillis)
- var firstFrame = true
- val result2 =
- animatable.animateTo(300f, spring()) {
- // First frame will arrive with a timestamp of the time of
- // interruption,
- // which is 100ms. The subsequent frames will be consistent with
- // what's
- // tracked in `playTimeMillis`.
- val playTime = if (firstFrame) 100L else playTimeMillis
- assertTrue(isRunning)
- assertEquals(300f, targetValue)
- assertEquals(anim2.getValueFromMillis((playTime - 100)), value)
- assertEquals(
- anim2.getVelocityFromMillis((playTime - 100)),
- velocity
- )
- if (!firstFrame) {
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- } else {
- firstFrame = false
- }
- }
- assertFalse(animatable.isRunning)
- assertEquals(AnimationEndReason.Finished, result2.endReason)
- assertEquals(300f, animatable.targetValue)
- assertEquals(300f, animatable.value)
- assertEquals(0f, animatable.velocity)
- }
-
- clock.trySendFrame(0)
- launch {
- try {
- animatable.animateTo(
- 200f,
- animationSpec = tween(200, easing = LinearEasing)
- ) {
- assertTrue(isRunning)
- assertEquals(targetValue, 200f)
- assertEquals(anim1.getValueFromMillis(playTimeMillis), value)
- assertEquals(anim1.getVelocityFromMillis(playTimeMillis), velocity)
-
- assertTrue(playTimeMillis <= 100)
- if (playTimeMillis == 100L) {
- [email protected] {
- // No more new frame until the ongoing animation is
- // canceled.
- createInterruption()
- }
- } else {
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- }
- }
- } finally {
- // At this point the previous animation on the Animatable has been
- // canceled. Pump a frame to get the new animation going.
- playTimeMillis += interval
- clock.trySendFrame(playTimeMillis * 1_000_000L)
- }
- }
- }
- }
- }
- }
-
- @Test
- fun testUpdateBounds() {
- val animatable = Animatable(5f)
- // Update bounds when *not* running
- animatable.updateBounds(0f, 4f)
- assertEquals(4f, animatable.value)
- runBlocking {
- val clock = SuspendAnimationTest.TestFrameClock()
- // Put two frames in clock
- clock.frame(0L)
- clock.frame(200 * 1_000_000L)
-
- withContext(clock) {
- animatable.animateTo(4f, tween(100)) {
- if (animatable.upperBound == 4f) {
- // Update bounds while running
- animatable.updateBounds(-4f, 0f)
- }
- }
- }
- }
- assertEquals(0f, animatable.value)
-
- // Snap to value out of bounds
- runBlocking { animatable.snapTo(animatable.lowerBound!! - 100f) }
- assertEquals(animatable.lowerBound!!, animatable.value)
- }
-
- @Test
- fun testIntSize_alwaysWithinValidBounds() {
- val animatable =
- Animatable(
- initialValue = IntSize(10, 10),
- typeConverter = IntSize.VectorConverter,
- visibilityThreshold = IntSize.VisibilityThreshold
- )
-
- val values = mutableListOf<IntSize>()
-
- runBlocking {
- val clock = SuspendAnimationTest.TestFrameClock()
-
- // Add frames to evaluate at
- clock.frame(0L)
- clock.frame(25L * 1_000_000L)
- clock.frame(75L * 1_000_000L)
- clock.frame(100L * 1_000_000L)
-
- withContext(clock) {
- // Animate linearly from -100 to 100
- animatable.animateTo(
- IntSize(100, 100),
- keyframes {
- durationMillis = 100
- IntSize(-100, -100) at 0 using LinearEasing
- }
- ) {
- values.add(value)
- }
- }
- }
-
- // The internal animation is expected to be: -100, -50, 50, 100. But for IntSize, we don't
- // support negative values, so it's clamped to Zero
- assertEquals(4, values.size)
- assertEquals(IntSize.Zero, values[0])
- assertEquals(IntSize.Zero, values[1])
- assertEquals(IntSize(50, 50), values[2])
- assertEquals(IntSize(100, 100), values[3])
- }
-
- @Test
- fun animationResult_toString() {
- val animatable =
- AnimationResult(endReason = AnimationEndReason.Finished, endState = AnimationState(42f))
- val string = animatable.toString()
- assertThat(string).contains(AnimationResult::class.java.simpleName)
- assertThat(string).contains("endReason=Finished")
- assertThat(string).contains("endState=")
- }
-
- @Test
- fun animationState_toString() {
- val state =
- AnimationState(
- initialValue = 42f,
- initialVelocity = 2f,
- lastFrameTimeNanos = 4000L,
- finishedTimeNanos = 3000L,
- isRunning = true
- )
- val string = state.toString()
- assertThat(string).contains(AnimationState::class.java.simpleName)
- assertThat(string).contains("value=42.0")
- assertThat(string).contains("velocity=2.0")
- assertThat(string).contains("lastFrameTimeNanos=4000")
- assertThat(string).contains("finishedTimeNanos=3000")
- assertThat(string).contains("isRunning=true")
- }
-
- private class MyTestFrameClock : MonotonicFrameClock {
- // Make the send non-blocking
- private val frameCh = Channel<Long>(Channel.UNLIMITED)
-
- suspend fun frame(frameTimeNanos: Long) {
- frameCh.send(frameTimeNanos)
- }
-
- fun trySendFrame(frameTimeNanos: Long) {
- frameCh.trySend(frameTimeNanos)
- }
-
- override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
- onFrame(frameCh.receive())
- }
-}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt
new file mode 100644
index 0000000..4b5a229
--- /dev/null
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.android.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.util.floatFromBits
+import kotlin.math.ulp
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+// This test can't be in commonTest because
+// Float.ulp is jvm only: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/ulp.html
+class EasingTestAndroid {
+ private val ZeroEpsilon = -(1.0f.ulp * 2.0f)
+ private val OneEpsilon = 1.0f + 1.0f.ulp * 2.0f
+
+ @Test
+ fun canSolveCubicForFractionsCloseToOne() {
+ // Only test curves defined in [0..1]
+ // For instance, EaseInOutBack is defined in a larger domain, so exclude it from the list
+ val curves =
+ listOf(
+ CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f),
+ Ease,
+ EaseIn,
+ EaseInBack,
+ EaseInCirc,
+ EaseInCubic,
+ EaseInExpo,
+ EaseInOut,
+ EaseInOutCirc,
+ EaseInOutCubic,
+ EaseInOutExpo,
+ EaseInOutQuad,
+ EaseInOutQuart,
+ EaseInOutQuint,
+ EaseInOutSine,
+ EaseInOutQuad,
+ EaseInOutQuart,
+ EaseInOutQuint,
+ EaseInSine,
+ EaseOut,
+ EaseOutCirc,
+ EaseOutCubic,
+ EaseOutExpo,
+ EaseOutQuad,
+ EaseOutQuart,
+ EaseOutQuint,
+ EaseOutSine
+ )
+
+ for (curve in curves) {
+ // Test the last 16 ulps until 1.0f
+ for (i in 0x3f7ffff0..0x3f7fffff) {
+ val fraction = floatFromBits(i)
+ val t = curve.transform(fraction)
+ assertTrue(
+ t in -ZeroEpsilon..OneEpsilon,
+ "f($fraction) = $t out of range for $curve | ${-ZeroEpsilon}..${OneEpsilon}"
+ )
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
new file mode 100644
index 0000000..ea43d32
--- /dev/null
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimatableTest.kt
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
+import androidx.kruth.assertThat
+import kotlin.math.abs
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+
+class AnimatableTest {
+ @Test
+ fun animateDecayTest() = runTest {
+ val from = 9f
+ val initialVelocity = 20f
+ val decaySpec = FloatExponentialDecaySpec()
+ val anim = DecayAnimation(decaySpec, initialValue = from, initialVelocity = initialVelocity)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ withContext(clock) {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..5000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ }
+ var playTimeMillis = 0L
+ val animatable = Animatable(9f)
+ val result =
+ animatable.animateDecay(20f, animationSpec = exponentialDecay()) {
+ assertTrue(isRunning)
+ assertEquals(anim.targetValue, targetValue)
+ assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
+ assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
+ playTimeMillis += interval
+ assertEquals(value, animatable.value, 0.0001f)
+ assertEquals(velocity, animatable.velocity, 0.0001f)
+ }
+ // After animation
+ assertEquals(anim.targetValue, animatable.value)
+ assertEquals(false, animatable.isRunning)
+ assertEquals(0f, animatable.velocity)
+ assertEquals(AnimationEndReason.Finished, result.endReason)
+ assertTrue(abs(result.endState.velocity) <= decaySpec.absVelocityThreshold)
+ }
+ }
+
+ @Test
+ fun animateToTest() = runTest {
+ val anim =
+ TargetBasedAnimation(
+ spring(dampingRatio = Spring.DampingRatioMediumBouncy),
+ Float.VectorConverter,
+ initialValue = 0f,
+ targetValue = 1f
+ )
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ val animatable = Animatable(0f)
+ withContext(clock) {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..5000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ }
+ var playTimeMillis = 0L
+ val result =
+ animatable.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
+ assertTrue(isRunning)
+ assertEquals(1f, targetValue)
+ assertEquals(anim.getValueFromMillis(playTimeMillis), value, 0.001f)
+ assertEquals(anim.getVelocityFromMillis(playTimeMillis), velocity, 0.001f)
+ playTimeMillis += interval
+ }
+ // After animation
+ assertEquals(anim.targetValue, animatable.value)
+ assertEquals(0f, animatable.velocity)
+ assertEquals(false, animatable.isRunning)
+ assertEquals(AnimationEndReason.Finished, result.endReason)
+ }
+ }
+
+ @Test
+ fun animateToGenericTypeTest() = runTest {
+ val from = Offset(666f, 321f)
+ val to = Offset(919f, 864f)
+ val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.x, it.y) },
+ convertFromVector = { Offset(it.v1, it.v2) }
+ )
+ val anim =
+ TargetBasedAnimation(tween(500), offsetToVector, initialValue = from, targetValue = to)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ val interval = 50
+ val animatable = Animatable(initialValue = from, typeConverter = offsetToVector)
+ coroutineScope {
+ withContext(clock) {
+ launch {
+ // Put in a bunch of frames 50 milliseconds apart
+ for (frameTimeMillis in 0..1000 step interval) {
+ clock.frame(frameTimeMillis * 1_000_000L)
+ delay(5)
+ }
+ }
+ launch {
+ // The first frame should start at 100ms
+ var playTimeMillis = 0L
+ animatable.animateTo(to, animationSpec = tween(500)) {
+ assertTrue(isRunning, "PlayTime Millis: $playTimeMillis")
+ assertEquals(to, targetValue)
+ val expectedValue = anim.getValueFromMillis(playTimeMillis)
+ assertEquals(
+ expectedValue.x,
+ value.x,
+ 0.001f,
+ "PlayTime Millis: $playTimeMillis"
+ )
+ assertEquals(
+ expectedValue.y,
+ value.y,
+ 0.001f,
+ "PlayTime Millis: $playTimeMillis"
+ )
+ playTimeMillis += interval
+
+ if (playTimeMillis == 300L) {
+ // Prematurely cancel the animation and check corresponding states
+ [email protected] {
+ stop()
+ assertFalse(isRunning)
+ assertEquals(playTimeMillis, 300L)
+ assertEquals(to, animatable.targetValue)
+ assertEquals(AnimationVector(0f, 0f), animatable.velocityVector)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun animateToWithInterruption() = runTest {
+ val anim1 =
+ TargetBasedAnimation(tween(200, easing = LinearEasing), Float.VectorConverter, 0f, 200f)
+ val clock = MyTestFrameClock()
+ val interval = 50
+ coroutineScope {
+ withContext(clock) {
+ val animatable = Animatable(0f)
+ var playTimeMillis by mutableStateOf(0L)
+
+ suspend fun createInterruption() {
+ val anim2 =
+ TargetBasedAnimation(
+ spring(),
+ Float.VectorConverter,
+ animatable.value,
+ 300f,
+ animatable.velocity
+ )
+ assertEquals(100L, playTimeMillis)
+ var firstFrame = true
+ val result2 =
+ animatable.animateTo(300f, spring()) {
+ // First frame will arrive with a timestamp of the time of
+ // interruption,
+ // which is 100ms. The subsequent frames will be consistent with
+ // what's
+ // tracked in `playTimeMillis`.
+ val playTime = if (firstFrame) 100L else playTimeMillis
+ assertTrue(isRunning)
+ assertEquals(300f, targetValue)
+ assertEquals(anim2.getValueFromMillis((playTime - 100)), value)
+ assertEquals(anim2.getVelocityFromMillis((playTime - 100)), velocity)
+ if (!firstFrame) {
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ } else {
+ firstFrame = false
+ }
+ }
+ assertFalse(animatable.isRunning)
+ assertEquals(AnimationEndReason.Finished, result2.endReason)
+ assertEquals(300f, animatable.targetValue)
+ assertEquals(300f, animatable.value)
+ assertEquals(0f, animatable.velocity)
+ }
+
+ clock.trySendFrame(0)
+ launch {
+ try {
+ animatable.animateTo(
+ 200f,
+ animationSpec = tween(200, easing = LinearEasing)
+ ) {
+ assertTrue(isRunning)
+ assertEquals(targetValue, 200f)
+ assertEquals(anim1.getValueFromMillis(playTimeMillis), value)
+ assertEquals(anim1.getVelocityFromMillis(playTimeMillis), velocity)
+
+ assertTrue(playTimeMillis <= 100)
+ if (playTimeMillis == 100L) {
+ [email protected] {
+ // No more new frame until the ongoing animation is
+ // canceled.
+ createInterruption()
+ }
+ } else {
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ }
+ }
+ } finally {
+ // At this point the previous animation on the Animatable has been
+ // canceled. Pump a frame to get the new animation going.
+ playTimeMillis += interval
+ clock.trySendFrame(playTimeMillis * 1_000_000L)
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testUpdateBounds() = runTest {
+ val animatable = Animatable(5f)
+ // Update bounds when *not* running
+ animatable.updateBounds(0f, 4f)
+ assertEquals(4f, animatable.value)
+ val clock = SuspendAnimationTest.TestFrameClock()
+ // Put two frames in clock
+ clock.frame(0L)
+ clock.frame(200 * 1_000_000L)
+
+ withContext(clock) {
+ animatable.animateTo(4f, tween(100)) {
+ if (animatable.upperBound == 4f) {
+ // Update bounds while running
+ animatable.updateBounds(-4f, 0f)
+ }
+ }
+ }
+ assertEquals(0f, animatable.value)
+
+ // Snap to value out of bounds
+ animatable.snapTo(animatable.lowerBound!! - 100f)
+ assertEquals(animatable.lowerBound!!, animatable.value)
+ }
+
+ @Test
+ fun testIntSize_alwaysWithinValidBounds() = runTest {
+ val animatable =
+ Animatable(
+ initialValue = IntSize(10, 10),
+ typeConverter = IntSize.VectorConverter,
+ visibilityThreshold = IntSize.VisibilityThreshold
+ )
+
+ val values = mutableListOf<IntSize>()
+
+ val clock = SuspendAnimationTest.TestFrameClock()
+
+ // Add frames to evaluate at
+ clock.frame(0L)
+ clock.frame(25L * 1_000_000L)
+ clock.frame(75L * 1_000_000L)
+ clock.frame(100L * 1_000_000L)
+
+ withContext(clock) {
+ // Animate linearly from -100 to 100
+ animatable.animateTo(
+ IntSize(100, 100),
+ keyframes {
+ durationMillis = 100
+ IntSize(-100, -100) at 0 using LinearEasing
+ }
+ ) {
+ values.add(value)
+ }
+ }
+
+ // The internal animation is expected to be: -100, -50, 50, 100. But for IntSize, we don't
+ // support negative values, so it's clamped to Zero
+ assertEquals(4, values.size)
+ assertEquals(IntSize.Zero, values[0])
+ assertEquals(IntSize.Zero, values[1])
+ assertEquals(IntSize(50, 50), values[2])
+ assertEquals(IntSize(100, 100), values[3])
+ }
+
+ @Test
+ fun animationResult_toString() {
+ val animatable =
+ AnimationResult(endReason = AnimationEndReason.Finished, endState = AnimationState(42f))
+ val string = animatable.toString()
+ assertThat(string).contains(AnimationResult::class.simpleName!!)
+ assertThat(string).contains("endReason=Finished")
+ assertThat(string).contains("endState=")
+ }
+
+ @Test
+ fun animationState_toString() {
+ val state =
+ AnimationState(
+ initialValue = 42f,
+ initialVelocity = 2f,
+ lastFrameTimeNanos = 4000L,
+ finishedTimeNanos = 3000L,
+ isRunning = true
+ )
+ val string = state.toString()
+ assertThat(string).contains(AnimationState::class.simpleName!!)
+ assertThat(string).contains("value=42.0")
+ assertThat(string).contains("velocity=2.0")
+ assertThat(string).contains("lastFrameTimeNanos=4000")
+ assertThat(string).contains("finishedTimeNanos=3000")
+ assertThat(string).contains("isRunning=true")
+ }
+
+ private class MyTestFrameClock : MonotonicFrameClock {
+ // Make the send non-blocking
+ private val frameCh = Channel<Long>(Channel.UNLIMITED)
+
+ suspend fun frame(frameTimeNanos: Long) {
+ frameCh.send(frameTimeNanos)
+ }
+
+ fun trySendFrame(frameTimeNanos: Long) {
+ frameCh.trySend(frameTimeNanos)
+ }
+
+ override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
+ onFrame(frameCh.receive())
+ }
+}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
index b09fcfd..e935a2c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,10 @@
package androidx.compose.animation.core
-import java.lang.Long.max
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.max
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class AnimationTest {
@Test
fun testSnap() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
index 6cfd113..286efb5 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationTestUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
index 263ac7b..f0834b0 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/AnimationVectorTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,10 @@
package androidx.compose.animation.core
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
-@RunWith(JUnit4::class)
class AnimationVectorTest {
@Test
fun testReset() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
similarity index 98%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
index a8a8e70..957eafb 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/ArcAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,17 +19,14 @@
import androidx.compose.animation.core.ArcMode.Companion.ArcAbove
import androidx.compose.animation.core.ArcMode.Companion.ArcBelow
import androidx.compose.animation.core.ArcMode.Companion.ArcLinear
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
/** Mostly tests some mathematical assumptions about arcs. */
@Suppress("JoinDeclarationAndAssignment") // Looks kinda messy
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class ArcAnimationTest {
// Animation parameters used in all tests
private val timeMillis = 1000
@@ -512,7 +509,7 @@
start = endTime * segment.startPercent,
end = endTime * segment.endPercent
)
- assertEquals("Graph on X dimension not equals", expectGraphX, arcSplineX)
+ assertEquals(expectGraphX, arcSplineX, message = "Graph on X dimension not equals")
val arcSplineY =
plot2DArcSpline(
@@ -521,7 +518,7 @@
start = endTime * segment.startPercent,
end = endTime * segment.endPercent
)
- assertEquals("Graph on Y dimension not equals", expectGraphY, arcSplineY)
+ assertEquals(expectGraphY, arcSplineY, message = "Graph on Y dimension not equals")
}
private inline fun <reified V : AnimationVector> VectorizedDurationBasedAnimationSpec<V>
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
index da144e4f..092be60 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DecayAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,13 @@
package androidx.compose.animation.core
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.abs
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
const val epsilon = 0.00001f
-@RunWith(JUnit4::class)
class DecayAnimationTest {
@Test
@@ -47,7 +45,7 @@
if (!finished) {
// Before the animation finishes, absolute velocity is above the threshold
- assertTrue(Math.abs(velocity) >= 2.0f)
+ assertTrue(abs(velocity) >= 2.0f)
assertEquals(value, animWrapper.getValueFromNanos(playTimeNanos), epsilon)
assertEquals(
velocity,
@@ -57,7 +55,7 @@
assertTrue(playTimeNanos < finishTimeNanos)
} else {
// When the animation is finished, expect absolute velocity < threshold
- assertTrue(Math.abs(velocity) < 2.0f)
+ assertTrue(abs(velocity) < 2.0f)
// Once the animation is finished, the value should not change any more
assertEquals(finishValue, animWrapper.getValueFromNanos(playTimeNanos), epsilon)
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
index 97feb08..52f232c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DelayedAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class DelayedAnimationTest {
@Test
fun duration() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
similarity index 87%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
index 8e87eb0..70c84dd 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/DurationScaleTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,18 +17,15 @@
package androidx.compose.animation.core
import androidx.compose.ui.MotionDurationScale
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
class DurationScaleTest {
@Test
- fun testAnimatable() = runBlocking {
+ fun testAnimatable() = runTest {
val clock = SuspendAnimationTest.TestFrameClock()
withContext(
clock +
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
similarity index 62%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
index 816bec4..c5e5ca3 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/EasingUnitTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,13 @@
package androidx.compose.animation.core
import androidx.compose.ui.util.floatFromBits
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import kotlin.math.ulp
-import org.junit.Assert.assertNotEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
-class EasingTest {
- private val ZeroEpsilon = -(1.0f.ulp * 2.0f)
- private val OneEpsilon = 1.0f + 1.0f.ulp * 2.0f
+class EasingUnitTest {
@Test
fun cubicBezierStartsAt0() {
@@ -105,52 +99,4 @@
assertTrue(t in 0.0f..1.0f)
}
}
-
- @Test
- fun canSolveCubicForFractionsCloseToOne() {
- // Only test curves defined in [0..1]
- // For instance, EaseInOutBack is defined in a larger domain, so exclude it from the list
- val curves =
- listOf(
- CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f),
- Ease,
- EaseIn,
- EaseInBack,
- EaseInCirc,
- EaseInCubic,
- EaseInExpo,
- EaseInOut,
- EaseInOutCirc,
- EaseInOutCubic,
- EaseInOutExpo,
- EaseInOutQuad,
- EaseInOutQuart,
- EaseInOutQuint,
- EaseInOutSine,
- EaseInOutQuad,
- EaseInOutQuart,
- EaseInOutQuint,
- EaseInSine,
- EaseOut,
- EaseOutCirc,
- EaseOutCubic,
- EaseOutExpo,
- EaseOutQuad,
- EaseOutQuart,
- EaseOutQuint,
- EaseOutSine
- )
-
- for (curve in curves) {
- // Test the last 16 ulps until 1.0f
- for (i in 0x3f7ffff0..0x3f7fffff) {
- val fraction = floatFromBits(i)
- val t = curve.transform(fraction)
- assertTrue(
- "f($fraction) = $t out of range for $curve",
- t in -ZeroEpsilon..OneEpsilon
- )
- }
- }
- }
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
similarity index 93%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
index f4a65b4..4c11a00 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/IsInfiniteTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class IsInfiniteTest {
@Test
fun testTweenIsFinite() {
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
index ad48e92..ca6754d 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
@@ -1,21 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,14 +16,11 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
class KeyframeAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
similarity index 95%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
index 97b2ea2..d4ebe2a 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeArcAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,15 +20,12 @@
import androidx.compose.animation.core.ArcMode.Companion.ArcBelow
import androidx.compose.animation.core.ArcMode.Companion.ArcLinear
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
@Suppress("JoinDeclarationAndAssignment") // Looks kinda messy
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class KeyframeArcAnimationTest {
private val timeMillis = 3000
private val initialValue = 0f
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
index fcae260..31acd22 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,12 @@
package androidx.compose.animation.core
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
@OptIn(ExperimentalAnimationSpecApi::class)
-@RunWith(JUnit4::class)
class KeyframeSplineAnimationTest {
/** See [MonoSplineTest] to test the interpolation curves. */
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
similarity index 90%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
index dd3d23d..3b514b1 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,11 @@
package androidx.compose.animation.core
-import java.util.Arrays
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class MonoSplineTest {
@Test
fun testCurveFit01() {
@@ -108,14 +106,14 @@
var maxY = y[0]
var ret = ""
for (i in x.indices) {
- minX = Math.min(minX, x[i])
- maxX = Math.max(maxX, x[i])
- minY = Math.min(minY, y[i])
- maxY = Math.max(maxY, y[i])
+ minX = min(minX, x[i])
+ maxX = max(maxX, x[i])
+ minY = min(minY, y[i])
+ maxY = max(maxY, y[i])
}
val c = Array(dimy) { CharArray(dimx) }
for (i in 0 until dimy) {
- Arrays.fill(c[i], ' ')
+ repeat(c[i].size) { c[i][it] = ' ' }
}
val dimx1 = dimx - 1
val dimy1 = dimy - 1
@@ -134,14 +132,14 @@
v = (v * 1000 + 0.5).toInt() / 1000f
ret +=
if (i % 5 == 0 || i == c.size - 1) {
- "|" + String(c[i]) + "| " + v + "\n"
+ "|" + c[i].concatToString() + "| " + v + "\n"
} else {
- "|" + String(c[i]) + "|\n"
+ "|" + c[i].concatToString() + "|\n"
}
}
val minStr = ((minX * 1000 + 0.5).toInt() / 1000f).toString()
val maxStr = ((maxX * 1000 + 0.5).toInt() / 1000f).toString()
- var s = minStr + String(CharArray(dimx) { ' ' })
+ var s = minStr + CharArray(dimx) { ' ' }.concatToString()
s = s.substring(0, dimx - maxStr.length + 2) + maxStr + '\n'
return (ret + s).trimIndent()
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
similarity index 97%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
index 6adbe32..3ac7408 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/PhysicsAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,12 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
+import androidx.kruth.assertThat
import kotlin.math.sign
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-@RunWith(JUnit4::class)
class PhysicsAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
similarity index 96%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
index 4f29357..13ddd1c 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/RepeatableAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,14 +16,11 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
-@RunWith(JUnit4::class)
class RepeatableAnimationTest {
private val DelayedAnimation =
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
similarity index 82%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
index 3ba3606..ce0efae 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SnapAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class SnapAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
similarity index 93%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
index b038c33..0746cf5 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/SuspendAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,20 +18,17 @@
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.ui.geometry.Offset
-import junit.framework.TestCase.assertEquals
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-@RunWith(JUnit4::class)
class SuspendAnimationTest {
@Test
- fun animateFloatVariantTest() = runBlocking {
+ fun animateFloatVariantTest() = runTest {
val anim =
TargetBasedAnimation(
spring(dampingRatio = Spring.DampingRatioMediumBouncy),
@@ -58,7 +55,7 @@
}
@Test
- fun animateGenericsVariantTest() = runBlocking {
+ fun animateGenericsVariantTest() = runTest {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
@@ -86,7 +83,7 @@
}
@Test
- fun animateDecayTest() = runBlocking {
+ fun animateDecayTest() = runTest {
val from = 666f
val velocity = 999f
val anim =
@@ -115,7 +112,7 @@
@Test
fun animateToTest() {
- runBlocking {
+ runTest {
val from = Offset(666f, 321f)
val to = Offset(919f, 864f)
val offsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
@@ -179,7 +176,7 @@
}
@Test
- fun animateDecayOnAnimationStateTest() = runBlocking {
+ fun animateDecayOnAnimationStateTest() = runTest {
val from = 9f
val initialVelocity = 20f
val anim =
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
similarity index 91%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
index 42c9d1e..ed55738 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TweenAnimationTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import androidx.kruth.assertThat
+import kotlin.test.Test
-@RunWith(JUnit4::class)
class TweenAnimationTest {
@Test
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
similarity index 84%
rename from compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
rename to compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
index 81b503b..db2f485 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
+++ b/compose/animation/animation-core/src/commonTest/kotlin/androidx/compose/animation/core/TypeConverterTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,9 @@
package androidx.compose.animation.core
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlin.test.Test
+import kotlin.test.assertEquals
-@RunWith(JUnit4::class)
class TypeConverterTest {
@Test
fun testFloatToVectorConverter() {
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index f968628..c7595d2 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -127,10 +127,6 @@
samples(project(":compose:animation:animation:animation-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.compose.animation"
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 49c87a5..d607c7f 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -33,11 +33,7 @@
implementation(project(":compose:ui:ui-tooling-preview"))
implementation project(':compose:material3:material3')
implementation project(":navigation:navigation-compose")
- debugImplementation(project(":compose:ui:ui-tooling"))
-}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
+ implementation(project(":compose:ui:ui-tooling"))
}
android {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
index 375751a..803c694 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
@@ -70,7 +70,10 @@
val avatar = remember {
movableContentWithReceiverOf<SceneScope> {
Box(
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xffff6f69), RoundedCornerShape(20))
.fillMaxSize()
)
@@ -81,7 +84,10 @@
movableContentWithReceiverOf<SceneScope, @Composable () -> Unit> { child ->
Surface(
modifier =
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xfffdedac)),
color = Color(0xfffdedac),
shape = RoundedCornerShape(10.dp)
@@ -166,44 +172,53 @@
val progress: Float
}
-context(LookaheadScope)
@SuppressLint("PrimitiveInCollection")
-fun <T> Modifier.sharedElementBasedOnProgress(provider: ProgressProvider<T>) = composed {
- val sizeMap = remember { mutableMapOf<T, IntSize>() }
- val offsetMap = remember { mutableMapOf<T, Offset>() }
- val calculateSize: (IntSize) -> IntSize = {
- sizeMap[provider.targetState] = it
- val (width, height) =
- lerp(
- sizeMap[provider.initialState]!!.toSize(),
- sizeMap[provider.targetState]!!.toSize(),
- provider.progress
- )
- IntSize(width.roundToInt(), height.roundToInt())
- }
-
- val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
- with(it) {
- coordinates?.let {
- offsetMap[provider.targetState] =
- lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- val lerpedOffset =
+fun <T> Modifier.sharedElementBasedOnProgress(
+ lookaheadScope: LookaheadScope,
+ provider: ProgressProvider<T>
+) =
+ with(lookaheadScope) {
+ composed {
+ val sizeMap = remember { mutableMapOf<T, IntSize>() }
+ val offsetMap = remember { mutableMapOf<T, Offset>() }
+ val calculateSize: (IntSize) -> IntSize = {
+ sizeMap[provider.targetState] = it
+ val (width, height) =
lerp(
- offsetMap[provider.initialState]!!,
- offsetMap[provider.targetState]!!,
+ sizeMap[provider.initialState]!!.toSize(),
+ sizeMap[provider.targetState]!!.toSize(),
provider.progress
)
- val currentOffset = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
- (lerpedOffset - currentOffset).round()
- } ?: IntOffset(0, 0)
+ IntSize(width.roundToInt(), height.roundToInt())
+ }
+
+ val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
+ with(it) {
+ coordinates?.let {
+ offsetMap[provider.targetState] =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it)
+ val lerpedOffset =
+ lerp(
+ offsetMap[provider.initialState]!!,
+ offsetMap[provider.targetState]!!,
+ provider.progress
+ )
+ val currentOffset =
+ lookaheadScopeCoordinates.localPositionOf(
+ it,
+ androidx.compose.ui.geometry.Offset.Zero
+ )
+ (lerpedOffset - currentOffset).round()
+ } ?: IntOffset(0, 0)
+ }
+ }
+ this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
+ val (width, height) = calculateSize(lookaheadSize)
+ val animatedConstraints = androidx.compose.ui.unit.Constraints.fixed(width, height)
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(calculateOffset(this@approachLayout))
+ }
+ }
}
}
- this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
- val (width, height) = calculateSize(lookaheadSize)
- val animatedConstraints = Constraints.fixed(width, height)
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- placeable.place(calculateOffset(this@approachLayout))
- }
- }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
index 5562c9c..0879163 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
@@ -71,7 +71,9 @@
Box(Modifier.padding(start = 50.dp, top = 200.dp, bottom = 100.dp)) {
val icon = remember { movableContentOf<Boolean> { MyIcon(it) } }
val title = remember {
- movableContentOf<Boolean> { Title(visible = it, Modifier.animatePosition()) }
+ movableContentOf<Boolean> {
+ Title(visible = it, Modifier.animatePosition(this@LookaheadScope))
+ }
}
val details = remember { movableContentOf<Boolean> { Details(visible = it) } }
@@ -129,39 +131,41 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalAnimatableApi::class)
@SuppressLint("UnnecessaryComposedModifier")
-fun Modifier.animatePosition(): Modifier = composed {
- val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val coroutineScope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = { false },
- isPlacementApproachInProgress = {
- offsetAnimation.updateTarget(
- lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow)
- )
- !offsetAnimation.isIdle
- }
- ) { measurable, constraints ->
- measurable.measure(constraints).run {
- layout(width, height) {
- val (x, y) =
- coordinates?.let { coordinates ->
- val origin = this.lookaheadScopeCoordinates
- val animOffset =
- offsetAnimation.updateTarget(
- origin.localLookaheadPositionOf(coordinates).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow),
- )
- val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
- animOffset - currentOffset.round()
- } ?: IntOffset.Zero
- place(x, y)
+fun Modifier.animatePosition(lookaheadScope: LookaheadScope): Modifier =
+ with(lookaheadScope) {
+ composed {
+ val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
+ val coroutineScope = rememberCoroutineScope()
+ this.approachLayout(
+ isMeasurementApproachInProgress = { false },
+ isPlacementApproachInProgress = {
+ offsetAnimation.updateTarget(
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow)
+ )
+ !offsetAnimation.isIdle
+ }
+ ) { measurable, constraints ->
+ measurable.measure(constraints).run {
+ layout(width, height) {
+ val (x, y) =
+ coordinates?.let { coordinates ->
+ val origin = this.lookaheadScopeCoordinates
+ val animOffset =
+ offsetAnimation.updateTarget(
+ origin.localLookaheadPositionOf(coordinates).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow),
+ )
+ val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
+ animOffset - currentOffset.round()
+ } ?: IntOffset.Zero
+ place(x, y)
+ }
+ }
}
}
}
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index a02fc9a..d294736 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -18,10 +18,6 @@
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateBounds
-import androidx.compose.animation.core.DeferredTargetAnimation
-import androidx.compose.animation.core.ExperimentalAnimatableApi
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
import androidx.compose.animation.demos.fancy.AnimatedDotsDemo
import androidx.compose.animation.demos.statetransition.InfiniteProgress
import androidx.compose.animation.demos.statetransition.InfinitePulsingHeart
@@ -43,22 +39,14 @@
import androidx.compose.runtime.movableContentWithReceiverOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@@ -157,37 +145,5 @@
}
}
-context(LookaheadScope)
-@OptIn(ExperimentalAnimatableApi::class)
-fun Modifier.animateBoundsInScope(): Modifier = composed {
- val sizeAnim = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
- val offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val scope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = {
- sizeAnim.updateTarget(it, scope)
- !sizeAnim.isIdle
- },
- isPlacementApproachInProgress = {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- offsetAnim.updateTarget(target.round(), scope, spring())
- !offsetAnim.isIdle
- }
- ) { measurable, _ ->
- val (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope, spring())
- measurable.measure(Constraints.fixed(animWidth, animHeight)).run {
- layout(width, height) {
- coordinates?.let {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
- val animOffset = offsetAnim.updateTarget(target, scope, spring())
- val current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
- val (x, y) = animOffset - current
- place(x, y)
- } ?: place(0, 0)
- }
- }
- }
-}
-
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
index 2ea3bd2..ed920ea 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
@@ -55,13 +55,15 @@
Text(if (shouldAnimate) "Stop animating bounds" else "Animate bounds")
}
SubcomposeLayout(
- Modifier.background(colors[3]).conditionallyAnimateBounds(shouldAnimate)
+ Modifier.background(colors[3])
+ .conditionallyAnimateBounds(this@LookaheadScope, shouldAnimate)
) {
val constraints = it.copy(minWidth = 0)
val placeable =
subcompose(0) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -75,6 +77,7 @@
subcompose(1) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -91,6 +94,7 @@
Box(
Modifier.width(totalWidth.toDp())
.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.height(if (isWide) 150.dp else 70.dp)
)
@@ -108,12 +112,12 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalSharedTransitionApi::class)
private fun Modifier.conditionallyAnimateBounds(
+ lookaheadScope: LookaheadScope,
shouldAnimate: Boolean,
modifier: Modifier = Modifier
-) = if (shouldAnimate) this.animateBounds(this@LookaheadScope, modifier) else this.then(modifier)
+) = if (shouldAnimate) this.animateBounds(lookaheadScope, modifier) else this.then(modifier)
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff2a9d84), Color(0xff264653))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index 1b94fc1..ecd48cd 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -106,6 +106,9 @@
layout(placeable.width, placeable.height) {
val (x, y) =
offsetAnimation.updateTargetBasedOnCoordinates(
+ this@SceneScope,
+ this@layout,
+ this@with,
spring(stiffness = Spring.StiffnessMediumLow),
)
coordinates?.let {
@@ -153,25 +156,32 @@
}
}
-context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
@OptIn(ExperimentalAnimatableApi::class)
internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+ lookaheadScope: LookaheadScope,
+ placementScope: Placeable.PlacementScope,
+ coroutineScope: CoroutineScope,
animationSpec: FiniteAnimationSpec<IntOffset>,
): IntOffset {
- coordinates?.let { coordinates ->
- with(this@PlacementScope) {
- val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
- val animOffset =
- updateTarget(
- targetOffset.round(),
- this@CoroutineScope,
- animationSpec,
- )
- val current =
- lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
- return (animOffset - current)
+ with(lookaheadScope) {
+ with(placementScope) {
+ coordinates?.let { coordinates ->
+ with(placementScope) {
+ val targetOffset =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+ val animOffset =
+ updateTarget(
+ targetOffset.round(),
+ coroutineScope,
+ animationSpec,
+ )
+ val current =
+ lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
+ return (animOffset - current)
+ }
+ }
+
+ return IntOffset.Zero
}
}
-
- return IntOffset.Zero
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
index 25b32da..78916a7 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
@@ -90,180 +90,208 @@
) {
// TODO: Double check on container transform scrolling
if (it != null) {
- DetailView(model = model, selected = it, model.items[6])
+ DetailView(
+ this@AnimatedContent,
+ this@SharedTransitionLayout,
+ model = model,
+ selected = it,
+ model.items[6]
+ )
} else {
- GridView(model = model)
+ GridView(this@AnimatedContent, this@SharedTransitionLayout, model = model)
}
}
}
}
-context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
-fun Details(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
- .fillMaxHeight()
- .wrapContentHeight(Alignment.Top)
- .fillMaxWidth()
- .background(Color.White)
- .padding(start = 10.dp, end = 10.dp)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Column {
- Spacer(Modifier.size(20.dp))
- Text(
- kitty.name,
- fontSize = 25.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Text(
- kitty.breed,
- fontSize = 22.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
+fun Details(
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
+ .fillMaxHeight()
+ .wrapContentHeight(Alignment.Top)
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(start = 10.dp, end = 10.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column {
+ Spacer(Modifier.size(20.dp))
+ Text(
+ kitty.name,
+ fontSize = 25.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Text(
+ kitty.breed,
+ fontSize = 22.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
+ Spacer(Modifier.weight(1f))
+ Icon(
+ Icons.Outlined.Favorite,
+ contentDescription = null,
+ Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
)
Spacer(Modifier.size(10.dp))
}
- Spacer(Modifier.weight(1f))
- Icon(
- Icons.Outlined.Favorite,
- contentDescription = null,
- Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
+ Box(
+ modifier =
+ Modifier.padding(bottom = 10.dp)
+ .height(2.dp)
+ .fillMaxWidth()
+ .background(Color(0xffeeeeee))
)
- Spacer(Modifier.size(10.dp))
+ Text(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
+ " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
+ " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
+ " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
+ " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
+ "\n" +
+ "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
+ " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
+ " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
+ " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
+ " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
+ " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
+ " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
+ " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
+ " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
+ " mauris at urna dictum ornare.\n" +
+ "\n" +
+ "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
+ " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
+ " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
+ " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
+ " Nullam mattis luctus orci at pulvinar.\n" +
+ "\n" +
+ "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
+ " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
+ " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
+ " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
+ " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
+ " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
+ " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
+ " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
+ " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
+ " lobortis turpis.\n" +
+ "\n",
+ color = Color.Gray,
+ fontSize = 15.sp,
+ )
}
- Box(
- modifier =
- Modifier.padding(bottom = 10.dp)
- .height(2.dp)
- .fillMaxWidth()
- .background(Color(0xffeeeeee))
- )
- Text(
- text =
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
- " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
- " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
- " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
- " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
- "\n" +
- "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
- " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
- " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
- " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
- " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
- " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
- " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
- " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
- " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
- " mauris at urna dictum ornare.\n" +
- "\n" +
- "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
- " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
- " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
- " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
- " Nullam mattis luctus orci at pulvinar.\n" +
- "\n" +
- "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
- " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
- " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
- " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
- " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
- " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
- " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
- " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
- " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
- " lobortis turpis.\n" +
- "\n",
- color = Color.Gray,
- fontSize = 15.sp,
- )
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
-@Suppress("UNUSED_PARAMETER")
@Composable
-fun DetailView(model: MyModel, selected: Kitty, next: Kitty?) {
- Column(
- Modifier.clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null
- ) {
- model.selected = null
- }
- .sharedBounds(
- rememberSharedContentState(key = "container + ${selected.id}"),
- this@AnimatedVisibilityScope,
- fadeIn(),
- fadeOut(),
- resizeMode = ScaleToBounds(ContentScale.Crop),
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
- )
- ) {
- Row(Modifier.fillMaxHeight(0.5f)) {
- Image(
- painter = painterResource(selected.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.padding(10.dp)
- .sharedElement(
- rememberSharedContentState(key = selected.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .fillMaxHeight()
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- if (next != null) {
+fun DetailView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel,
+ selected: Kitty,
+ next: Kitty?
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ model.selected = null
+ }
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${selected.id}"),
+ animatedVisibilityScope,
+ fadeIn(),
+ fadeOut(),
+ resizeMode = ScaleToBounds(ContentScale.Crop),
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
+ )
+ ) {
+ Row(Modifier.fillMaxHeight(0.5f)) {
Image(
- painter = painterResource(next.photoResId),
+ painter = painterResource(selected.photoResId),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
- Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
- .fillMaxWidth()
+ Modifier.padding(10.dp)
+ .sharedElement(
+ rememberSharedContentState(key = selected.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
.fillMaxHeight()
+ .aspectRatio(1f)
.clip(RoundedCornerShape(20.dp))
- .blur(10.dp)
)
+ if (next != null) {
+ Image(
+ painter = painterResource(next.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(20.dp))
+ .blur(10.dp)
+ )
+ }
}
+ Details(sharedTransitionScope, animatedVisibilityScope, kitty = selected)
}
- Details(kitty = selected)
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun GridView(model: MyModel) {
- Box(Modifier.background(lessVibrantPurple)) {
- Box(
- Modifier.padding(20.dp)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
- .animateEnterExit(fadeIn(), fadeOut())
- ) {
- SearchBar()
- }
- LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- contentPadding = PaddingValues(top = 90.dp)
- ) {
- items(6) {
- Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
- KittyItem(model.items[it])
+fun GridView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel
+) {
+ with(animatedVisibilityScope) {
+ with(sharedTransitionScope) {
+ Box(Modifier.background(lessVibrantPurple)) {
+ Box(
+ Modifier.padding(20.dp)
+ .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
+ .animateEnterExit(fadeIn(), fadeOut())
+ ) {
+ SearchBar()
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ contentPadding = PaddingValues(top = 90.dp)
+ ) {
+ items(6) {
+ Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
+ KittyItem(
+ animatedVisibilityScope,
+ sharedTransitionScope,
+ model.items[it]
+ )
+ }
+ }
}
}
}
@@ -284,54 +312,59 @@
var selected: Kitty? by mutableStateOf(null)
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun KittyItem(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container + ${kitty.id}"),
- this@AnimatedVisibilityScope,
+fun KittyItem(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${kitty.id}"),
+ animatedVisibilityScope,
+ )
+ .background(Color.White, RoundedCornerShape(20.dp))
+ ) {
+ Image(
+ painter = painterResource(kitty.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.sharedElement(
+ rememberSharedContentState(key = kitty.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(20.dp))
)
- .background(Color.White, RoundedCornerShape(20.dp))
- ) {
- Image(
- painter = painterResource(kitty.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.sharedElement(
- rememberSharedContentState(key = kitty.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- Spacer(Modifier.size(10.dp))
- Text(
- kitty.name,
- fontSize = 18.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(5.dp))
- Text(
- kitty.breed,
- fontSize = 15.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(10.dp))
+ Spacer(Modifier.size(10.dp))
+ Text(
+ kitty.name,
+ fontSize = 18.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(5.dp))
+ Text(
+ kitty.breed,
+ fontSize = 15.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
index 9a3c30d..3cd4c47 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
@@ -83,111 +83,114 @@
)
) {
SharedTransitionLayout {
- HomePage(!showExpandedCard)
- ExpandedCard(showExpandedCard)
+ HomePage(this@SharedTransitionLayout, !showExpandedCard)
+ ExpandedCard(this@SharedTransitionLayout, showExpandedCard)
}
}
}
-context(SharedTransitionScope)
@Composable
-fun HomePage(showCard: Boolean) {
- Box(Modifier.fillMaxSize().background(Color.White)) {
- Column {
- SearchBarAndTabs()
- Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
- androidx.compose.animation.AnimatedVisibility(visible = showCard) {
- Column(
- Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- clipInOverlayDuringTransition =
- OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(color = cardBackgroundColor),
- ) {
- Box {
- Column {
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState(key = "quiet_night"),
- this@AnimatedVisibility,
- zIndexInOverlay = 0.5f,
- ),
- contentScale = ContentScale.FillWidth
+fun HomePage(sharedTransitionScope: SharedTransitionScope, showCard: Boolean) {
+ with(sharedTransitionScope) {
+ Box(Modifier.fillMaxSize().background(Color.White)) {
+ Column {
+ SearchBarAndTabs()
+ Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
+ androidx.compose.animation.AnimatedVisibility(visible = showCard) {
+ Column(
+ Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ clipInOverlayDuringTransition =
+ OverlayClip(RoundedCornerShape(20.dp))
)
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 20.dp, end = 20.dp, top = 20.dp)
- .height(14.dp)
- .sharedElement(
- rememberSharedContentState(key = "longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(
- align = Alignment.Top,
- unbounded = true
- )
- .skipToLookaheadSize(),
- )
- }
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(color = cardBackgroundColor),
+ ) {
+ Box {
+ Column {
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState(key = "quiet_night"),
+ this@AnimatedVisibility,
+ zIndexInOverlay = 0.5f,
+ ),
+ contentScale = ContentScale.FillWidth
+ )
+ Text(
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ .height(14.dp)
+ .sharedElement(
+ rememberSharedContentState(key = "longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(
+ align = Alignment.Top,
+ unbounded = true
+ )
+ .skipToLookaheadSize(),
+ )
+ }
- Text(
- text = title,
- fontFamily = FontFamily.Default,
- color = Color.White,
- fontSize = 20.sp,
- modifier =
- Modifier.fillMaxWidth()
- .align(Alignment.BottomCenter)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn(tween(1000)) + slideInVertically { -it / 3 },
- fadeOut(tween(50)) + slideOutVertically { -it / 3 }
- )
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(
- Color.Transparent,
- Color.Black,
- Color.Transparent
+ Text(
+ text = title,
+ fontFamily = FontFamily.Default,
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .renderInSharedTransitionScopeOverlay(
+ zIndexInOverlay = 1f
+ )
+ .animateEnterExit(
+ fadeIn(tween(1000)) + slideInVertically { -it / 3 },
+ fadeOut(tween(50)) + slideOutVertically { -it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(
+ Color.Transparent,
+ Color.Black,
+ Color.Transparent
+ )
)
)
- )
- .padding(20.dp),
+ .padding(20.dp),
+ )
+ }
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElementWithCallerManagedVisibility(
+ rememberSharedContentState(key = "install_bar"),
+ showCard,
+ )
)
}
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElementWithCallerManagedVisibility(
- rememberSharedContentState(key = "install_bar"),
- showCard,
- )
- )
}
}
+ Cluster()
}
- Cluster()
+ Image(
+ painterResource(R.drawable.navigation_bar),
+ contentDescription = null,
+ Modifier.fillMaxWidth().align(Alignment.BottomCenter),
+ contentScale = ContentScale.FillWidth
+ )
}
- Image(
- painterResource(R.drawable.navigation_bar),
- contentDescription = null,
- Modifier.fillMaxWidth().align(Alignment.BottomCenter),
- contentScale = ContentScale.FillWidth
- )
}
}
@@ -221,97 +224,98 @@
}
}
-context(SharedTransitionScope)
@Composable
-fun ExpandedCard(visible: Boolean) {
- AnimatedVisibility(
- visible = visible,
- Modifier.fillMaxSize(),
- enter = fadeIn(),
- exit = fadeOut()
- ) {
- Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
- Column(
- Modifier.align(Alignment.Center)
- .padding(20.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- enter = EnterTransition.None,
- exit = ExitTransition.None,
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(cardBackgroundColor)
- ) {
+fun ExpandedCard(sharedTransitionScope: SharedTransitionScope, visible: Boolean) {
+ with(sharedTransitionScope) {
+ AnimatedVisibility(
+ visible = visible,
+ Modifier.fillMaxSize(),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
Column(
- Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn() + slideInVertically { it / 3 },
- fadeOut() + slideOutVertically { it / 3 }
+ Modifier.align(Alignment.Center)
+ .padding(20.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ enter = EnterTransition.None,
+ exit = ExitTransition.None,
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
)
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(Color.Transparent, Color.Black, Color.Transparent)
- )
- )
- .padding(start = 20.dp, end = 20.dp),
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(cardBackgroundColor)
) {
- Text(
- text = "Lorem ipsum",
- Modifier.padding(top = 20.dp, bottom = 10.dp)
- .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
- .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
- color = Color.Black,
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Bold,
- fontSize = 15.sp
+ Column(
+ Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
+ .animateEnterExit(
+ fadeIn() + slideInVertically { it / 3 },
+ fadeOut() + slideOutVertically { it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(Color.Transparent, Color.Black, Color.Transparent)
+ )
+ )
+ .padding(start = 20.dp, end = 20.dp),
+ ) {
+ Text(
+ text = "Lorem ipsum",
+ Modifier.padding(top = 20.dp, bottom = 10.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
+ .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+ color = Color.Black,
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ )
+ Text(
+ text = title,
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ )
+ }
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState("quiet_night"),
+ this@AnimatedVisibility,
+ ),
+ contentScale = ContentScale.FillWidth
)
+
Text(
- text = title,
- color = Color.White,
- fontSize = 30.sp,
- modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 15.dp, end = 10.dp, top = 10.dp)
+ .height(50.dp)
+ .sharedElement(
+ rememberSharedContentState("longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(align = Alignment.Top, unbounded = true)
+ .skipToLookaheadSize(),
+ )
+
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElement(
+ rememberSharedContentState("install_bar"),
+ this@AnimatedVisibility,
+ )
)
}
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState("quiet_night"),
- this@AnimatedVisibility,
- ),
- contentScale = ContentScale.FillWidth
- )
-
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 15.dp, end = 10.dp, top = 10.dp)
- .height(50.dp)
- .sharedElement(
- rememberSharedContentState("longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(align = Alignment.Top, unbounded = true)
- .skipToLookaheadSize(),
- )
-
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElement(
- rememberSharedContentState("install_bar"),
- this@AnimatedVisibility,
- )
- )
}
}
}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
index c69c971b..2b199c0 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimateBoundsTest.kt
@@ -183,10 +183,12 @@
// Wait until first animated frame, for test stability
do {
rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
} while (expectedSmallSize.round() == boxSize)
// Advance to approx. the middle of the animation (minus the first animated frame)
rule.mainClock.advanceTimeBy(durationMillis / 2L - frameTime)
+ rule.waitForIdle()
val expectedMidIntSize = (expectedLargeSize + expectedSmallSize).times(0.5f).round()
assertEquals(expectedMidIntSize, boxSize)
diff --git a/compose/benchmark-utils/build.gradle b/compose/benchmark-utils/build.gradle
index b994260..fd97201 100644
--- a/compose/benchmark-utils/build.gradle
+++ b/compose/benchmark-utils/build.gradle
@@ -33,17 +33,17 @@
dependencies {
api("androidx.activity:activity:1.2.0")
api(project(":compose:test-utils"))
- api(projectOrArtifact(":benchmark:benchmark-junit4"))
+ api(project(":benchmark:benchmark-junit4"))
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
implementation(project(":tracing:tracing-ktx"))
implementation(libs.testRules)
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ compileOnly(project(":compose:ui:ui-android-stubs"))
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ContextualFlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ContextualFlowLayout.kt
index 0a4280f..a22b962 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ContextualFlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ContextualFlowLayout.kt
@@ -334,7 +334,7 @@
override val maxHeight: Dp
) : RowScope by RowScopeInstance, ContextualFlowRowScope {
override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
- requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
+ requirePrecondition(fraction in 0.0f..1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(
@@ -353,7 +353,7 @@
override val maxHeightInLine: Dp
) : ColumnScope by ColumnScopeInstance, ContextualFlowColumnScope {
override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
- requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
+ requirePrecondition(fraction in 0.0f..1.0f) {
"invalid fraction $fraction; must be >= 0 and <= 1.0"
}
return this.then(
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
index 3b4aff7..05f1276 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
@@ -44,6 +44,8 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEachIndexed
import kotlin.math.ceil
import kotlin.math.max
@@ -513,8 +515,8 @@
endIndex: Int
): MeasureResult {
with(measureScope) {
- var width: Int
- var height: Int
+ val width: Int
+ val height: Int
if (isHorizontal) {
width = mainAxisLayoutSize
height = crossAxisLayoutSize
@@ -522,6 +524,12 @@
width = crossAxisLayoutSize
height = mainAxisLayoutSize
}
+ val layoutDirection =
+ if (isHorizontal) {
+ LayoutDirection.Ltr
+ } else {
+ layoutDirection
+ }
return layout(width, height) {
val crossAxisLineOffset = crossAxisOffset?.get(currentLineIndex) ?: 0
for (i in startIndex until endIndex) {
@@ -529,7 +537,6 @@
val crossAxisPosition =
getCrossAxisPosition(
placeable,
- placeable.rowColumnParentData,
crossAxisLayoutSize,
layoutDirection,
beforeCrossAxisAlignmentLine
@@ -546,20 +553,15 @@
fun getCrossAxisPosition(
placeable: Placeable,
- rowColumnParentData: RowColumnParentData?,
crossAxisLayoutSize: Int,
layoutDirection: LayoutDirection,
beforeCrossAxisAlignmentLine: Int
): Int {
- val childCrossAlignment = rowColumnParentData?.crossAxisAlignment ?: crossAxisAlignment
+ val childCrossAlignment =
+ placeable.rowColumnParentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize - placeable.crossAxisSize(),
- layoutDirection =
- if (isHorizontal) {
- LayoutDirection.Ltr
- } else {
- layoutDirection
- },
+ layoutDirection = layoutDirection,
placeable = placeable,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
)
@@ -888,8 +890,8 @@
if (children.isEmpty()) {
return 0
}
- val mainAxisSizes = IntArray(children.size) { 0 }
- val crossAxisSizes = IntArray(children.size) { 0 }
+ val mainAxisSizes = IntArray(children.size)
+ val crossAxisSizes = IntArray(children.size)
for (index in children.indices) {
val child = children[index]
@@ -1246,7 +1248,7 @@
positionInLine = if (willFitLine) nextIndexInLine else 0,
maxMainAxisSize =
if (willFitLine) {
- (leftOver - spacing).coerceAtLeast(0)
+ (leftOver - spacing).fastCoerceAtLeast(0)
} else {
mainAxisMax
}
@@ -1256,7 +1258,7 @@
leftOverCrossAxis
} else {
(leftOverCrossAxis - currentLineCrossAxisSize - crossAxisSpacing)
- .coerceAtLeast(0)
+ .fastCoerceAtLeast(0)
}
.toDp()
)
@@ -1330,8 +1332,8 @@
}
val arrayOfPlaceables: Array<Placeable?> = Array(measurables.size) { placeables[it] }
- val crossAxisOffsets = IntArray(endBreakLineList.size) { 0 }
- val crossAxisSizesArray = IntArray(endBreakLineList.size) { 0 }
+ val crossAxisOffsets = IntArray(endBreakLineList.size)
+ val crossAxisSizesArray = IntArray(endBreakLineList.size)
crossAxisTotalSize = 0
var startIndex = 0
@@ -1352,7 +1354,7 @@
crossAxisOffsets,
currentLineIndex
)
- var mainAxisSize: Int
+ val mainAxisSize: Int
if (measurePolicy.isHorizontal) {
mainAxisSize = result.width
crossAxisSize = result.height
@@ -1460,7 +1462,7 @@
val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
totalCrossAxisSize += totalCrossAxisSpacing
totalCrossAxisSize =
- totalCrossAxisSize.coerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
+ totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
arrange(totalCrossAxisSize, crossAxisSizes, outPosition)
}
} else {
@@ -1468,13 +1470,13 @@
val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
totalCrossAxisSize += totalCrossAxisSpacing
totalCrossAxisSize =
- totalCrossAxisSize.coerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
+ totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
arrange(totalCrossAxisSize, crossAxisSizes, layoutDirection, outPosition)
}
}
val finalMainAxisTotalSize =
- mainAxisTotalSize.coerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
+ mainAxisTotalSize.fastCoerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
val layoutWidth: Int
val layoutHeight: Int
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
index 8482560..0a9e390 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
@@ -23,6 +23,8 @@
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import kotlin.math.max
import kotlin.math.min
@@ -139,7 +141,7 @@
val placeableCrossAxisSize = placeable.crossAxisSize()
childrenMainAxisSize[i - startIndex] = placeableMainAxisSize
spaceAfterLastNoWeight =
- min(arrangementSpacingInt, (remaining - placeableMainAxisSize).coerceAtLeast(0))
+ min(arrangementSpacingInt, (remaining - placeableMainAxisSize).fastCoerceAtLeast(0))
fixedSpace += placeableMainAxisSize + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeableCrossAxisSize)
placeables[i] = placeable
@@ -160,7 +162,7 @@
}
val arrangementSpacingTotal = arrangementSpacingPx * (weightChildrenCount - 1)
val remainingToTarget =
- (targetSpace - fixedSpace - arrangementSpacingTotal).coerceAtLeast(0)
+ (targetSpace - fixedSpace - arrangementSpacingTotal).fastCoerceAtLeast(0)
val weightUnitSpace = remainingToTarget / totalWeight
var remainder = remainingToTarget
@@ -168,44 +170,7 @@
val measurable = measurables[i]
val itemWeight = measurable.rowColumnParentData.weight
val weightedSize = (weightUnitSpace * itemWeight)
- try {
- remainder -= weightedSize.fastRoundToInt()
- } catch (e: IllegalArgumentException) {
- throw initCause(
- IllegalArgumentException(
- "This log indicates a hard-to-reproduce Compose issue, " +
- "modified with additional debugging details. " +
- "Please help us by adding your experiences to the bug link provided. " +
- "Thank you for helping us improve Compose. " +
- "https://issuetracker.google.com/issues/297974033 " +
- "mainAxisMax " +
- mainAxisMax +
- "mainAxisMin " +
- mainAxisMin +
- "targetSpace " +
- targetSpace +
- "arrangementSpacingPx " +
- arrangementSpacingPx +
- "weightChildrenCount " +
- weightChildrenCount +
- "fixedSpace " +
- fixedSpace +
- "arrangementSpacingTotal " +
- arrangementSpacingTotal +
- "remainingToTarget " +
- remainingToTarget +
- "totalWeight " +
- totalWeight +
- "weightUnitSpace " +
- weightUnitSpace +
- "itemWeight " +
- itemWeight +
- "weightedSize " +
- weightedSize
- ),
- e
- )
- }
+ remainder -= weightedSize.fastRoundToInt()
}
for (i in startIndex until endIndex) {
@@ -227,64 +192,19 @@
remainder -= remainderUnit
val weightedSize = (weightUnitSpace * weight)
val childMainAxisSize = max(0, weightedSize.fastRoundToInt() + remainderUnit)
-
- val childConstraints: Constraints
- try {
- childConstraints =
- createConstraints(
- mainAxisMin =
- if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
- childMainAxisSize
- } else {
- 0
- },
- crossAxisMin = crossAxisDesiredSize ?: 0,
- mainAxisMax = childMainAxisSize,
- crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
- isPrioritizing = true
- )
- } catch (e: IllegalArgumentException) {
- throw initCause(
- IllegalArgumentException(
- "This log indicates a hard-to-reproduce Compose issue, " +
- "modified with additional debugging details. " +
- "Please help us by adding your experiences to the bug link provided. " +
- "Thank you for helping us improve Compose. " +
- "https://issuetracker.google.com/issues/300280216 " +
- "mainAxisMax " +
- mainAxisMax +
- "mainAxisMin " +
- mainAxisMin +
- "targetSpace " +
- targetSpace +
- "arrangementSpacingPx " +
- arrangementSpacingPx +
- "weightChildrenCount " +
- weightChildrenCount +
- "fixedSpace " +
- fixedSpace +
- "arrangementSpacingTotal " +
- arrangementSpacingTotal +
- "remainingToTarget " +
- remainingToTarget +
- "totalWeight " +
- totalWeight +
- "weightUnitSpace " +
- weightUnitSpace +
- "weight " +
- weight +
- "weightedSize " +
- weightedSize +
- "crossAxisDesiredSize " +
- crossAxisDesiredSize +
- "remainderUnit " +
- remainderUnit +
- "childMainAxisSize " +
+ val childConstraints: Constraints =
+ createConstraints(
+ mainAxisMin =
+ if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
childMainAxisSize
- ),
- e
+ } else {
+ 0
+ },
+ crossAxisMin = crossAxisDesiredSize ?: 0,
+ mainAxisMax = childMainAxisSize,
+ crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
+ isPrioritizing = true
)
- }
val placeable = child.measure(childConstraints)
val placeableMainAxisSize = placeable.mainAxisSize()
val placeableCrossAxisSize = placeable.crossAxisSize()
@@ -295,7 +215,9 @@
}
}
weightedSpace =
- (weightedSpace + arrangementSpacingTotal).toInt().coerceIn(0, mainAxisMax - fixedSpace)
+ (weightedSpace + arrangementSpacingTotal)
+ .toInt()
+ .fastCoerceIn(0, mainAxisMax - fixedSpace)
}
// we've done this check in weights as to avoid going through another loop
@@ -327,13 +249,13 @@
}
// Compute the Row or Column size and position the children.
- val mainAxisLayoutSize = max((fixedSpace + weightedSpace).coerceAtLeast(0), mainAxisMin)
+ val mainAxisLayoutSize = max((fixedSpace + weightedSpace).fastCoerceAtLeast(0), mainAxisMin)
val crossAxisLayoutSize =
max(
crossAxisSpace,
max(crossAxisMin, beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine)
)
- val mainAxisPositions = IntArray(subSize) { 0 }
+ val mainAxisPositions = IntArray(subSize)
populateMainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
@@ -354,8 +276,3 @@
endIndex
)
}
-
-internal expect inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 0d0e4ff..2763e69 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -21,3 +21,7 @@
ParameterNameChange: androidx.compose.foundation.gestures.DraggableAnchors#positionOf(T) parameter #0:
Attempted to change parameter name from value to anchor in method androidx.compose.foundation.gestures.DraggableAnchors.positionOf
+
+
+RemovedClass: androidx.compose.foundation.draganddrop.AndroidDragAndDropSource_androidKt:
+ Removed class androidx.compose.foundation.draganddrop.AndroidDragAndDropSource_androidKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index bced9b3..6081c4a 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -371,16 +371,9 @@
package androidx.compose.foundation.draganddrop {
- public final class AndroidDragAndDropSource_androidKt {
- method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.draganddrop.DragAndDropSourceScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- }
-
public final class DragAndDropSourceKt {
- method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.draganddrop.DragAndDropSourceScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- }
-
- public interface DragAndDropSourceScope extends androidx.compose.ui.input.pointer.PointerInputScope {
- method public void startTransfer(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData);
+ method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?> transferData);
+ method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration, kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?> transferData);
}
public final class DragAndDropTargetKt {
@@ -835,10 +828,6 @@
method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
}
- public final class LazyListAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.LazyListState state);
- }
-
public interface LazyListItemInfo {
method public default Object? getContentType();
method public int getIndex();
@@ -900,6 +889,10 @@
method public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
+ public final class LazyListScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.LazyListState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
@androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState();
ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
@@ -967,10 +960,6 @@
property public final int currentLineSpan;
}
- public final class LazyGridAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.grid.LazyGridState state);
- }
-
public final class LazyGridDslKt {
method @androidx.compose.runtime.Composable public static void LazyHorizontalGrid(androidx.compose.foundation.lazy.grid.GridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.grid.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.grid.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridScope,kotlin.Unit> content);
@@ -1068,6 +1057,10 @@
@kotlin.DslMarker public @interface LazyGridScopeMarker {
}
+ public final class LazyGridScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.grid.LazyGridState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
public final class LazyGridSpanKt {
method public static long GridItemSpan(@IntRange(from=1L) int currentLineSpan);
}
@@ -1128,19 +1121,6 @@
property public final T value;
}
- public interface LazyLayoutAnimateScrollScope {
- method public int calculateDistanceTo(int targetIndex, optional int targetOffset);
- method public int getFirstVisibleItemIndex();
- method public int getFirstVisibleItemScrollOffset();
- method public int getItemCount();
- method public int getLastVisibleItemIndex();
- method public void snapToItem(androidx.compose.foundation.gestures.ScrollScope, int index, optional int offset);
- property public abstract int firstVisibleItemIndex;
- property public abstract int firstVisibleItemScrollOffset;
- property public abstract int itemCount;
- property public abstract int lastVisibleItemIndex;
- }
-
public abstract class LazyLayoutIntervalContent<Interval extends androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent.Interval> {
ctor public LazyLayoutIntervalContent();
method public final Object? getContentType(int index);
@@ -1211,6 +1191,19 @@
method public void markAsUrgent();
}
+ public interface LazyLayoutScrollScope extends androidx.compose.foundation.gestures.ScrollScope {
+ method public int calculateDistanceTo(int targetIndex, optional int targetOffset);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public int getItemCount();
+ method public int getLastVisibleItemIndex();
+ method public void snapToItem(int index, optional int offset);
+ property public abstract int firstVisibleItemIndex;
+ property public abstract int firstVisibleItemScrollOffset;
+ property public abstract int itemCount;
+ property public abstract int lastVisibleItemIndex;
+ }
+
public final class Lazy_androidKt {
method public static Object getDefaultLazyLayoutKey(int index);
}
@@ -1245,10 +1238,6 @@
package androidx.compose.foundation.lazy.staggeredgrid {
- public final class LazyStaggeredGridAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state);
- }
-
public final class LazyStaggeredGridDslKt {
method @androidx.compose.runtime.Composable public static void LazyHorizontalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional float horizontalItemSpacing, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void LazyVerticalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional float verticalItemSpacing, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
@@ -1303,6 +1292,10 @@
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
}
+ public final class LazyStaggeredGridScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
@androidx.compose.runtime.Stable public final class LazyStaggeredGridState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public LazyStaggeredGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemOffset);
method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1434,11 +1427,11 @@
property public abstract java.util.List<androidx.compose.foundation.pager.PageInfo> visiblePagesInfo;
}
- public final class PagerLazyAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.pager.PagerState state);
+ public sealed interface PagerScope {
}
- public sealed interface PagerScope {
+ public final class PagerScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
}
@androidx.compose.runtime.Stable public interface PagerSnapDistance {
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 0d0e4ff..2763e69 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -21,3 +21,7 @@
ParameterNameChange: androidx.compose.foundation.gestures.DraggableAnchors#positionOf(T) parameter #0:
Attempted to change parameter name from value to anchor in method androidx.compose.foundation.gestures.DraggableAnchors.positionOf
+
+
+RemovedClass: androidx.compose.foundation.draganddrop.AndroidDragAndDropSource_androidKt:
+ Removed class androidx.compose.foundation.draganddrop.AndroidDragAndDropSource_androidKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 38305f9..5f71eff 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -373,16 +373,9 @@
package androidx.compose.foundation.draganddrop {
- public final class AndroidDragAndDropSource_androidKt {
- method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.draganddrop.DragAndDropSourceScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- }
-
public final class DragAndDropSourceKt {
- method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.draganddrop.DragAndDropSourceScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
- }
-
- public interface DragAndDropSourceScope extends androidx.compose.ui.input.pointer.PointerInputScope {
- method public void startTransfer(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData);
+ method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?> transferData);
+ method public static androidx.compose.ui.Modifier dragAndDropSource(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration, kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?> transferData);
}
public final class DragAndDropTargetKt {
@@ -837,10 +830,6 @@
method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
}
- public final class LazyListAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.LazyListState state);
- }
-
public interface LazyListItemInfo {
method public default Object? getContentType();
method public int getIndex();
@@ -902,6 +891,10 @@
method public void stickyHeader(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
+ public final class LazyListScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.LazyListState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
@androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState();
ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
@@ -969,10 +962,6 @@
property public final int currentLineSpan;
}
- public final class LazyGridAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.grid.LazyGridState state);
- }
-
public final class LazyGridDslKt {
method @androidx.compose.runtime.Composable public static void LazyHorizontalGrid(androidx.compose.foundation.lazy.grid.GridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.grid.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.grid.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.grid.LazyGridScope,kotlin.Unit> content);
@@ -1070,6 +1059,10 @@
@kotlin.DslMarker public @interface LazyGridScopeMarker {
}
+ public final class LazyGridScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.grid.LazyGridState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
public final class LazyGridSpanKt {
method public static long GridItemSpan(@IntRange(from=1L) int currentLineSpan);
}
@@ -1130,19 +1123,6 @@
property public final T value;
}
- public interface LazyLayoutAnimateScrollScope {
- method public int calculateDistanceTo(int targetIndex, optional int targetOffset);
- method public int getFirstVisibleItemIndex();
- method public int getFirstVisibleItemScrollOffset();
- method public int getItemCount();
- method public int getLastVisibleItemIndex();
- method public void snapToItem(androidx.compose.foundation.gestures.ScrollScope, int index, optional int offset);
- property public abstract int firstVisibleItemIndex;
- property public abstract int firstVisibleItemScrollOffset;
- property public abstract int itemCount;
- property public abstract int lastVisibleItemIndex;
- }
-
public abstract class LazyLayoutIntervalContent<Interval extends androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent.Interval> {
ctor public LazyLayoutIntervalContent();
method public final Object? getContentType(int index);
@@ -1213,6 +1193,19 @@
method public void markAsUrgent();
}
+ public interface LazyLayoutScrollScope extends androidx.compose.foundation.gestures.ScrollScope {
+ method public int calculateDistanceTo(int targetIndex, optional int targetOffset);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public int getItemCount();
+ method public int getLastVisibleItemIndex();
+ method public void snapToItem(int index, optional int offset);
+ property public abstract int firstVisibleItemIndex;
+ property public abstract int firstVisibleItemScrollOffset;
+ property public abstract int itemCount;
+ property public abstract int lastVisibleItemIndex;
+ }
+
public final class Lazy_androidKt {
method public static Object getDefaultLazyLayoutKey(int index);
}
@@ -1247,10 +1240,6 @@
package androidx.compose.foundation.lazy.staggeredgrid {
- public final class LazyStaggeredGridAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state);
- }
-
public final class LazyStaggeredGridDslKt {
method @androidx.compose.runtime.Composable public static void LazyHorizontalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional float horizontalItemSpacing, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void LazyVerticalStaggeredGrid(androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional float verticalItemSpacing, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope,kotlin.Unit> content);
@@ -1305,6 +1294,10 @@
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends java.lang.Object?> contentType, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan>? span, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
}
+ public final class LazyStaggeredGridScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
+ }
+
@androidx.compose.runtime.Stable public final class LazyStaggeredGridState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public LazyStaggeredGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemOffset);
method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1436,11 +1429,11 @@
property public abstract java.util.List<androidx.compose.foundation.pager.PageInfo> visiblePagesInfo;
}
- public final class PagerLazyAnimateScrollScopeKt {
- method public static androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope LazyLayoutAnimateScrollScope(androidx.compose.foundation.pager.PagerState state);
+ public sealed interface PagerScope {
}
- public sealed interface PagerScope {
+ public final class PagerScrollScopeKt {
+ method public static androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope LazyLayoutScrollScope(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.ScrollScope scrollScope);
}
@androidx.compose.runtime.Stable public interface PagerSnapDistance {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
index 3fa9b31..cec2c31 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
@@ -42,7 +42,7 @@
implementation(project(":compose:ui:ui-text:ui-text-samples"))
implementation(project(":paging:paging-compose:integration-tests:paging-demos"))
implementation(project(":compose:ui:ui-tooling-preview"))
- debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation(project(":compose:ui:ui-tooling"))
implementation(project(":internal-testutils-fonts"))
implementation("androidx.collection:collection:1.4.2")
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index a328cd2..dfb7495 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -34,7 +34,6 @@
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Arrangement
@@ -1045,15 +1044,9 @@
private fun LazyItemScope.DragAndDropItem(index: Int, color: Color) {
Box(
Modifier.dragAndDropSource {
- detectTapGestures(
- onLongPress = {
- startTransfer(
- DragAndDropTransferData(
- clipData = ClipData.newPlainText("item_id", index.toString()),
- localState = index
- )
- )
- }
+ DragAndDropTransferData(
+ clipData = ClipData.newPlainText("item_id", index.toString()),
+ localState = index
)
}
.animateItem()
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 6152c8f..4169d00 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.demos.text
+import android.annotation.SuppressLint
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
@@ -35,11 +36,15 @@
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -100,7 +105,9 @@
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
@@ -120,8 +127,26 @@
val fontSize8 = 25.sp
val fontSize10 = 30.sp
-private val overflowOptions = listOf(TextOverflow.Visible, TextOverflow.Ellipsis, TextOverflow.Clip)
-private val paragraphOptions = listOf(true, false)
+@SuppressLint("PrimitiveInCollection")
+private val overflowOptions =
+ listOf(
+ TextOverflow.Clip,
+ TextOverflow.Visible,
+ TextOverflow.StartEllipsis,
+ TextOverflow.MiddleEllipsis,
+ TextOverflow.Ellipsis
+ )
+private val boolOptions = listOf(true, false)
+@SuppressLint("PrimitiveInCollection")
+private val textAlignments =
+ listOf(
+ TextAlign.Left,
+ TextAlign.Start,
+ TextAlign.Center,
+ TextAlign.Right,
+ TextAlign.End,
+ TextAlign.Justify
+ )
@Preview
@Composable
@@ -623,9 +648,30 @@
@Composable
fun TextOverflowDemo() {
- Column {
- var singleParagraph by remember { mutableStateOf(paragraphOptions[0]) }
+ Column(
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
+ var singleParagraph by remember { mutableStateOf(boolOptions[0]) }
var selectedOverflow by remember { mutableStateOf(overflowOptions[0]) }
+ var singleLinePerPar by remember { mutableStateOf(boolOptions[1]) }
+ var width by remember { mutableFloatStateOf(250f) }
+ var height by remember { mutableFloatStateOf(50f) }
+ var letterSpacing by remember { mutableFloatStateOf(0f) }
+ var textAlign by remember { mutableStateOf(TextAlign.Left) }
+ var softWrap by remember { mutableStateOf(true) }
+
+ TextOverflowDemo(
+ singleParagraph,
+ selectedOverflow,
+ singleLinePerPar,
+ width.dp,
+ height.dp,
+ letterSpacing.sp,
+ textAlign,
+ softWrap
+ )
+
Row(Modifier.fillMaxWidth()) {
Column(Modifier.selectableGroup().weight(1f)) {
Text("TextOverflow", fontWeight = FontWeight.Bold)
@@ -649,7 +695,7 @@
}
Column(Modifier.selectableGroup().weight(1f)) {
Text("Paragraph", fontWeight = FontWeight.Bold)
- paragraphOptions.forEach {
+ boolOptions.forEach {
Row(
Modifier.fillMaxWidth()
.selectable(
@@ -667,14 +713,86 @@
}
}
}
+ Column(Modifier.selectableGroup().weight(1f)) {
+ Text("Single line", fontWeight = FontWeight.Bold)
+ boolOptions.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == singleLinePerPar),
+ onClick = { singleLinePerPar = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == singleLinePerPar), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
}
-
- TextOverflowDemo(singleParagraph, selectedOverflow)
+ Column {
+ Text("Width " + "%.1f".format(width) + "dp")
+ Slider(width, { width = it }, valueRange = 30f..300f)
+ }
+ Column {
+ Text("Height " + "%.1f".format(height) + "dp")
+ Slider(height, { height = it }, valueRange = 5f..300f)
+ }
+ Column {
+ Text("Letter spacing " + "%.1f".format(letterSpacing) + "sp")
+ Slider(letterSpacing, { letterSpacing = it }, valueRange = -4f..8f, steps = 11)
+ }
+ Row(Modifier.fillMaxWidth()) {
+ Column(Modifier.weight(1f)) {
+ Text("Text Align", fontWeight = FontWeight.Bold)
+ textAlignments.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == textAlign),
+ onClick = { textAlign = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == textAlign), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
+ Column(Modifier.weight(1f)) {
+ Text("Soft wrap", fontWeight = FontWeight.Bold)
+ boolOptions.forEach {
+ Row(
+ Modifier.fillMaxWidth()
+ .selectable(
+ selected = (it == softWrap),
+ onClick = { softWrap = it },
+ role = Role.RadioButton
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(selected = (it == softWrap), onClick = null)
+ Text(text = it.toString())
+ }
+ }
+ }
+ }
}
}
@Composable
-private fun ColumnScope.TextOverflowDemo(singleParagraph: Boolean, textOverflow: TextOverflow) {
+private fun ColumnScope.TextOverflowDemo(
+ singleParagraph: Boolean,
+ textOverflow: TextOverflow,
+ singeLine: Boolean,
+ width: Dp,
+ height: Dp,
+ letterSpacing: TextUnit,
+ textAlign: TextAlign,
+ softWrap: Boolean
+) {
Box(Modifier.weight(1f).fillMaxWidth()) {
val text =
if (singleParagraph) {
@@ -687,11 +805,19 @@
}
}
}
- Text(
+ val textStyle =
+ TextStyle(fontSize = fontSize6, letterSpacing = letterSpacing, textAlign = textAlign)
+ BasicText(
text = text,
- modifier = Modifier.align(Alignment.Center).background(Color.Magenta).size(100.dp),
- fontSize = fontSize6,
- overflow = textOverflow
+ modifier =
+ Modifier.align(Alignment.Center)
+ .background(Color.Magenta)
+ .widthIn(max = width)
+ .heightIn(max = height),
+ style = textStyle,
+ overflow = textOverflow,
+ maxLines = if (singeLine) 1 else Int.MAX_VALUE,
+ softWrap = softWrap
)
}
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt
new file mode 100644
index 0000000..48708bb
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateScrollScope.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyLayoutScrollScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListAnimateScrollScope(orientation: Orientation) :
+ BaseLazyListTestWithOrientation(orientation) {
+
+ @Test
+ fun animateToItem_stickyHeader_shouldNotConsiderItemFound() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState(initialFirstVisibleItemIndex = 3)
+ LazyColumnOrRow(Modifier.crossAxisSize(150.dp).mainAxisSize(100.dp), state) {
+ stickyHeader { Box(Modifier.size(150.dp)) }
+ items(20) { Box(Modifier.size(150.dp)) }
+ }
+ }
+
+ val animatedScrollScope = LazyLayoutScrollScope(state, NoOpScope)
+ /**
+ * Sticky item is considered non visible whilst sticking, distance should be best effort,
+ * average size * (target pos - current pos)
+ */
+ assertThat(animatedScrollScope.calculateDistanceTo(0))
+ .isEqualTo(-3 * with(rule.density) { 150.dp.roundToPx() })
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+
+ val NoOpScope =
+ object : ScrollScope {
+ override fun scrollBy(pixels: Float): Float = 0f
+ }
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
index 84a7799..6131d42 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridContentPaddingTest.kt
@@ -20,7 +20,11 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
@@ -343,4 +347,37 @@
rule.onNodeWithTag("19").assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 3f)
}
+
+ @Test
+ fun pinnedItemWorksIsPlacedOnceInContentPadding() {
+ state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+ val focusRequester = FocusRequester()
+ rule.setContent {
+ Box(Modifier.axisSize(itemSizeDp * 2, itemSizeDp * 4)) {
+ LazyStaggeredGrid(
+ lanes = 1,
+ modifier = Modifier.testTag(LazyStaggeredGrid),
+ contentPadding = PaddingValues(beforeContent = itemSizeDp),
+ state = state
+ ) {
+ item {
+ LaunchedEffect(Unit) { focusRequester.requestFocus() }
+ BasicTextField(
+ "Test",
+ onValueChange = {},
+ modifier =
+ Modifier.focusRequester(focusRequester).mainAxisSize(itemSizeDp)
+ )
+ }
+
+ items(10) { Spacer(Modifier.mainAxisSize(itemSizeDp).testTag("$it")) }
+ }
+ }
+ }
+
+ // scroll to the end
+ state.scrollBy(itemSizeDp / 2)
+
+ rule.onNodeWithTag("0").assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 1.5f)
+ }
}
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 61e8271..b2239a0 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -45,7 +45,7 @@
implementation("androidx.compose.ui:ui:1.2.1")
implementation("androidx.compose.ui:ui-text:1.2.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.0")
- debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation(project(":compose:ui:ui-tooling"))
}
androidx {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragAndDropSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragAndDropSamples.kt
index 75dd94e..4ba070c 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragAndDropSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragAndDropSamples.kt
@@ -33,7 +33,6 @@
import androidx.compose.foundation.border
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -117,15 +116,9 @@
modifier =
modifier
.dragAndDropSource {
- detectTapGestures(
- onLongPress = {
- startTransfer(
- DragAndDropTransferData(
- clipData = ClipData.newPlainText(label, label),
- flags = View.DRAG_FLAG_GLOBAL,
- )
- )
- }
+ DragAndDropTransferData(
+ clipData = ClipData.newPlainText(label, label),
+ flags = View.DRAG_FLAG_GLOBAL,
)
}
.border(
@@ -158,6 +151,22 @@
)
}
var backgroundColor by remember { mutableStateOf(Color.Transparent) }
+ val dragAndDropTarget = remember {
+ object : DragAndDropTarget {
+ override fun onStarted(event: DragAndDropEvent) {
+ backgroundColor = Color.DarkGray.copy(alpha = 0.2f)
+ }
+
+ override fun onDrop(event: DragAndDropEvent): Boolean {
+ onDragAndDropEventDropped(event)
+ return true
+ }
+
+ override fun onEnded(event: DragAndDropEvent) {
+ backgroundColor = Color.Transparent
+ }
+ }
+ }
Box(
modifier =
Modifier.fillMaxSize()
@@ -169,21 +178,7 @@
}
hasValidMimeType
},
- target =
- object : DragAndDropTarget {
- override fun onStarted(event: DragAndDropEvent) {
- backgroundColor = Color.DarkGray.copy(alpha = 0.2f)
- }
-
- override fun onDrop(event: DragAndDropEvent): Boolean {
- onDragAndDropEventDropped(event)
- return true
- }
-
- override fun onEnded(event: DragAndDropEvent) {
- backgroundColor = Color.Transparent
- }
- },
+ target = dragAndDropTarget,
)
.background(backgroundColor)
.border(width = 4.dp, color = Color.Magenta, shape = RoundedCornerShape(16.dp)),
@@ -288,7 +283,7 @@
Modifier.size(56.dp).background(color = color).dragAndDropSource(
drawDragDecoration = { drawRect(color) },
) {
- detectTapGestures(onLongPress = { startTransfer(color.toDragAndDropTransfer()) })
+ color.toDragAndDropTransfer()
}
)
}
@@ -325,15 +320,13 @@
dragAndDropSource(
drawDragDecoration = { drawRoundRect(state.color) },
) {
- detectTapGestures(onLongPress = { startTransfer(state.color.toDragAndDropTransfer()) })
+ state.color.toDragAndDropTransfer()
}
-private fun Modifier.stateDropTarget(state: State) =
- dragAndDropTarget(
- shouldStartDragAndDrop = { startEvent ->
- startEvent.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_INTENT)
- },
- target =
+@Composable
+private fun Modifier.stateDropTarget(state: State): Modifier {
+ val dragAndDropTarget =
+ remember(state) {
object : DragAndDropTarget {
override fun onStarted(event: DragAndDropEvent) {
state.onStarted()
@@ -371,7 +364,14 @@
}
}
}
+ }
+ return dragAndDropTarget(
+ shouldStartDragAndDrop = { startEvent ->
+ startEvent.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_INTENT)
+ },
+ target = dragAndDropTarget
)
+}
@Stable
private class State(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
index b5f57f0..8fadca5 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyDslSamples.kt
@@ -27,13 +27,14 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.LazyLayoutScrollScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -209,28 +210,30 @@
@Sampled
@Composable
-fun CustomLazyListAnimateToItemScrollSample() {
+@Preview
+fun LazyListCustomScrollUsingLazyLayoutScrollScopeSample() {
+ suspend fun LazyListState.customScroll(block: suspend LazyLayoutScrollScope.() -> Unit) =
+ scroll {
+ block.invoke(LazyLayoutScrollScope(this@customScroll, this))
+ }
val itemsList = (0..100).toList()
val state = rememberLazyListState()
val scope = rememberCoroutineScope()
- val animatedScrollScope = remember(state) { LazyLayoutAnimateScrollScope(state) }
Column(Modifier.verticalScroll(rememberScrollState())) {
Button(
onClick = {
scope.launch {
- state.scroll {
- with(animatedScrollScope) {
- snapToItem(40, 0) // teleport to item 40
- val distance = calculateDistanceTo(50).toFloat()
- var previousValue = 0f
- androidx.compose.animation.core.animate(
- 0f,
- distance,
- animationSpec = tween(5_000)
- ) { currentValue, _ ->
- previousValue += scrollBy(currentValue - previousValue)
- }
+ state.customScroll {
+ snapToItem(40, 0) // teleport to item 40
+ val distance = calculateDistanceTo(50).toFloat()
+ var previousValue = 0f
+ androidx.compose.animation.core.animate(
+ 0f,
+ distance,
+ animationSpec = tween(5_000)
+ ) { currentValue, _ ->
+ previousValue += scrollBy(currentValue - previousValue)
}
}
}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
index d7cc99e..7cbe8d4 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyGridSamples.kt
@@ -33,11 +33,12 @@
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
-import androidx.compose.foundation.lazy.grid.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.grid.LazyLayoutScrollScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
@@ -53,6 +54,7 @@
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -205,28 +207,31 @@
@Sampled
@Composable
-fun CustomLazyGridAnimateToItemScrollSample() {
+@Preview
+fun LazyGridCustomScrollUsingLazyLayoutScrollScopeSample() {
+ suspend fun LazyGridState.customScroll(block: suspend LazyLayoutScrollScope.() -> Unit) =
+ scroll {
+ block.invoke(LazyLayoutScrollScope(this@customScroll, this))
+ }
+
val itemsList = (0..100).toList()
val state = rememberLazyGridState()
val scope = rememberCoroutineScope()
- val animatedScrollScope = remember(state) { LazyLayoutAnimateScrollScope(state) }
Column(Modifier.verticalScroll(rememberScrollState())) {
Button(
onClick = {
scope.launch {
- state.scroll {
- with(animatedScrollScope) {
- snapToItem(40, 0) // teleport to item 40
- val distance = calculateDistanceTo(50).toFloat()
- var previousValue = 0f
- androidx.compose.animation.core.animate(
- 0f,
- distance,
- animationSpec = tween(5_000)
- ) { currentValue, _ ->
- previousValue += scrollBy(currentValue - previousValue)
- }
+ state.customScroll {
+ snapToItem(40, 0) // teleport to item 40
+ val distance = calculateDistanceTo(50).toFloat()
+ var previousValue = 0f
+ androidx.compose.animation.core.animate(
+ 0f,
+ distance,
+ animationSpec = tween(5_000)
+ ) { currentValue, _ ->
+ previousValue += scrollBy(currentValue - previousValue)
}
}
}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyStaggeredGridSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyStaggeredGridSamples.kt
index 3063491..1c949f6 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyStaggeredGridSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyStaggeredGridSamples.kt
@@ -29,11 +29,10 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid
-import androidx.compose.foundation.lazy.staggeredgrid.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.staggeredgrid.LazyLayoutScrollScope
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
@@ -163,28 +162,30 @@
@Sampled
@Composable
-fun CustomLazyStaggeredGridAnimateToItemScrollSample() {
+@Preview
+fun LazyStaggeredGridCustomScrollUsingLazyLayoutScrollScopeSample() {
+ suspend fun LazyStaggeredGridState.customScroll(
+ block: suspend LazyLayoutScrollScope.() -> Unit
+ ) = scroll { block.invoke(LazyLayoutScrollScope(this@customScroll, this)) }
+
val itemsList = (0..100).toList()
val state = rememberLazyStaggeredGridState()
val scope = rememberCoroutineScope()
- val animatedScrollScope = remember(state) { LazyLayoutAnimateScrollScope(state) }
Column(Modifier.verticalScroll(rememberScrollState())) {
Button(
onClick = {
scope.launch {
- state.scroll {
- with(animatedScrollScope) {
- snapToItem(40, 0) // teleport to item 40
- val distance = calculateDistanceTo(50).toFloat()
- var previousValue = 0f
- androidx.compose.animation.core.animate(
- 0f,
- distance,
- animationSpec = tween(5_000)
- ) { currentValue, _ ->
- previousValue += scrollBy(currentValue - previousValue)
- }
+ state.customScroll {
+ snapToItem(40, 0) // teleport to item 40
+ val distance = calculateDistanceTo(50).toFloat()
+ var previousValue = 0f
+ androidx.compose.animation.core.animate(
+ 0f,
+ distance,
+ animationSpec = tween(5_000)
+ ) { currentValue, _ ->
+ previousValue += scrollBy(currentValue - previousValue)
}
}
}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
index ec737e2..a4e8e84 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/PagerSamples.kt
@@ -29,10 +29,9 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.pager.LazyLayoutScrollScope
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
@@ -349,28 +348,29 @@
@Preview
@Sampled
@Composable
-fun CustomPagerAnimateToPageScrollSample() {
+fun PagerCustomScrollUsingLazyLayoutScrollScopeSample() {
+ suspend fun PagerState.customScroll(block: suspend LazyLayoutScrollScope.() -> Unit) = scroll {
+ block.invoke(LazyLayoutScrollScope(this@customScroll, this))
+ }
+
val itemsList = (0..100).toList()
val state = rememberPagerState { itemsList.size }
val scope = rememberCoroutineScope()
- val animatedScrollScope = remember(state) { LazyLayoutAnimateScrollScope(state) }
Column(Modifier.verticalScroll(rememberScrollState())) {
Button(
onClick = {
scope.launch {
- state.scroll {
- with(animatedScrollScope) {
- snapToItem(40, 0) // teleport to item 40
- val distance = calculateDistanceTo(50).toFloat()
- var previousValue = 0f
- androidx.compose.animation.core.animate(
- 0f,
- distance,
- animationSpec = tween(5_000)
- ) { currentValue, _ ->
- previousValue += scrollBy(currentValue - previousValue)
- }
+ state.customScroll {
+ snapToItem(40, 0) // teleport to item 40
+ val distance = calculateDistanceTo(50).toFloat()
+ var previousValue = 0f
+ androidx.compose.animation.core.animate(
+ 0f,
+ distance,
+ animationSpec = tween(5_000)
+ ) { currentValue, _ ->
+ previousValue += scrollBy(currentValue - previousValue)
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
index 3da96f9..b985ef1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicTooltipTest.kt
@@ -17,10 +17,18 @@
package androidx.compose.foundation
import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.longClick
@@ -135,6 +143,97 @@
rule.waitForIdle()
Truth.assertThat(state.isVisible).isFalse()
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun tooltip_longPress_deepPress() {
+ lateinit var view: View
+ lateinit var state: BasicTooltipState
+ rule.setContent {
+ view = LocalView.current
+ state = rememberBasicTooltipState(initialIsVisible = false)
+ BasicTooltipBox(
+ positionProvider = EmptyPositionProvider(),
+ tooltip = {},
+ state = state,
+ modifier = Modifier.testTag(TOOLTIP_ANCHOR)
+ ) {
+ Box(modifier = Modifier.requiredSize(1.dp)) {}
+ }
+ }
+
+ // Stop auto advance for test consistency
+ rule.mainClock.autoAdvance = false
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle { Truth.assertThat(state.isVisible).isFalse() }
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ // Even though the timeout didn't pass, the deep press should immediately show the tooltip
+ rule.runOnIdle { Truth.assertThat(state.isVisible).isTrue() }
+ }
}
private class EmptyPositionProvider : PopupPositionProvider {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
index c2529e9..94a68ea 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
@@ -16,7 +16,15 @@
package androidx.compose.foundation
+import android.os.Build
import android.os.Build.VERSION.SDK_INT
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.View
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
@@ -61,6 +69,7 @@
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalInputModeManager
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
@@ -94,6 +103,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
@@ -2239,6 +2249,229 @@
.assertOnClickLabelMatches("true")
.assertOnLongClickLabelMatches("true")
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun longClick_deepPress() {
+ lateinit var view: View
+ var clicks = 0
+ var longClicks = 0
+ var doubleClicks = 0
+ val onClick: () -> Unit = { ++clicks }
+ val onLongClick: () -> Unit = { ++longClicks }
+ val onDoubleClick: () -> Unit = { ++doubleClicks }
+ rule.setContent {
+ view = LocalView.current
+ Box {
+ BasicText(
+ "ClickableText",
+ modifier =
+ Modifier.testTag("myClickable")
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onDoubleClick = onDoubleClick
+ )
+ )
+ }
+ }
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ assertThat(longClicks).isEqualTo(0)
+ assertThat(doubleClicks).isEqualTo(0)
+ }
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // click
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ assertThat(longClicks).isEqualTo(1)
+ assertThat(doubleClicks).isEqualTo(0)
+ }
+ }
+
+ /** Detect the second deep press as long click. */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun secondTapLongClick_deepPress() {
+ lateinit var view: View
+ var clicks = 0
+ var longClicks = 0
+ var doubleClicks = 0
+ val onClick: () -> Unit = { ++clicks }
+ val onLongClick: () -> Unit = { ++longClicks }
+ val onDoubleClick: () -> Unit = { ++doubleClicks }
+ rule.setContent {
+ view = LocalView.current
+ Box {
+ BasicText(
+ "ClickableText",
+ modifier =
+ Modifier.testTag("myClickable")
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onDoubleClick = onDoubleClick
+ )
+ )
+ }
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput {
+ down(0, Offset(5f, 5f))
+ up(0)
+ }
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ assertThat(longClicks).isEqualTo(0)
+ assertThat(doubleClicks).isEqualTo(0)
+ }
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ assertThat(longClicks).isEqualTo(0)
+ assertThat(doubleClicks).isEqualTo(0)
+ }
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 100,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // press
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ assertThat(longClicks).isEqualTo(1)
+ assertThat(doubleClicks).isEqualTo(0)
+ }
+ }
}
private fun SemanticsNodeInteraction.assertOnLongClickLabelMatches(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
index cdd939a..5493358 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
@@ -808,14 +808,12 @@
val moveAngle = Math.atan(moveOffset.x / moveOffset.y.toDouble())
rule.runOnIdle {
- assertEquals(
- downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat(),
- onDragStartedOffset.x
- )
- assertEquals(
- downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat(),
- onDragStartedOffset.y
- )
+ assertThat(downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.x)
+ assertThat(downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.y)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt
index e141d52..0a2b2a3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/HoverableTest.kt
@@ -305,11 +305,21 @@
rule.runOnIdle { moveContent = true }
rule.runOnIdle {
- Truth.assertThat(interactions).hasSize(2)
+ Truth.assertThat(interactions).hasSize(3)
+ // Check first interaction
Truth.assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
+
+ // Check second interaction
+ // Because the content is moved to a new parent during an active event stream, the
+ // current event stream cancelled and an exit is triggered.
Truth.assertThat(interactions[1]).isInstanceOf(HoverInteraction.Exit::class.java)
- Truth.assertThat((interactions[1] as HoverInteraction.Exit).enter)
- .isEqualTo(interactions[0])
+ val hoverInteractionExit = interactions[1] as HoverInteraction.Exit
+ Truth.assertThat(hoverInteractionExit.enter).isEqualTo(interactions[0])
+
+ // Check third interaction
+ // After the content is moved, the hover enter is re-triggered since the mouse is now
+ // hovering over the new content.
+ Truth.assertThat(interactions[2]).isInstanceOf(HoverInteraction.Enter::class.java)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
similarity index 75%
rename from compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt
rename to compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
index 3d2c626..498c9df 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitTouchEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/AwaitLongPressOrCancellationTest.kt
@@ -16,6 +16,15 @@
package androidx.compose.foundation.gestures
+import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.MotionEvent.PointerCoords
+import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.Modifier
@@ -23,6 +32,7 @@
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.click
@@ -32,6 +42,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth
import org.junit.After
@@ -339,4 +350,95 @@
rule.runOnIdle { Truth.assertThat(counter).isEqualTo(0) }
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun awaitLongPressOrCancellationTest_deepPress_assertTriggers() {
+ var counter = 0
+
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ Box {
+ BasicText(
+ "LongPressText",
+ modifier =
+ Modifier.testTag("myLongPress").pointerInput(Unit) {
+ awaitEachGesture {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ awaitLongPressOrCancellation(down.id)?.let { counter++ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle { Truth.assertThat(counter).isEqualTo(0) }
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle { Truth.assertThat(counter).isEqualTo(1) }
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorWhileMovingUIToPopupTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorWhileMovingUIToPopupTest.kt
new file mode 100644
index 0000000..80dd6a0
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorWhileMovingUIToPopupTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.gestures
+
+import android.view.View
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Popup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TargetTag = "TargetLayout"
+
+/*
+ * Moving Composable UI to a Popup changes the top-level container which previously caused issues
+ * when done during an event stream and when the Composable UI contained a lower-level view
+ * (see b/327245338).
+ *
+ * This tests both moving a pure Composable and a Composable containing a View between a Popup
+ * during an event stream.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class DragGestureDetectorWhileMovingUIToPopupTest {
+ @get:Rule val rule = createComposeRule()
+
+ private val dragAmount = Offset(0f, 50f)
+
+ private var onDragStartCount = 0
+ private var onDragEndCount = 0
+ private var onDragCancelCount = 0
+ private var onDragCount = 0
+
+ private var popUpContainsContent = false
+
+ @Before
+ fun setup() {
+ onDragStartCount = 0
+ onDragEndCount = 0
+ onDragCancelCount = 0
+ onDragCount = 0
+
+ popUpContainsContent = false
+ }
+
+ @Test
+ fun dragGesture_dragStartMovesAndroidViewContentToPopup_shouldNotCrash() {
+ rule.setContent {
+ ContainerMovesContentToPopupOnDrag(
+ modifier = Modifier.fillMaxSize(0.9f).background(Color.Green),
+ testTag = TargetTag,
+ onDragStart = { onDragStartCount++ },
+ onDragCancel = { onDragCancelCount++ },
+ onDrag = { onDragCount++ },
+ onDragEnd = { onDragEndCount++ },
+ ) {
+ AndroidView(factory = ::View, modifier = Modifier.size(200.dp).aspectRatio(1f)) {
+ it.setBackgroundColor(Color.Red.toArgb())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertEquals(0, onDragStartCount)
+ assertEquals(0, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(0, onDragEndCount)
+ assertEquals(false, popUpContainsContent)
+ }
+
+ rule.onNodeWithTag(TargetTag).performTouchInput {
+ down(Offset.Zero)
+ moveBy(dragAmount)
+ }
+
+ rule.waitForIdle()
+
+ rule.runOnIdle {
+ assertEquals(1, onDragStartCount)
+ assertEquals(1, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(0, onDragEndCount)
+ assertEquals(true, popUpContainsContent)
+ }
+
+ rule.onNodeWithTag(TargetTag).performTouchInput { up() }
+
+ rule.runOnIdle {
+ assertEquals(1, onDragStartCount)
+ assertEquals(1, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(1, onDragEndCount)
+ assertEquals(true, popUpContainsContent)
+ }
+ }
+
+ @Test
+ fun dragGesture_dragStartMovesComposeContentToPopup_shouldNotCrash() {
+ rule.setContent {
+ ContainerMovesContentToPopupOnDrag(
+ modifier = Modifier.fillMaxSize(0.9f).background(Color.Green),
+ testTag = TargetTag,
+ onDragStart = { onDragStartCount++ },
+ onDragCancel = { onDragCancelCount++ },
+ onDrag = { onDragCount++ },
+ onDragEnd = { onDragEndCount++ },
+ ) {
+ Box(modifier = Modifier.size(200.dp).background(Color.Red)) {}
+ }
+ }
+
+ rule.runOnIdle {
+ assertEquals(0, onDragStartCount)
+ assertEquals(0, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(0, onDragEndCount)
+ assertEquals(false, popUpContainsContent)
+ }
+
+ rule.onNodeWithTag(TargetTag).performTouchInput {
+ down(Offset.Zero)
+ moveBy(dragAmount)
+ }
+
+ rule.waitForIdle()
+
+ rule.runOnIdle {
+ assertEquals(1, onDragStartCount)
+ assertEquals(1, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(0, onDragEndCount)
+ assertEquals(true, popUpContainsContent)
+ }
+
+ rule.onNodeWithTag(TargetTag).performTouchInput { up() }
+
+ rule.runOnIdle {
+ assertEquals(1, onDragStartCount)
+ assertEquals(1, onDragCount)
+ assertEquals(0, onDragCancelCount)
+ assertEquals(1, onDragEndCount)
+ assertEquals(true, popUpContainsContent)
+ }
+ }
+
+ @Composable
+ private fun ContainerMovesContentToPopupOnDrag(
+ modifier: Modifier = Modifier,
+ testTag: String,
+ onDragStart: () -> Unit = {},
+ onDragCancel: () -> Unit = {},
+ onDrag: () -> Unit = {},
+ onDragEnd: () -> Unit = {},
+ content: @Composable () -> Unit,
+ ) {
+ val movableContent = remember { movableContentOf(content) }
+ var showPopup by remember { mutableStateOf(false) }
+ var offset by remember { mutableStateOf(Offset.Zero) }
+ Box(
+ modifier =
+ modifier.testTag(testTag).pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = {
+ onDragStart()
+ showPopup = true
+ offset = Offset.Zero
+ },
+ onDragCancel = {
+ onDragCancel()
+ showPopup = false
+ offset = Offset.Zero
+ },
+ onDrag = { _, deltaOffset ->
+ onDrag()
+ offset += deltaOffset
+ },
+ onDragEnd = {
+ onDragEnd()
+ showPopup = false
+ offset = Offset.Zero
+ }
+ )
+ }
+ ) {
+ if (showPopup) {
+ popUpContainsContent = true
+ Popup { Box(Modifier.offset { offset.round() }) { movableContent() } }
+ } else {
+ movableContent()
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index cb0ca97..f29a00b 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -16,6 +16,15 @@
package androidx.compose.foundation.gestures
+import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.MotionEvent.PointerCoords
+import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -31,6 +40,7 @@
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.TouchInjectionScope
@@ -39,6 +49,7 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
+import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -756,9 +767,203 @@
assertTrue(longPressed)
assertFalse(released)
assertFalse(canceled)
+ assertFalse(doubleTapped)
rule.mainClock.advanceTimeBy(500)
performTouch { up(1) }
assertTrue(released)
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun longPress_deepPress() {
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ allGestures()
+ }
+
+ rule.waitForIdle()
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ assertFalse(longPressed)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // press
+ assertTrue(longPressed)
+ assertFalse(tapped)
+ assertFalse(released)
+ assertFalse(canceled)
+ assertFalse(doubleTapped)
+ }
+
+ /** Detect the second deep press as long press. */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun secondTapLongPress_deepPress() {
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ allGestures()
+ }
+
+ performTouch {
+ down(0, Offset(5f, 5f))
+ up(0)
+ }
+
+ assertTrue(pressed)
+ assertTrue(released)
+ assertFalse(canceled)
+ assertFalse(tapped)
+ assertFalse(doubleTapped)
+ assertFalse(longPressed)
+
+ pressed = false
+ released = false
+
+ rule.mainClock.advanceTimeBy(50)
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ assertFalse(longPressed)
+ assertFalse(tapped)
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 100,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ assertTrue(pressed)
+ // Even though the timeout didn't pass, the deep press should immediately trigger the long
+ // press
+ assertTrue(longPressed)
+ assertFalse(tapped)
+ assertFalse(released)
+ assertFalse(canceled)
+ assertFalse(doubleTapped)
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
index b79b6e3..a12137b 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/CursorAnchorInfoBuilderTest.kt
@@ -690,7 +690,8 @@
style = input.style,
constraints = Constraints(maxWidth = ceil(width).toInt()),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(intWidth, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
index 341e2ec..ebbfe63c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyCursorAnchorInfoBuilderTest.kt
@@ -698,7 +698,8 @@
style = input.style,
constraints = Constraints(maxWidth = ceil(width).toInt()),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(intWidth, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
index 694f85a..f7c6b06 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/LegacyTextInputMethodRequestCursorAnchorInfoTest.kt
@@ -412,7 +412,8 @@
style = input.style,
constraints = Constraints(maxWidth = width),
density = input.density,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
return TextLayoutResult(input, paragraph, IntSize(width, ceil(paragraph.height).toInt()))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
index d18722d..abeabd3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -337,7 +337,7 @@
fontFamilyResolver,
text.spanStyles,
maxLines = 5,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
assertThat(actual.height).isEqualTo(expected.height)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
index 6f75020..6af662b 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
@@ -16,6 +16,9 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
+import androidx.compose.foundation.text.DefaultMinLines
+import androidx.compose.foundation.text.FontSizeSearchScope
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.toIntPx
import androidx.compose.ui.text.ExperimentalTextApi
@@ -27,6 +30,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -222,6 +226,54 @@
}
@Test
+ fun TextLayoutResult_layout_withStartEllipsis_withoutSoftWrap() {
+ val fontSize = 20f
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World! Hello World! Hello World! Hello World!",
+ style = createTextStyle(fontSize = fontSize.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis,
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
+ // Makes width smaller than needed.
+ val width = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 2
+ val constraints = Constraints(maxWidth = width)
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+
+ assertThat(layoutResult.lineCount).isEqualTo(1)
+ assertThat(layoutResult.isLineEllipsized(0)).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_layout_withMiddleEllipsis_withoutSoftWrap() {
+ val fontSize = 20f
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World! Hello World! Hello World! Hello World!",
+ style = createTextStyle(fontSize = fontSize.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
+ // Makes width smaller than needed.
+ val width = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 2
+ val constraints = Constraints(maxWidth = width)
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+
+ assertThat(layoutResult.lineCount).isEqualTo(1)
+ assertThat(layoutResult.isLineEllipsized(0)).isTrue()
+ }
+
+ @Test
fun TextLayoutResult_layoutWithLimitedHeight_withEllipsis() {
val fontSize = 20f
@@ -268,6 +320,339 @@
}
@Test
+ fun TextLayoutResult_autoSize_oneSize_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(25.sp))
+ )
+ .also { it.density = density }
+
+ // 25.6.sp doesn't overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(100)
+
+ textDelegate.updateAutoSize(
+ text = "Hello World",
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.7.sp))
+ )
+
+ // 25.7.sp does overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isTrue()
+ assertThat(layoutResult.height).isEqualTo(1000)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_multipleSizes_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(23.5.sp, 22.sp, 25.6.sp))
+ )
+ .also { it.density = density }
+
+ // All font sizes shouldn't overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(100)
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.7.sp, 25.6.sp, 50.sp))
+ )
+
+ // Only 25.6.sp shouldn't overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(100)
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizePreset(arrayOf(25.9.sp, 25.7.sp, 50.sp))
+ )
+
+ // All font sizes should overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isTrue()
+ assertThat(layoutResult.height).isEqualTo(1000)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_differentConstraints_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 50, minHeight = 0, maxHeight = 50)
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World",
+ style = createTextStyle(20.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+ // this should overflow - 20.sp is too large a font size to use for the smaller constraints
+ assertThat(textDelegate.didOverflow).isTrue()
+ assertThat(layoutResult.height).isEqualTo(120)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_differentText_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis.",
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+ // this should overflow - 20.sp is too large of a font size to use for the longer text
+ assertThat(textDelegate.didOverflow).isTrue()
+ assertThat(layoutResult.height).isEqualTo(600)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_ellipsized_isLineEllipsized() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis."
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+ // Without ellipsis logic, the text would overflow with a height of 600.
+ // This shouldn't overflow due to the ellipsis logic.
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(100)
+ assertThat(layoutResult.isLineEllipsized(4)).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_visibleOverflow_doesOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec egestas " +
+ "sollicitudin arcu, sed mattis orci gravida vel. Donec luctus turpis.",
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Visible,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+ // this should overflow
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(600)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(5.sp),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Ellipsis,
+ autoSize = AutoSizePreset(arrayOf(5.12.em)) // = 25.6sp
+ )
+ .also { it.density = density }
+
+ // 5.12.em / 25.6.sp shouldn't overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ var layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isEqualTo(100)
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = 5.sp,
+ autoSize = AutoSizePreset(arrayOf(5.14.em))
+ )
+
+ // 5.14 .em / 25.7.sp should overflow
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isTrue()
+ assertThat(layoutResult.height).isEqualTo(1000)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun toPx_em_style_fontSize_is_em_throws() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "Hello World",
+ style = TextStyle(fontSize = 0.01.em),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(2.em))
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_style_fontSize_is_unspecified_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizePreset(arrayOf(1.em))
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // doesn't overflow
+ assertThat(textDelegate.didOverflow).isFalse()
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ AutoSizePreset(arrayOf(2.em))
+ )
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // does overflow
+ assertThat(textDelegate.didOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_withoutToPx_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(1.em),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizeWithoutToPx(2.em)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // this shouldn't overflow
+ assertThat(textDelegate.didOverflow).isFalse()
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = 1.em,
+ autoSize = AutoSizeWithoutToPx(3.em)
+ )
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // this should overflow
+ assertThat(textDelegate.didOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_em_withoutToPx_unspecifiedStyleFontSize_checkOverflow() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+ val text = "Hello World"
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = text,
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ autoSize = AutoSizeWithoutToPx(1.em)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // this shouldn't overflow
+ assertThat(textDelegate.didOverflow).isFalse()
+
+ textDelegate.updateAutoSize(
+ text = text,
+ fontSize = TextUnit.Unspecified,
+ autoSize = AutoSizeWithoutToPx(2.em)
+ )
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ // this should overflow
+ assertThat(textDelegate.didOverflow).isTrue()
+ }
+
+ @Test
+ fun TextLayoutResult_autoSize_minLines_greaterThan_1_checkOverflowAndHeight() {
+ val constraints = Constraints(minWidth = 0, maxWidth = 100, minHeight = 0, maxHeight = 100)
+
+ val textDelegate =
+ ParagraphLayoutCache(
+ text = "H",
+ style = createTextStyle(TextUnit.Unspecified),
+ fontFamilyResolver = fontFamilyResolver,
+ minLines = 2,
+ autoSize = AutoSize.StepBased(20.sp, 51.sp, 1.sp)
+ )
+ .also { it.density = density }
+
+ textDelegate.layoutWithConstraints(constraints, LayoutDirection.Ltr)
+ val layoutResult = textDelegate.paragraph!!
+ assertThat(textDelegate.didOverflow).isFalse()
+ assertThat(layoutResult.height).isAtMost(55) // this value is different between
+ // different API levels. Either 51 or 52. Using isAtMost to anticipate future permutations.
+ }
+
+ @Test
fun maxHeight_hasSameHeight_asParagraph() {
val text = "a\n".repeat(20)
val textDelegate =
@@ -291,7 +676,7 @@
fontFamilyResolver,
emptyList(),
maxLines = 5,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
assertThat(actual.height).isEqualTo(expected.height)
}
@@ -347,4 +732,70 @@
letterSpacing = letterSpacing
)
}
+
+ private fun ParagraphLayoutCache.updateAutoSize(
+ text: String,
+ fontSize: TextUnit,
+ autoSize: AutoSize
+ ) =
+ update(
+ text = text,
+ style = createTextStyle(fontSize),
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip,
+ softWrap = true,
+ maxLines = Int.MAX_VALUE,
+ minLines = DefaultMinLines,
+ autoSize = autoSize
+ )
+
+ private class AutoSizePreset(private val presets: Array<TextUnit>) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ var optimalFontSize = 0.sp
+ for (size in presets) {
+ if (
+ size.toPx() > optimalFontSize.toPx() &&
+ !performLayoutAndGetOverflow(size.toPx().toSp())
+ ) {
+ optimalFontSize = size
+ }
+ }
+ return if (optimalFontSize != 0.sp) optimalFontSize else 100.sp
+ // 100.sp is the font size returned when all sizes in the presets array overflow
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AutoSizePreset
+
+ return presets.contentEquals(other.presets)
+ }
+
+ override fun hashCode(): Int {
+ return presets.contentHashCode()
+ }
+ }
+}
+
+private class AutoSizeWithoutToPx(private val fontSize: TextUnit) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ // if there is overflow then 100.sp is returned. Otherwise 0.sp is returned
+ if (performLayoutAndGetOverflow(fontSize)) return 100.sp
+ return 0.sp
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AutoSizeWithoutToPx
+
+ return fontSize == other.fontSize
+ }
+
+ override fun hashCode(): Int {
+ return fontSize.hashCode()
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
index bf5d70c..461caca 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNodeInvalidationTest.kt
@@ -31,7 +31,9 @@
maxLines = params.maxLines,
softWrap = params.softWrap,
fontFamilyResolver = params.fontFamilyResolver,
- overflow = params.overflow
+ overflow = params.overflow,
+ // TODO(b/364657660): Give this a non-null value when AutoSize becomes public
+ autoSize = null
)
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
index 72e2f66..513cfaf 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
@@ -16,16 +16,16 @@
package androidx.compose.foundation
+import androidx.compose.foundation.gestures.LongPressResult
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.gestures.waitForLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.pointerInput
@@ -149,20 +149,14 @@
this.pointerInput(state) {
coroutineScope {
awaitEachGesture {
- val longPressTimeout = viewConfiguration.longPressTimeoutMillis
val pass = PointerEventPass.Initial
// wait for the first down press
val inputType = awaitFirstDown(pass = pass).type
if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
- try {
- // listen to if there is up gesture
- // within the longPressTimeout limit
- withTimeout(longPressTimeout) {
- waitForUpOrCancellation(pass = pass)
- }
- } catch (_: PointerEventTimeoutCancellationException) {
+ val longPress = waitForLongPress(pass = pass)
+ if (longPress is LongPressResult.Success) {
// handle long press - Show the tooltip
launch { state.show(MutatePriority.UserInput) }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.android.kt
index d3f1862..a4471df 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.android.kt
@@ -19,8 +19,8 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.TransferableContent
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
import androidx.compose.ui.draganddrop.toAndroidDragEvent
import androidx.compose.ui.platform.toClipEntry
import androidx.compose.ui.platform.toClipMetadata
@@ -29,8 +29,8 @@
internal actual fun ReceiveContentDragAndDropNode(
receiveContentConfiguration: ReceiveContentConfiguration,
dragAndDropRequestPermission: (DragAndDropEvent) -> Unit
-): DragAndDropModifierNode {
- return DragAndDropModifierNode(
+): DragAndDropTargetModifierNode {
+ return DragAndDropTargetModifierNode(
shouldStartDragAndDrop = {
// accept any dragging item. The actual decider will be the onReceive callback.
true
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
index 9871638..d9aae34 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/draganddrop/AndroidDragAndDropSource.android.kt
@@ -17,90 +17,26 @@
package androidx.compose.foundation.draganddrop
import android.graphics.Picture
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.CacheDrawModifierNode
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-/**
- * A Modifier that allows an element it is applied to to be treated like a source for drag and drop
- * operations. It displays the element dragged as a drag shadow.
- *
- * Learn how to use [Modifier.dragAndDropSource]:
- *
- * @sample androidx.compose.foundation.samples.TextDragAndDropSourceSample
- * @param block A lambda with a [DragAndDropSourceScope] as a receiver which provides a
- * [PointerInputScope] to detect the drag gesture, after which a drag and drop gesture can be
- * started with [DragAndDropSourceScope.startTransfer].
- */
-fun Modifier.dragAndDropSource(block: suspend DragAndDropSourceScope.() -> Unit): Modifier =
- this then
- DragAndDropSourceWithDefaultShadowElement(
- dragAndDropSourceHandler = block,
- )
-
-private class DragAndDropSourceWithDefaultShadowElement(
- /** @see Modifier.dragAndDropSource */
- val dragAndDropSourceHandler: suspend DragAndDropSourceScope.() -> Unit
-) : ModifierNodeElement<DragSourceNodeWithDefaultPainter>() {
- override fun create() =
- DragSourceNodeWithDefaultPainter(
- dragAndDropSourceHandler = dragAndDropSourceHandler,
- )
-
- override fun update(node: DragSourceNodeWithDefaultPainter) =
- with(node) {
- dragAndDropSourceHandler =
- this@DragAndDropSourceWithDefaultShadowElement.dragAndDropSourceHandler
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "dragSourceWithDefaultPainter"
- properties["dragAndDropSourceHandler"] = dragAndDropSourceHandler
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is DragAndDropSourceWithDefaultShadowElement) return false
-
- return dragAndDropSourceHandler == other.dragAndDropSourceHandler
- }
-
- override fun hashCode(): Int {
- return dragAndDropSourceHandler.hashCode()
+@Immutable
+internal actual object DragAndDropSourceDefaults {
+ actual val DefaultStartDetector: DragAndDropStartDetector = {
+ detectTapGestures(onLongPress = { offset -> requestDragAndDropTransfer(offset) })
}
}
-private class DragSourceNodeWithDefaultPainter(
- var dragAndDropSourceHandler: suspend DragAndDropSourceScope.() -> Unit
-) : DelegatingNode() {
-
- init {
- val cacheDrawScopeDragShadowCallback =
- CacheDrawScopeDragShadowCallback().also {
- delegate(CacheDrawModifierNode(it::cachePicture))
- }
- delegate(
- DragAndDropSourceNode(
- drawDragDecoration = { cacheDrawScopeDragShadowCallback.drawDragShadow(this) },
- dragAndDropSourceHandler = { dragAndDropSourceHandler.invoke(this) }
- )
- )
- }
-}
-
-private class CacheDrawScopeDragShadowCallback {
+internal actual class CacheDrawScopeDragShadowCallback {
private var cachedPicture: Picture? = null
- fun drawDragShadow(drawScope: DrawScope) =
+ actual fun drawDragShadow(drawScope: DrawScope) =
with(drawScope) {
when (val picture = cachedPicture) {
null ->
@@ -111,7 +47,7 @@
}
}
- fun cachePicture(scope: CacheDrawScope): DrawResult =
+ actual fun cachePicture(scope: CacheDrawScope): DrawResult =
with(scope) {
val picture = Picture()
cachedPicture = picture
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt
similarity index 70%
copy from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
copy to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt
index c273ad3..7da1ea3 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.android.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.compose.foundation.gestures
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isDeepPress: Boolean
+ get() = classification == MotionEvent.CLASSIFICATION_DEEP_PRESS
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.android.kt
index 07d110e..04d66f2 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.android.kt
@@ -18,8 +18,8 @@
import androidx.compose.foundation.content.MediaType
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
import androidx.compose.ui.draganddrop.toAndroidDragEvent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ClipEntry
@@ -37,8 +37,8 @@
onChanged: ((event: DragAndDropEvent) -> Unit)?,
onExited: ((event: DragAndDropEvent) -> Unit)?,
onEnded: ((event: DragAndDropEvent) -> Unit)?,
-): DragAndDropModifierNode {
- return DragAndDropModifierNode(
+): DragAndDropTargetModifierNode {
+ return DragAndDropTargetModifierNode(
shouldStartDragAndDrop = { dragAndDropEvent ->
// If there's a receiveContent modifier wrapping around this TextField, initially all
// dragging items should be accepted for drop. This is expected to be met by the caller
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
new file mode 100644
index 0000000..ba7fb8d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/AutoSizeTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class AutoSizeTest {
+ @Test
+ fun stepBased_valid_args() {
+ // we shouldn't throw here
+ AutoSize.StepBased(1.sp, 2.sp, 3.sp)
+
+ AutoSize.StepBased(0.sp, 0.1.sp, 0.0001.sp)
+
+ AutoSize.StepBased(1.em, 2.em, 0.1.em)
+
+ AutoSize.StepBased(2.sp, 1.em, 0.1.sp)
+ }
+
+ @Test
+ fun stepBased_minFontSize_greaterThan_maxFontSize_coercesTo_maxFontSize() {
+ var autoSize1 = AutoSize.StepBased(2.sp, 1.sp)
+ var autoSize2 = AutoSize.StepBased(1.sp, 1.sp)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+
+ autoSize1 = AutoSize.StepBased(3.6.em, 2.em)
+ autoSize2 = AutoSize.StepBased(2.em, 2.em)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_stepSize_tooSmall() {
+ AutoSize.StepBased(0.00000134.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_minFontSize_unspecified() {
+ AutoSize.StepBased(TextUnit.Unspecified, 1.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_maxFontSize_unspecified() {
+ AutoSize.StepBased(2.sp, TextUnit.Unspecified)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_stepSize_unspecified() {
+ AutoSize.StepBased(TextUnit.Unspecified)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_minFontSize_negative() {
+ AutoSize.StepBased((-1).sp, 0.sp)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun stepBased_maxFontSize_negative() {
+ AutoSize.StepBased(0.sp, (-1).sp)
+ }
+
+ @Test
+ fun stepBased_equals() {
+ var autoSize1 = AutoSize.StepBased(1.sp, 10.sp, 2.sp)
+ var autoSize2 = AutoSize.StepBased(1.0.sp, 10.0.sp, 2.0.sp)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.1.sp, 10.sp, 2.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.sp, 11.1.sp, 2.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = AutoSize.StepBased(1.sp, 10.sp, 2.5.sp)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+ assertThat(autoSize2).isNotEqualTo(autoSize1)
+
+ autoSize2 = TestAutoSize(7)
+ assertThat(autoSize1).isNotEqualTo(autoSize2)
+
+ autoSize1 = AutoSize.StepBased(1.em, 2.em, 0.1.em)
+ autoSize2 = AutoSize.StepBased(1.0.em, 2.0.em, 0.1.em)
+ assertThat(autoSize1).isEqualTo(autoSize2)
+ assertThat(autoSize2).isEqualTo(autoSize1)
+ }
+
+ @Test
+ fun stepBased_getFontSize_alwaysOverflows() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = AlwaysOverflows()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(12) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_neverOverflows() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = NeverOverflows()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(112) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_overflowsWhenFontSizeIsGreaterThan60Px() {
+ val autoSize = AutoSize.StepBased(12.sp, 112.sp, 0.25.sp)
+ val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+ with(autoSize) { assertThat(searchScope.getFontSize().value).isEqualTo(60) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_differentStepSizes() {
+ val autoSize1 = AutoSize.StepBased(10.sp, 100.sp, 10.sp)
+ val autoSize2 = AutoSize.StepBased(10.sp, 100.sp, 20.sp)
+ val searchScope: FontSizeSearchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+
+ with(autoSize1) { assertThat(searchScope.getFontSize().value).isEqualTo(60) }
+ with(autoSize2) { assertThat(searchScope.getFontSize().value).isEqualTo(50) }
+ }
+
+ @Test
+ fun stepBased_getFontSize_stepSize_greaterThan_maxFontSize_minus_minFontSize() {
+ // regardless of the bounds of the container, the only potential font size is minFontSize
+ val autoSize = AutoSize.StepBased(45.sp, 55.sp, 15.sp)
+ with(autoSize) {
+ var searchScope: FontSizeSearchScope = AlwaysOverflows()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+
+ searchScope = NeverOverflows()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+
+ searchScope = OverflowsWhenFontSizeIsGreaterThan60px()
+ assertThat(searchScope.getFontSize().value).isEqualTo(45)
+ }
+ }
+
+ private class TestAutoSize(private val testParam: Int) : AutoSize {
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ return if (!performLayoutAndGetOverflow(testParam.sp)) testParam.sp else 3.sp
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is TestAutoSize) return false
+
+ return testParam == other.testParam
+ }
+
+ override fun hashCode(): Int {
+ return testParam
+ }
+ }
+
+ private class AlwaysOverflows : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return true
+ }
+ }
+
+ private class NeverOverflows : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return false
+ }
+ }
+
+ private class OverflowsWhenFontSizeIsGreaterThan60px : FontSizeSearchScope {
+ override val density = 1f
+ override val fontScale = 1f
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ return fontSize.toPx() > 60
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.kt
index d49df0d..ce5ee66 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.kt
@@ -17,9 +17,9 @@
package androidx.compose.foundation.content.internal
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
internal expect fun ReceiveContentDragAndDropNode(
receiveContentConfiguration: ReceiveContentConfiguration,
dragAndDropRequestPermission: (DragAndDropEvent) -> Unit
-): DragAndDropModifierNode
+): DragAndDropTargetModifierNode
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.kt
index 3a08e9e..13d45d1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.kt
@@ -16,15 +16,22 @@
package androidx.compose.foundation.draganddrop
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
+import androidx.compose.ui.draganddrop.DragAndDropSourceModifierNode
import androidx.compose.ui.draganddrop.DragAndDropTransferData
+import androidx.compose.ui.draw.CacheDrawModifierNode
+import androidx.compose.ui.draw.CacheDrawScope
+import androidx.compose.ui.draw.DrawResult
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
@@ -33,16 +40,52 @@
* A scope that allows for the detection of the start of a drag and drop gesture, and subsequently
* starting a drag and drop session.
*/
-interface DragAndDropSourceScope : PointerInputScope {
+internal interface DragAndDropStartDetectorScope : PointerInputScope {
/**
- * Starts a drag and drop session with [transferData] as the data to be transferred on gesture
- * completion
+ * Requests a drag and drop transfer.
+ *
+ * @param offset the offset value representing position of the input pointer.
*/
- fun startTransfer(transferData: DragAndDropTransferData)
+ fun requestDragAndDropTransfer(offset: Offset = Offset.Unspecified)
}
/**
- * A Modifier that allows an element it is applied to to be treated like a source for drag and drop
+ * This typealias represents a suspend function with [DragAndDropStartDetectorScope] that is used to
+ * detect the start of a drag and drop gesture and initiate a drag and drop session.
+ */
+internal typealias DragAndDropStartDetector = suspend DragAndDropStartDetectorScope.() -> Unit
+
+/** Contains the default values used by [Modifier.dragAndDropSource]. */
+@Immutable
+internal expect object DragAndDropSourceDefaults {
+ /**
+ * The default start detector for drag and drop operations. It might vary on different
+ * platforms.
+ */
+ val DefaultStartDetector: DragAndDropStartDetector
+}
+
+/**
+ * A [Modifier] that allows an element it is applied to be treated like a source for drag and drop
+ * operations. It displays the element dragged as a drag shadow.
+ *
+ * Learn how to use [Modifier.dragAndDropSource]:
+ *
+ * @sample androidx.compose.foundation.samples.TextDragAndDropSourceSample
+ * @param transferData A function that receives the current offset of the drag operation and returns
+ * the [DragAndDropTransferData] to be transferred. If null is returned, the drag and drop
+ * transfer won't be started.
+ */
+fun Modifier.dragAndDropSource(transferData: (Offset) -> DragAndDropTransferData?): Modifier =
+ this then
+ DragAndDropSourceWithDefaultShadowElement(
+ // TODO: Expose this as public argument
+ detectDragStart = DragAndDropSourceDefaults.DefaultStartDetector,
+ transferData = transferData
+ )
+
+/**
+ * A [Modifier] that allows an element it is applied to be treated like a source for drag and drop
* operations.
*
* Learn how to use [Modifier.dragAndDropSource] while providing a custom drag shadow:
@@ -50,72 +93,157 @@
* @sample androidx.compose.foundation.samples.DragAndDropSourceWithColoredDragShadowSample
* @param drawDragDecoration provides the visual representation of the item dragged during the drag
* and drop gesture.
- * @param block A lambda with a [DragAndDropSourceScope] as a receiver which provides a
- * [PointerInputScope] to detect the drag gesture, after which a drag and drop gesture can be
- * started with [DragAndDropSourceScope.startTransfer].
+ * @param transferData A function that receives the current offset of the drag operation and returns
+ * the [DragAndDropTransferData] to be transferred. If null is returned, the drag and drop
+ * transfer won't be started.
*/
fun Modifier.dragAndDropSource(
drawDragDecoration: DrawScope.() -> Unit,
- block: suspend DragAndDropSourceScope.() -> Unit
+ transferData: (Offset) -> DragAndDropTransferData?
): Modifier =
this then
DragAndDropSourceElement(
drawDragDecoration = drawDragDecoration,
- dragAndDropSourceHandler = block,
+ // TODO: Expose this as public argument
+ detectDragStart = DragAndDropSourceDefaults.DefaultStartDetector,
+ transferData = transferData
)
private data class DragAndDropSourceElement(
/** @see Modifier.dragAndDropSource */
val drawDragDecoration: DrawScope.() -> Unit,
/** @see Modifier.dragAndDropSource */
- val dragAndDropSourceHandler: suspend DragAndDropSourceScope.() -> Unit
+ val detectDragStart: DragAndDropStartDetector,
+ /** @see Modifier.dragAndDropSource */
+ val transferData: (Offset) -> DragAndDropTransferData?
) : ModifierNodeElement<DragAndDropSourceNode>() {
override fun create() =
DragAndDropSourceNode(
drawDragDecoration = drawDragDecoration,
- dragAndDropSourceHandler = dragAndDropSourceHandler,
+ detectDragStart = detectDragStart,
+ transferData = transferData
)
override fun update(node: DragAndDropSourceNode) =
with(node) {
drawDragDecoration = [email protected]
- dragAndDropSourceHandler = [email protected]
+ detectDragStart = [email protected]
+ transferData = [email protected]
}
override fun InspectorInfo.inspectableProperties() {
name = "dragSource"
properties["drawDragDecoration"] = drawDragDecoration
- properties["dragAndDropSourceHandler"] = dragAndDropSourceHandler
+ properties["detectDragStart"] = detectDragStart
+ properties["transferData"] = transferData
}
}
internal class DragAndDropSourceNode(
var drawDragDecoration: DrawScope.() -> Unit,
- var dragAndDropSourceHandler: suspend DragAndDropSourceScope.() -> Unit
+ var detectDragStart: DragAndDropStartDetector,
+ var transferData: (Offset) -> DragAndDropTransferData?
) : DelegatingNode(), LayoutAwareModifierNode {
private var size: IntSize = IntSize.Zero
- init {
- val dragAndDropModifierNode = delegate(DragAndDropModifierNode())
-
+ private val dragAndDropModifierNode =
delegate(
- SuspendingPointerInputModifierNode {
- dragAndDropSourceHandler(
- object : DragAndDropSourceScope, PointerInputScope by this {
- override fun startTransfer(transferData: DragAndDropTransferData) =
- dragAndDropModifierNode.drag(
- transferData = transferData,
- decorationSize = size.toSize(),
- drawDragDecoration = drawDragDecoration
- )
- }
- )
+ DragAndDropSourceModifierNode { offset ->
+ val transferData = transferData(offset)
+ if (transferData != null) {
+ startDragAndDropTransfer(
+ transferData = transferData,
+ decorationSize = size.toSize(),
+ drawDragDecoration = drawDragDecoration
+ )
+ }
}
)
+
+ private var inputModifierNode: PointerInputModifierNode? = null
+
+ override fun onAttach() {
+ if (dragAndDropModifierNode.isRequestDragAndDropTransferRequired) {
+ inputModifierNode =
+ delegate(
+ SuspendingPointerInputModifierNode {
+ detectDragStart(
+ object : DragAndDropStartDetectorScope, PointerInputScope by this {
+ override fun requestDragAndDropTransfer(offset: Offset) {
+ dragAndDropModifierNode.requestDragAndDropTransfer(offset)
+ }
+ }
+ )
+ }
+ )
+ }
+ }
+
+ override fun onDetach() {
+ inputModifierNode?.let { undelegate(it) }
+ }
+
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ dragAndDropModifierNode.onPlaced(coordinates)
}
override fun onRemeasured(size: IntSize) {
this.size = size
+ dragAndDropModifierNode.onRemeasured(size)
}
}
+
+private data class DragAndDropSourceWithDefaultShadowElement(
+ /** @see Modifier.dragAndDropSource */
+ var detectDragStart: DragAndDropStartDetector,
+ /** @see Modifier.dragAndDropSource */
+ var transferData: (Offset) -> DragAndDropTransferData?
+) : ModifierNodeElement<DragSourceNodeWithDefaultPainter>() {
+ override fun create() =
+ DragSourceNodeWithDefaultPainter(
+ detectDragStart = detectDragStart,
+ transferData = transferData
+ )
+
+ override fun update(node: DragSourceNodeWithDefaultPainter) =
+ with(node) {
+ detectDragStart = [email protected]
+ transferData = [email protected]
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "dragSourceWithDefaultPainter"
+ properties["detectDragStart"] = detectDragStart
+ properties["transferData"] = transferData
+ }
+}
+
+private class DragSourceNodeWithDefaultPainter(
+ detectDragStart: DragAndDropStartDetector,
+ transferData: (Offset) -> DragAndDropTransferData?
+) : DelegatingNode() {
+
+ private val cacheDrawScopeDragShadowCallback =
+ CacheDrawScopeDragShadowCallback().also {
+ delegate(CacheDrawModifierNode(it::cachePicture))
+ }
+
+ private val dragAndDropModifierNode =
+ delegate(
+ DragAndDropSourceNode(
+ drawDragDecoration = { cacheDrawScopeDragShadowCallback.drawDragShadow(this) },
+ detectDragStart = detectDragStart,
+ transferData = transferData
+ )
+ )
+
+ var detectDragStart: DragAndDropStartDetector by dragAndDropModifierNode::detectDragStart
+ var transferData: (Offset) -> DragAndDropTransferData? by dragAndDropModifierNode::transferData
+}
+
+internal expect class CacheDrawScopeDragShadowCallback() {
+ fun drawDragShadow(drawScope: DrawScope)
+
+ fun cachePicture(scope: CacheDrawScope): DrawResult
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
index de3a61b..3263171 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
@@ -18,8 +18,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
@@ -37,8 +37,6 @@
*
* All drag and drop target modifiers in the hierarchy will be given an opportunity to participate
* in a given drag and drop session via [shouldStartDragAndDrop].
- *
- * @see [DragAndDropModifierNode.acceptDragAndDropTransfer]
*/
fun Modifier.dragAndDropTarget(
shouldStartDragAndDrop: (startEvent: DragAndDropEvent) -> Boolean,
@@ -89,7 +87,7 @@
private var target: DragAndDropTarget
) : DelegatingNode() {
- private var dragAndDropNode: DragAndDropModifierNode? = null
+ private var dragAndDropNode: DragAndDropTargetModifierNode? = null
override fun onAttach() {
createAndAttachDragAndDropModifierNode()
@@ -114,7 +112,7 @@
private fun createAndAttachDragAndDropModifierNode() {
dragAndDropNode =
delegate(
- DragAndDropModifierNode(
+ DragAndDropTargetModifierNode(
// We wrap the this.shouldStartDragAndDrop invocation in a lambda as it might
// change over
// time, and updates to shouldStartDragAndDrop are not destructive.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 9d07996..da1086f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -172,7 +172,7 @@
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) =
detectDragGestures(
- onDragStart = { change, _ -> onDragStart(change.position) },
+ onDragStart = { _, slopTriggerChange, _ -> onDragStart(slopTriggerChange.position) },
onDragEnd = { onDragEnd.invoke() },
onDragCancel = onDragCancel,
shouldAwaitTouchSlop = { true },
@@ -200,7 +200,8 @@
*
* @param onDragStart A lambda to be called when the drag gesture starts, it contains information
* about the last known [PointerInputChange] relative to the containing element and the post slop
- * delta.
+ * delta, slopTriggerChange. It also contains information about the down event where this gesture
+ * started and the overSlopOffset.
* @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
* up [PointerInputChange] that finished the gesture.
* @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
@@ -224,7 +225,10 @@
* @see detectDragGesturesAfterLongPress to detect gestures after long press
*/
internal suspend fun PointerInputScope.detectDragGestures(
- onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragStart:
+ (
+ down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset
+ ) -> Unit,
onDragEnd: (change: PointerInputChange) -> Unit,
onDragCancel: () -> Unit,
shouldAwaitTouchSlop: () -> Boolean,
@@ -242,7 +246,6 @@
}
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
- var initialDelta = Offset.Zero
overSlop = Offset.Zero
if (awaitTouchSlop) {
@@ -257,13 +260,12 @@
overSlop = over
}
} while (drag != null && !drag.isConsumed)
- initialDelta = overSlop
} else {
drag = initialDown
}
if (drag != null) {
- onDragStart.invoke(drag, initialDelta)
+ onDragStart.invoke(down, drag, overSlop)
onDrag(drag, overSlop)
val upEvent =
drag(
@@ -879,6 +881,7 @@
var currentDown = initialDown
val longPressTimeout = viewConfiguration.longPressTimeoutMillis
return try {
+ var deepPress = false
// wait for first tap up or long press
withTimeout(longPressTimeout) {
var finished = false
@@ -897,6 +900,11 @@
finished = true // Canceled
}
+ if (event.isDeepPress) {
+ deepPress = true
+ finished = true
+ }
+
// Check for cancel by position consumption. We can look on the Final pass of
// the existing pointer event because it comes after the Main pass we checked
// above.
@@ -919,7 +927,13 @@
}
}
}
- null
+ // If we finished early because of a deep press, return the relevant change as this counts
+ // as a long press
+ if (deepPress) {
+ longPress ?: initialDown
+ } else {
+ null
+ }
} catch (_: PointerEventTimeoutCancellationException) {
longPress ?: initialDown
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index f0490ff..82ad253 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -48,7 +48,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
@@ -463,23 +462,27 @@
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
- val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
- { startEvent, initialDelta ->
- if (canDrag.invoke(startEvent)) {
+
+ val onDragStart:
+ (
+ down: PointerInputChange,
+ slopTriggerChange: PointerInputChange,
+ postSlopOffset: Offset
+ ) -> Unit =
+ { down, slopTriggerChange, postSlopOffset ->
+ if (canDrag.invoke(down)) {
if (!isListeningForEvents) {
if (channel == null) {
channel = Channel(capacity = Channel.UNLIMITED)
}
startListeningForEvents()
}
- val overSlopOffset = initialDelta
- val xSign = sign(startEvent.position.x)
- val ySign = sign(startEvent.position.y)
- val adjustedStart =
- startEvent.position -
- Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-
- channel?.trySend(DragStarted(adjustedStart))
+ velocityTracker.addPointerInputChange(down)
+ val dragStartedOffset = slopTriggerChange.position - postSlopOffset
+ // the drag start event offset is the down event + touch slop value
+ // or in this case the event that triggered the touch slop minus
+ // the post slop offset
+ channel?.trySend(DragStarted(dragStartedOffset))
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index f875f45..753eb22 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -111,31 +111,39 @@
}
}
if (onPress !== NoPressGesture) launch { pressScope.onPress(down.position) }
- val longPressTimeout =
- onLongPress?.let { viewConfiguration.longPressTimeoutMillis } ?: (Long.MAX_VALUE / 2)
- var upOrCancel: PointerInputChange? = null
- var cancelOrReleaseJob: Job?
- try {
- // wait for first tap up or long press
- upOrCancel = withTimeout(longPressTimeout) { waitForUpOrCancellation() }
- if (upOrCancel == null) {
- cancelOrReleaseJob =
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- // tap-up was canceled
- pressScope.cancel()
+ val upOrCancel: PointerInputChange?
+ val cancelOrReleaseJob: Job?
+
+ // wait for first tap up or long press
+ if (onLongPress == null) {
+ upOrCancel = waitForUpOrCancellation()
+ } else {
+ upOrCancel =
+ when (val longPressResult = waitForLongPress()) {
+ LongPressResult.Success -> {
+ onLongPress.invoke(down.position)
+ consumeUntilUp()
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.release()
+ }
+ // End the current gesture
+ return@awaitEachGesture
}
- } else {
- upOrCancel.consume()
- cancelOrReleaseJob =
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.release()
- }
- }
- } catch (_: PointerEventTimeoutCancellationException) {
- onLongPress?.invoke(down.position)
- consumeUntilUp()
+ is LongPressResult.Released -> longPressResult.finalUpChange
+ is LongPressResult.Canceled -> null
+ }
+ }
+
+ if (upOrCancel == null) {
+ cancelOrReleaseJob =
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ // tap-up was canceled
+ pressScope.cancel()
+ }
+ } else {
+ upOrCancel.consume()
cancelOrReleaseJob =
launch(start = coroutineStartForCurrentDispatchBehavior) {
awaitResetOrSkip()
@@ -157,45 +165,54 @@
// Second tap down detected
resetJob =
launch(start = coroutineStartForCurrentDispatchBehavior) {
- cancelOrReleaseJob?.join()
+ cancelOrReleaseJob.join()
pressScope.reset()
}
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(secondDown.position) }
}
- try {
- // Might have a long second press as the second tap
- withTimeout(longPressTimeout) {
- val secondUp = waitForUpOrCancellation()
- if (secondUp != null) {
- secondUp.consume()
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.release()
+ // Might have a long second press as the second tap
+ val secondUp =
+ if (onLongPress == null) {
+ waitForUpOrCancellation()
+ } else {
+ when (val longPressResult = waitForLongPress()) {
+ LongPressResult.Success -> {
+ // The first tap was valid, but the second tap is a long press -
+ // we
+ // intentionally do not invoke onClick() for the first tap,
+ // since the 'main'
+ // gesture here is a long press, which canceled the double tap
+ // / tap.
+
+ // notify for the long press
+ onLongPress.invoke(secondDown.position)
+ consumeUntilUp()
+
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.release()
+ }
+ return@awaitEachGesture
}
- onDoubleTap(secondUp.position)
- } else {
- launch(start = coroutineStartForCurrentDispatchBehavior) {
- awaitResetOrSkip()
- pressScope.cancel()
- }
- onTap?.invoke(upOrCancel.position)
+ is LongPressResult.Released -> longPressResult.finalUpChange
+ is LongPressResult.Canceled -> null
}
}
- } catch (e: PointerEventTimeoutCancellationException) {
- // The first tap was valid, but the second tap is a long press - we
- // intentionally do not invoke onClick() for the first tap, since the 'main'
- // gesture here is a long press, which cancelled the double tap / tap.
-
- // notify for the long press
- onLongPress?.invoke(secondDown.position)
- consumeUntilUp()
-
+ if (secondUp != null) {
+ secondUp.consume()
launch(start = coroutineStartForCurrentDispatchBehavior) {
awaitResetOrSkip()
pressScope.release()
}
+ onDoubleTap(secondUp.position)
+ } else {
+ launch(start = coroutineStartForCurrentDispatchBehavior) {
+ awaitResetOrSkip()
+ pressScope.cancel()
+ }
+ onTap?.invoke(upOrCancel.position)
}
}
}
@@ -318,6 +335,12 @@
waitForUpOrCancellation(PointerEventPass.Main)
/**
+ * Whether the event is considered a deep press, and should trigger long click before the timeout
+ * has been reached.
+ */
+internal expect val PointerEvent.isDeepPress: Boolean
+
+/**
* Reads events in the given [pass] until all pointers are up or the gesture was canceled. The
* gesture is considered canceled when a pointer leaves the event region, a position change has been
* consumed or a pointer down change event was already consumed in the given pass. If the gesture
@@ -348,6 +371,66 @@
}
}
+/**
+ * Reads events in the given [pass] until all pointers are up or the gesture was canceled. The
+ * gesture is considered canceled when a pointer leaves the event region, a position change has been
+ * consumed or a pointer down change event was already consumed in the given pass. If the gesture
+ * was not canceled, the final up change is returned or `null` if the event was canceled.
+ */
+internal suspend fun AwaitPointerEventScope.waitForLongPress(
+ pass: PointerEventPass = PointerEventPass.Main
+): LongPressResult {
+ var result: LongPressResult = LongPressResult.Canceled
+ try {
+ withTimeout(viewConfiguration.longPressTimeoutMillis) {
+ while (true) {
+ val event = awaitPointerEvent(pass)
+ if (event.changes.fastAll { it.changedToUp() }) {
+ // All pointers are up
+ result = LongPressResult.Released(event.changes[0])
+ break
+ }
+
+ if (event.isDeepPress) {
+ result = LongPressResult.Success
+ break
+ }
+
+ if (
+ event.changes.fastAny {
+ it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
+ }
+ ) {
+ result = LongPressResult.Canceled
+ break
+ }
+
+ // Check for cancel by position consumption. We can look on the Final pass of the
+ // existing pointer event because it comes after the pass we checked above.
+ val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+ if (consumeCheck.changes.fastAny { it.isConsumed }) {
+ result = LongPressResult.Canceled
+ break
+ }
+ }
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ return LongPressResult.Success
+ }
+ return result
+}
+
+internal sealed class LongPressResult {
+ /** Long press was triggered */
+ object Success : LongPressResult()
+
+ /** All pointers were released without long press being triggered */
+ class Released(val finalUpChange: PointerInputChange) : LongPressResult()
+
+ /** The gesture was canceled */
+ object Canceled : LongPressResult()
+}
+
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn("This API feature-flags new behavior and will be removed in the future.")
annotation class ExperimentalTapGestureDetectorBehaviorApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index f03d8b3..f69394e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -422,10 +422,17 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- // place normal items
- positionedItems.fastForEach { it.place(this, isLookingAhead) }
- // stickingItems should be placed after all other items
- stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ // place normal items
+ positionedItems.fastForEach { it.place(this, isLookingAhead) }
+ // stickingItems should be placed after all other items
+ stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ }
// we attach it during the placement so LazyListState can trigger re-placement
placementScopeInvalidator.attachToScope()
@@ -610,7 +617,7 @@
fun Int.reverseAware() = if (!reverseLayout) this else itemsCount - this - 1
val sizes = IntArray(itemsCount) { index -> items[index.reverseAware()].size }
- val offsets = IntArray(itemsCount) { 0 }
+ val offsets = IntArray(itemsCount)
if (isVertical) {
with(
requirePreconditionNotNull(verticalArrangement) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
index bcee59d..ca3fd35 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
@@ -170,7 +170,9 @@
repeat(placeableOffsets.size) { index ->
// placeableOffsets consist of x and y pairs for each placeable.
// if isVertical is true then the main axis offsets are located at indexes 1, 3, 5 etc.
- if ((isVertical && index % 2 == 1) || (!isVertical && index % 2 == 0)) {
+ // 1 when odd, 0 when even
+ val oddEven = index and 1
+ if ((isVertical && oddEven != 0) || (!isVertical && oddEven == 0)) {
placeableOffsets[index] += delta
}
}
@@ -255,7 +257,7 @@
get() = if (isVertical) height else width
private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
- IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
+ if (isVertical) IntOffset(x, mainAxisMap(y)) else IntOffset(mainAxisMap(x), y)
}
private const val Unset = Int.MIN_VALUE
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollScope.kt
similarity index 68%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollScope.kt
index 9b02c77..812f04a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollScope.kt
@@ -17,25 +17,23 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.grid.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.pager.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastSumBy
/**
- * An implementation of [LazyLayoutAnimateScrollScope] that can be used with LazyLists. Please refer
- * to the sample to learn how to use this API.
+ * An implementation of [LazyLayoutScrollScope] that can be used with LazyLists. Please refer to the
+ * sample to learn how to use this API.
*
- * @param state The [LazyListState] associated with the layout where this animated scroll should be
+ * @param state The [LazyListState] associated with the layout where this custom scroll should be
* performed.
- * @return An implementation of [LazyLayoutAnimateScrollScope] that works with [LazyRow] and
- * [LazyColumn].
- * @sample androidx.compose.foundation.samples.CustomLazyListAnimateToItemScrollSample
+ * @param scrollScope The base [ScrollScope] where the scroll session was created.
+ * @return An implementation of [LazyLayoutScrollScope] that works with [LazyRow] and [LazyColumn].
+ * @sample androidx.compose.foundation.samples.LazyListCustomScrollUsingLazyLayoutScrollScopeSample
*/
-fun LazyLayoutAnimateScrollScope(state: LazyListState): LazyLayoutAnimateScrollScope {
+fun LazyLayoutScrollScope(state: LazyListState, scrollScope: ScrollScope): LazyLayoutScrollScope {
- return object : LazyLayoutAnimateScrollScope {
+ return object : LazyLayoutScrollScope, ScrollScope by scrollScope {
override val firstVisibleItemIndex: Int
get() = state.firstVisibleItemIndex
@@ -48,21 +46,21 @@
override val itemCount: Int
get() = state.layoutInfo.totalItemsCount
- override fun ScrollScope.snapToItem(index: Int, offset: Int) {
+ override fun snapToItem(index: Int, offset: Int) {
state.snapToItemIndexInternal(index, offset, forceRemeasure = true)
}
override fun calculateDistanceTo(targetIndex: Int, targetOffset: Int): Int {
val layoutInfo = state.layoutInfo
if (layoutInfo.visibleItemsInfo.isEmpty()) return 0
- val visibleItem =
- layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
- return if (visibleItem == null) {
+ return if (targetIndex !in firstVisibleItemIndex..lastVisibleItemIndex) {
val averageSize = calculateVisibleItemsAverageSize(layoutInfo)
val indexesDiff = targetIndex - firstVisibleItemIndex
(averageSize * indexesDiff) - firstVisibleItemScrollOffset
} else {
- visibleItem.offset
+ val visibleItem =
+ layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
+ visibleItem?.offset ?: 0
} + targetOffset
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 04dd513..42d4be0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -286,8 +286,6 @@
}
}
- private val animatedScrollScope = LazyLayoutAnimateScrollScope(this)
-
/** Stores currently pinned items which are always composed. */
internal val pinnedItems = LazyLayoutPinnedItemList()
@@ -486,13 +484,8 @@
*/
suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
scroll {
- animatedScrollScope.animateScrollToItem(
- index,
- scrollOffset,
- NumberOfItemsToTeleport,
- density,
- this
- )
+ LazyLayoutScrollScope(this@LazyListState, this)
+ .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density, this)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 22d00c9..97d75c3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -390,8 +390,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { it.place(this) }
- stickingItems.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { it.place(this) }
+ stickingItems.fastForEach { it.place(this) }
+ }
// we attach it during the placement so LazyGridState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
@@ -472,7 +479,7 @@
fun Int.reverseAware() = if (!reverseLayout) this else linesCount - this - 1
val sizes = IntArray(linesCount) { index -> lines[index.reverseAware()].mainAxisSize }
- val offsets = IntArray(linesCount) { 0 }
+ val offsets = IntArray(linesCount)
if (isVertical) {
with(requirePreconditionNotNull(verticalArrangement) { "null verticalArrangement" }) {
density.arrange(mainAxisLayoutSize, sizes, offsets)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollScope.kt
similarity index 79%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollScope.kt
index 46353b5..7174f5c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollScope.kt
@@ -18,22 +18,22 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.pager.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.ui.util.fastFirstOrNull
import kotlin.math.max
/**
- * An implementation of [LazyLayoutAnimateScrollScope] that can be used with LazyGrids.
+ * An implementation of [LazyLayoutScrollScope] that can be used with LazyGrids.
*
- * @param state The [LazyGridState] associated with the layout where this animated scroll should be
+ * @param state The [LazyGridState] associated with the layout where this custom scroll should be
* performed.
- * @return An implementation of [LazyLayoutAnimateScrollScope] that works with [LazyHorizontalGrid]
- * and [LazyVerticalGrid].
- * @sample androidx.compose.foundation.samples.CustomLazyGridAnimateToItemScrollSample
+ * @param scrollScope The base [ScrollScope] where the scroll session was created.
+ * @return An implementation of [LazyLayoutScrollScope] that works with [LazyHorizontalGrid] and
+ * [LazyVerticalGrid].
+ * @sample androidx.compose.foundation.samples.LazyGridCustomScrollUsingLazyLayoutScrollScopeSample
*/
-fun LazyLayoutAnimateScrollScope(state: LazyGridState): LazyLayoutAnimateScrollScope {
- return object : LazyLayoutAnimateScrollScope {
+fun LazyLayoutScrollScope(state: LazyGridState, scrollScope: ScrollScope): LazyLayoutScrollScope {
+ return object : LazyLayoutScrollScope, ScrollScope by scrollScope {
override val firstVisibleItemIndex: Int
get() = state.firstVisibleItemIndex
@@ -46,17 +46,14 @@
override val itemCount: Int
get() = state.layoutInfo.totalItemsCount
- override fun ScrollScope.snapToItem(index: Int, offset: Int) {
+ override fun snapToItem(index: Int, offset: Int) {
state.snapToItemIndexInternal(index, offset, forceRemeasure = true)
}
override fun calculateDistanceTo(targetIndex: Int, targetOffset: Int): Int {
val layoutInfo = state.layoutInfo
if (layoutInfo.visibleItemsInfo.isEmpty()) return 0
- val visibleItem =
- layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
-
- return if (visibleItem == null) {
+ return if (targetIndex !in firstVisibleItemIndex..lastVisibleItemIndex) {
val slotsPerLine = state.slotsPerLine
val averageLineMainAxisSize = calculateLineAverageMainAxisSize(layoutInfo)
val before = targetIndex < firstVisibleItemIndex
@@ -65,11 +62,13 @@
(slotsPerLine - 1) * if (before) -1 else 1) / slotsPerLine
(averageLineMainAxisSize * linesDiff) - firstVisibleItemScrollOffset
} else {
+ val visibleItem =
+ layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == targetIndex }
if (layoutInfo.orientation == Orientation.Vertical) {
- visibleItem.offset.y
+ visibleItem?.offset?.y
} else {
- visibleItem.offset.x
- }
+ visibleItem?.offset?.x
+ } ?: 0
} + targetOffset
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index 08e88a4..db02048 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -276,8 +276,6 @@
}
}
- private val animateScrollScope = LazyLayoutAnimateScrollScope(this)
-
/** Stores currently pinned items which are always composed. */
internal val pinnedItems = LazyLayoutPinnedItemList()
@@ -449,13 +447,8 @@
*/
suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) {
scroll {
- animateScrollScope.animateScrollToItem(
- index,
- scrollOffset,
- numOfItemsToTeleport,
- density,
- this
- )
+ LazyLayoutScrollScope(this@LazyGridState, this)
+ .animateScrollToItem(index, scrollOffset, numOfItemsToTeleport, density, this)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
index d91adff..482ae1f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
@@ -182,7 +182,7 @@
}
}
- val accumulatedOffsetPerLane = IntArray(laneCount) { 0 }
+ val accumulatedOffsetPerLane = IntArray(laneCount)
if (shouldSetupAnimation && previousKeyToIndexMap != null) {
if (movingInFromStartBound.isNotEmpty()) {
movingInFromStartBound.sortByDescending { previousKeyToIndexMap.getIndex(it.key) }
@@ -315,7 +315,7 @@
val itemInfo = keyToItemInfoMap[item.key]!!
val accumulatedOffset = accumulatedOffsetPerLane.updateAndReturnOffsetFor(item)
val mainAxisOffset =
- if (isLookingAhead) positionedItems.last().let { it.mainAxisOffset }
+ if (isLookingAhead) positionedItems.last().mainAxisOffset
else {
itemInfo.layoutMaxOffset - item.mainAxisSizeWithSpacings
} + accumulatedOffset
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutScrollScope.kt
similarity index 94%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutScrollScope.kt
index c889773..8d8f1e2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutAnimateScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutScrollScope.kt
@@ -45,17 +45,17 @@
}
/**
- * A scope to allow customization of animated scroll in LazyLayouts. This scope contains all needed
- * information to perform an animatedScroll in a scrollable LazyLayout.
+ * A [ScrollScope] to allow customization of scroll sessions in LazyLayouts. This scope contains
+ * additional information to perform a custom scroll session in a scrollable LazyLayout.
*
* For implementations for the most common layouts see:
*
- * @see androidx.compose.foundation.lazy.grid.LazyLayoutAnimateScrollScope
- * @see androidx.compose.foundation.lazy.staggeredgrid.LazyLayoutAnimateScrollScope
- * @see androidx.compose.foundation.lazy.LazyLayoutAnimateScrollScope
- * @see androidx.compose.foundation.pager.LazyLayoutAnimateScrollScope
+ * @see androidx.compose.foundation.lazy.grid.LazyLayoutScrollScope
+ * @see androidx.compose.foundation.lazy.staggeredgrid.LazyLayoutScrollScope
+ * @see androidx.compose.foundation.lazy.LazyLayoutScrollScope
+ * @see androidx.compose.foundation.pager.LazyLayoutScrollScope
*/
-interface LazyLayoutAnimateScrollScope {
+interface LazyLayoutScrollScope : ScrollScope {
/** The index of the first visible item in the lazy layout. */
val firstVisibleItemIndex: Int
@@ -78,7 +78,7 @@
* @param index The position index where we should immediately snap to.
* @param offset The offset where we should immediately snap to.
*/
- fun ScrollScope.snapToItem(index: Int, offset: Int = 0)
+ fun snapToItem(index: Int, offset: Int = 0)
/**
* The "expected" distance to [targetIndex]. This means the "expected" offset of [targetIndex]
@@ -94,11 +94,11 @@
fun calculateDistanceTo(targetIndex: Int, targetOffset: Int = 0): Int
}
-internal fun LazyLayoutAnimateScrollScope.isItemVisible(index: Int): Boolean {
+internal fun LazyLayoutScrollScope.isItemVisible(index: Int): Boolean {
return index in firstVisibleItemIndex..lastVisibleItemIndex
}
-internal suspend fun LazyLayoutAnimateScrollScope.animateScrollToItem(
+internal suspend fun LazyLayoutScrollScope.animateScrollToItem(
index: Int,
scrollOffset: Int,
numOfItemsForTeleport: Int,
@@ -171,6 +171,7 @@
var prevValue = 0f
anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
// If we haven't found the item yet, check if it's visible.
+ debugLog { "firstVisibleItemIndex=$firstVisibleItemIndex" }
if (!isItemVisible(index)) {
// Springs can overshoot their target, clamp to the desired range
val coercedValue =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 15bba67..81a4eb3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -33,10 +33,10 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.max
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastForEachReversed
+import androidx.compose.ui.util.fastJoinToString
import androidx.compose.ui.util.fastMaxOfOrNull
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.packInts
@@ -783,6 +783,8 @@
it - beforeContentPadding + afterContentPadding
}
+ debugLog { "pinned items: $pinnedItems" }
+
var extraItemOffset = itemScrollOffsets[0]
val extraItemsBefore =
calculateExtraItems(
@@ -799,16 +801,24 @@
when (lane) {
Unset,
FullSpan -> {
- firstItemIndices.all { it > itemIndex }
+ measuredItems.all {
+ val firstIndex = it.firstOrNull()?.index ?: -1
+ firstIndex > itemIndex
+ }
}
else -> {
- firstItemIndices[lane] > itemIndex
+ val firstIndex = measuredItems[lane].firstOrNull()?.index ?: -1
+ firstIndex > itemIndex
}
}
},
beforeVisibleBounds = true
)
+ debugLog {
+ "extra items before: ${extraItemsBefore.fastJoinToString { it.index.toString() }}"
+ }
+
val visibleItems =
calculateVisibleItems(
measuredItems,
@@ -847,6 +857,10 @@
beforeVisibleBounds = false
)
+ debugLog {
+ "extra items after: ${extraItemsAfter.fastJoinToString { it.index.toString() }}"
+ }
+
val positionedItems = mutableListOf<LazyStaggeredGridMeasuredItem>()
positionedItems.addAll(extraItemsBefore)
positionedItems.addAll(visibleItems)
@@ -897,8 +911,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { item ->
- item.place(scope = this, context = this@measure)
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { item ->
+ item.place(scope = this, context = this@measure)
+ }
}
// we attach it during the placement so LazyStaggeredGridState can trigger
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollScope.kt
similarity index 80%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollScope.kt
index 88978cc..c0ed4b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollScope.kt
@@ -18,24 +18,26 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.grid.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
-import androidx.compose.foundation.pager.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastSumBy
/**
- * An implementation of [LazyLayoutAnimateScrollScope] that can be used with LazyStaggeredGrids.
+ * An implementation of [LazyLayoutScrollScope] that can be used with LazyStaggeredGrids.
*
- * @param state The [LazyStaggeredGridState] associated with the layout where this animated scroll
+ * @param state The [LazyStaggeredGridState] associated with the layout where this custom scroll
* should be performed.
- * @return An implementation of [LazyLayoutAnimateScrollScope] that works with
+ * @param scrollScope The base [ScrollScope] where the scroll session was created.
+ * @return An implementation of [LazyLayoutScrollScope] that works with
* [LazyHorizontalStaggeredGrid] and [LazyVerticalStaggeredGrid].
- * @sample androidx.compose.foundation.samples.CustomLazyStaggeredGridAnimateToItemScrollSample
+ * @sample androidx.compose.foundation.samples.LazyStaggeredGridCustomScrollUsingLazyLayoutScrollScopeSample
*/
-fun LazyLayoutAnimateScrollScope(state: LazyStaggeredGridState): LazyLayoutAnimateScrollScope {
+fun LazyLayoutScrollScope(
+ state: LazyStaggeredGridState,
+ scrollScope: ScrollScope
+): LazyLayoutScrollScope {
- return object : LazyLayoutAnimateScrollScope {
+ return object : LazyLayoutScrollScope, ScrollScope by scrollScope {
override val firstVisibleItemIndex: Int
get() = state.firstVisibleItemIndex
@@ -48,7 +50,7 @@
override val itemCount: Int
get() = state.layoutInfo.totalItemsCount
- override fun ScrollScope.snapToItem(index: Int, offset: Int) {
+ override fun snapToItem(index: Int, offset: Int) {
with(state) { snapToItemInternal(index, offset, forceRemeasure = true) }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index f1b227a..f447666 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -28,7 +28,6 @@
import androidx.compose.foundation.internal.checkPrecondition
import androidx.compose.foundation.internal.requirePrecondition
import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
@@ -215,8 +214,6 @@
/** backing field mutable field for [interactionSource] */
internal val mutableInteractionSource = MutableInteractionSource()
- private val animateScrollScope = LazyLayoutAnimateScrollScope(this)
-
/** Stores currently pinned items which are always composed. */
internal val pinnedItems = LazyLayoutPinnedItemList()
@@ -327,13 +324,14 @@
val layoutInfo = layoutInfoState.value
val numOfItemsToTeleport = 100 * layoutInfo.slots.sizes.size
scroll {
- animateScrollScope.animateScrollToItem(
- index,
- scrollOffset,
- numOfItemsToTeleport,
- layoutInfo.density,
- this
- )
+ LazyLayoutScrollScope(this@LazyStaggeredGridState, this)
+ .animateScrollToItem(
+ index,
+ scrollOffset,
+ numOfItemsToTeleport,
+ layoutInfo.density,
+ this
+ )
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 1333583..27ef456 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -465,7 +465,14 @@
firstVisiblePageScrollOffset = currentFirstPageScrollOffset,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedPages.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedPages.fastForEach { it.place(this) }
+ }
// we attach it during the placement so PagerState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
@@ -632,7 +639,7 @@
fun Int.reverseAware() = if (!reverseLayout) this else pagesCount - this - 1
val sizes = IntArray(pagesCount) { pageAvailableSize }
- val offsets = IntArray(pagesCount) { 0 }
+ val offsets = IntArray(pagesCount)
val arrangement = spacedBy(spaceBetweenPages.toDp())
if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollScope.kt
similarity index 70%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollScope.kt
index 695d831..d6f6312 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLazyAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollScope.kt
@@ -17,22 +17,23 @@
package androidx.compose.foundation.pager
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import kotlin.math.roundToInt
/**
- * A [LazyLayoutAnimateScrollScope] that allows customization of animated scroll in [Pager]. The
- * scope contains information about the layout where animated scroll can be performed as well as the
+ * A [LazyLayoutScrollScope] that allows customization of animated scroll in [Pager]. The scope
+ * contains information about the layout where animated scroll can be performed as well as the
* necessary tools to do that respecting the scroll mutation priority.
*
* @param state The [PagerState] associated with the layout where this animated scroll should be
* performed.
- * @return An implementation of [LazyLayoutAnimateScrollScope] that works with [HorizontalPager] and
+ * @param scrollScope The base [ScrollScope] where the scroll session was created.
+ * @return An implementation of [LazyLayoutScrollScope] that works with [HorizontalPager] and
* [VerticalPager].
- * @sample androidx.compose.foundation.samples.CustomPagerAnimateToPageScrollSample
+ * @sample androidx.compose.foundation.samples.PagerCustomScrollUsingLazyLayoutScrollScopeSample
*/
-fun LazyLayoutAnimateScrollScope(state: PagerState): LazyLayoutAnimateScrollScope {
- return object : LazyLayoutAnimateScrollScope {
+fun LazyLayoutScrollScope(state: PagerState, scrollScope: ScrollScope): LazyLayoutScrollScope {
+ return object : LazyLayoutScrollScope, ScrollScope by scrollScope {
override val firstVisibleItemIndex: Int
get() = state.firstVisiblePage
@@ -46,7 +47,7 @@
override val itemCount: Int
get() = state.pageCount
- override fun ScrollScope.snapToItem(index: Int, offset: Int) {
+ override fun snapToItem(index: Int, offset: Int) {
val offsetFraction = offset / state.pageSizeWithSpacing.toFloat()
state.snapToItem(index, offsetFraction, forceRemeasure = true)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index cffd236..68c6f453 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -32,10 +32,10 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.internal.requirePrecondition
import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
-import androidx.compose.foundation.lazy.layout.LazyLayoutAnimateScrollScope
import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
import androidx.compose.foundation.lazy.layout.PrefetchScheduler
import androidx.compose.runtime.Composable
@@ -454,8 +454,6 @@
/** Constraints passed to the prefetcher for premeasuring the prefetched items. */
internal var premeasureConstraints = Constraints()
- private val animateScrollScope = LazyLayoutAnimateScrollScope(this)
-
/** Stores currently pinned pages which are always composed, used by for beyond bound pages. */
internal val pinnedPages = LazyLayoutPinnedItemList()
@@ -589,13 +587,14 @@
val targetPageOffsetToSnappedPosition = (pageOffsetFraction * pageSizeWithSpacing)
scroll {
- animateScrollScope.animateScrollToPage(
- targetPage,
- targetPageOffsetToSnappedPosition,
- animationSpec,
- updateTargetPage = { updateTargetPage(it) },
- this
- )
+ LazyLayoutScrollScope(this@PagerState, this)
+ .animateScrollToPage(
+ targetPage,
+ targetPageOffsetToSnappedPosition,
+ animationSpec,
+ updateTargetPage = { updateTargetPage(it) },
+ this
+ )
}
}
@@ -911,7 +910,7 @@
.toLong()
}
-private suspend fun LazyLayoutAnimateScrollScope.animateScrollToPage(
+private suspend fun LazyLayoutScrollScope.animateScrollToPage(
targetPage: Int,
targetPageOffsetToSnappedPosition: Float,
animationSpec: AnimationSpec<Float>,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
new file mode 100644
index 0000000..c900e10
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/AutoSize.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.sp
+import kotlin.math.floor
+
+/** Interface used by Text composables to automatically size their text. */
+internal interface AutoSize {
+ /**
+ * Calculates font size. Use utility function [FontSizeSearchScope.performLayoutAndGetOverflow]
+ * to lay out the text and check if it overflows. The expectation is that
+ * implementation-specific constraints should be used in unison with
+ * [FontSizeSearchScope.performLayoutAndGetOverflow] to determine a suitable font size to be
+ * used.
+ *
+ * @return The derived optimal font size.
+ * @see [FontSizeSearchScope.performLayoutAndGetOverflow]
+ */
+ // TODO(b/362904946): Add sample
+ fun FontSizeSearchScope.getFontSize(): TextUnit
+
+ /**
+ * Require equals() to be implemented for performance purposes. Using a data class is
+ * sufficient. Singletons may implement this function with referential equality (`this ===
+ * other`). Instances with no properties may implement this function by checking the type of the
+ * other object.
+ *
+ * @return true if both AutoSize instances are structurally identical.
+ */
+ override fun equals(other: Any?): Boolean
+
+ companion object {
+ /**
+ * Automatically size the text to attempt to fit its container. This uses a
+ * step-based/granular implementation where potential font sizes are uniformly spread out
+ * between [minFontSize] and [maxFontSize]. [stepSize] is the smallest difference between
+ * two distinct font sizes. e.g. if `minFontSize = 1.sp`, `maxFontSize = 2.sp` and `stepSize
+ * = 0.5.sp`, the potential font sizes are `1.sp`, `1.5.sp`, and `2.sp`. In cases where
+ * [stepSize] is strictly greater than (not equal to) the difference between [minFontSize]
+ * and [maxFontSize], the only potential font size is [minFontSize].
+ *
+ * Both or neither [minFontSize] and [maxFontSize] must be declared.
+ *
+ * @param minFontSize The smallest potential font size of the text. Default = 12.sp. This
+ * must be smaller than [maxFontSize]; an [IllegalArgumentException] will be thrown
+ * otherwise.
+ * @param maxFontSize The largest potential font size of the text. Default = 112.sp. This
+ * must be larger than [minFontSize]; an [IllegalArgumentException] will be thrown
+ * otherwise.
+ * @param stepSize The smallest difference between potential font sizes. Specifically, every
+ * font size, when subtracted by [minFontSize], is divisible by [stepSize]. Default =
+ * 0.25.sp. This must not be less than `0.0001f.sp`; an [IllegalArgumentException] will be
+ * thrown otherwise.
+ * @return AutoSize instance with the step-based configuration. Using this in a compatible
+ * composable will cause its text to be sized as above.
+ */
+ fun StepBased(
+ minFontSize: TextUnit,
+ maxFontSize: TextUnit,
+ stepSize: TextUnit = 0.25.sp
+ ): AutoSize {
+ return AutoSizeStepBased(minFontSize, maxFontSize, stepSize)
+ }
+
+ /**
+ * Automatically size the text to attempt to fit its container. This uses a
+ * step-based/granular implementation where potential font sizes are uniformly spread out
+ * between `minFontSize` and `maxFontSize`. [stepSize] is the smallest difference between
+ * two distinct font sizes. e.g. if `minFontSize = 1.sp`, `maxFontSize = 2.sp` and `stepSize
+ * = 0.5.sp`, the potential font sizes are `1.sp`, `1.5.sp`, and `2.sp`. In cases where
+ * [stepSize] is strictly greater than (not equal to) the difference between `minFontSize`
+ * and `maxFontSize`, the only potential font size is `minFontSize`.
+ *
+ * Both or neither `minFontSize` and `maxFontSize` must be declared.
+ *
+ * @param stepSize The smallest difference between potential font sizes. Specifically, every
+ * font size, when subtracted by `minFontSize` (`12.sp`), is divisible by [stepSize].
+ * Default = 0.25.sp. [stepSize] must not be less than `0.0001f.sp`, an
+ * [IllegalArgumentException] will be thrown otherwise.
+ * @return AutoSize instance with the step-based configuration. Using this in a compatible
+ * composable will cause its text to be sized as above.
+ */
+ fun StepBased(stepSize: TextUnit = 0.25.sp): AutoSize {
+ return AutoSizeStepBased(minFontSize = 12.sp, maxFontSize = 112.sp, stepSize = stepSize)
+ }
+ }
+}
+
+/**
+ * This interface is used by classes responsible for laying out text. Layout will be performed here
+ * alongside logic that checks if the text overflows.
+ *
+ * These methods are used by [AutoSize] in the [AutoSize.getFontSize] method, where developers can
+ * lay out text with different font sizes and do certain logic depending on whether or not the text
+ * overflows.
+ *
+ * This may be implemented in unit tests when testing [AutoSize.getFontSize] to see if the method
+ * works as intended.
+ */
+internal interface FontSizeSearchScope : Density {
+ /**
+ * Lays out the text with the given font size.
+ *
+ * @return true if the text overflows.
+ */
+ fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean
+}
+
+private class AutoSizeStepBased(
+ private var minFontSize: TextUnit,
+ private val maxFontSize: TextUnit,
+ private val stepSize: TextUnit
+) : AutoSize {
+ init {
+ // Checks for validity of AutoSize instance
+ // Unspecified check
+ if (minFontSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for minFontSize. " +
+ "Try using other values e.g. 10.sp"
+ )
+ }
+ if (maxFontSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for maxFontSize. " +
+ "Try using other values e.g. 100.sp"
+ )
+ }
+ if (stepSize == TextUnit.Unspecified) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: TextUnit.Unspecified is not a valid value for stepSize. " +
+ "Try using other values e.g. 0.25.sp"
+ )
+ }
+
+ // minFontSize maxFontSize comparison check
+ if (minFontSize.type == maxFontSize.type && minFontSize > maxFontSize) {
+ minFontSize = maxFontSize
+ }
+
+ // check if stepSize is too small
+ if (stepSize.type == TextUnitType.Sp && stepSize < 0.0001f.sp) {
+ throw IllegalArgumentException(
+ "AutoSize.StepBased: stepSize must be greater than or equal to 0.0001f.sp"
+ )
+ }
+
+ // check if minFontSize or maxFontSize are negative
+ if (minFontSize.value < 0) {
+ throw IllegalArgumentException("AutoSize.StepBased: minFontSize must not be negative")
+ }
+ if (maxFontSize.value < 0) {
+ throw IllegalArgumentException("AutoSize.StepBased: maxFontSize must not be negative")
+ }
+ }
+
+ override fun FontSizeSearchScope.getFontSize(): TextUnit {
+ val stepSize = stepSize.toPx()
+ val smallest = minFontSize.toPx()
+ val largest = maxFontSize.toPx()
+ var min = smallest
+ var max = largest
+
+ var current = (min + max) / 2
+
+ while ((max - min) >= stepSize) {
+ // overflow indicates that whole text doesn't fit
+ if (performLayoutAndGetOverflow(current.toSp())) {
+ max = current
+ } else {
+ min = current
+ }
+ current = (min + max) / 2
+ }
+ // used size minus minFontSize must be divisible by stepSize
+ current = (floor((current - smallest) / stepSize) * stepSize + smallest)
+
+ // try the next size up and see if it fits
+ if (
+ (current + stepSize) <= largest &&
+ !performLayoutAndGetOverflow((current + stepSize).toSp())
+ ) {
+ current += stepSize
+ }
+
+ return current.toSp()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) return true
+ if (other == null) return false
+ if (other !is AutoSizeStepBased) return false
+
+ if (other.minFontSize != minFontSize) return false
+ if (other.maxFontSize != maxFontSize) return false
+ if (other.stepSize != stepSize) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = minFontSize.hashCode()
+ result = 31 * result + maxFontSize.hashCode()
+ result = 31 * result + stepSize.hashCode()
+ return result
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
index b3125c6..efab65a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
@@ -206,7 +206,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
index 944f65b..dac0923 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.isDeepPress
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusEventModifierNode
@@ -168,6 +169,9 @@
if (time >= viewConfiguration.longPressTimeoutMillis) {
break
}
+ if (pointerEvent.isDeepPress) {
+ break
+ }
val offset = change.position - firstDown.position
if (offset.getDistance() > viewConfiguration.handwritingSlop) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index b2feaa3..319e13f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -699,6 +699,18 @@
}
}
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ // If the node implements the same interface, it must manually forward calls to
+ // all its delegatable nodes.
+ dragAndDropNode.onPlaced(coordinates)
+ }
+
+ override fun onRemeasured(size: IntSize) {
+ // If the node implements the same interface, it must manually forward calls to
+ // all its delegatable nodes.
+ dragAndDropNode.onRemeasured(size)
+ }
+
private fun startInputSession(fromTap: Boolean) {
if (!fromTap && !keyboardOptions.showKeyboardOnFocusOrDefault) return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.kt
index 242a2c4..b6b51e1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.kt
@@ -19,7 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.content.MediaType
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.ClipMetadata
@@ -35,4 +35,4 @@
onChanged: ((event: DragAndDropEvent) -> Unit)? = null,
onExited: ((event: DragAndDropEvent) -> Unit)? = null,
onEnded: ((event: DragAndDropEvent) -> Unit)? = null,
-): DragAndDropModifierNode
+): DragAndDropTargetModifierNode
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index 3f475b2..d9dff5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -41,7 +41,7 @@
overflow: TextOverflow,
maxIntrinsicWidth: Float
): Int {
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val widthMatters = softWrap || overflow.isEllipsis
val maxWidth =
if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth
@@ -81,6 +81,13 @@
// AA…
// Here we assume there won't be any '\n' character when softWrap is false. And make
// maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ val overwriteMaxLines = !softWrap && overflow.isEllipsis
return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
}
+
+internal val TextOverflow.isEllipsis: Boolean
+ get() {
+ return this == TextOverflow.Ellipsis ||
+ this == TextOverflow.StartEllipsis ||
+ this == TextOverflow.MiddleEllipsis
+ }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
index d3d5f65..1ecfcd1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MinLinesConstrainer.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -103,7 +104,7 @@
density = density,
fontFamilyResolver = fontFamilyResolver,
maxLines = 1,
- ellipsis = false
+ overflow = TextOverflow.Clip
)
.height
@@ -115,7 +116,7 @@
density = density,
fontFamilyResolver = fontFamilyResolver,
maxLines = 2,
- ellipsis = false
+ overflow = TextOverflow.Clip
)
.height
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
index fe403c3..aaa4e25 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
@@ -259,7 +259,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines(softWrap, overflow, maxLines),
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
index 40a4249..482ba7d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
@@ -16,7 +16,9 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
+import androidx.compose.foundation.text.FontSizeSearchScope
import androidx.compose.foundation.text.ceilToIntPx
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.MultiParagraph
@@ -33,6 +35,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.constrain
import kotlin.math.min
@@ -52,6 +55,7 @@
private var softWrap: Boolean = true,
private var maxLines: Int = Int.MAX_VALUE,
private var minLines: Int = DefaultMinLines,
+ private var autoSize: AutoSize? = null
) {
/**
@@ -113,6 +117,9 @@
/** Output height for last call to [intrinsicHeight] at [cachedIntrinsicHeightInputWidth] */
private var cachedIntrinsicHeight: Int = -1
+ /** Used to get the font size if AutoSize is enabled and perform layout with many font sizes */
+ private val fontSizeSearchScope: FontSizeSearchScopeImpl = FontSizeSearchScopeImpl()
+
/**
* Update layout constraints for this text
*
@@ -121,19 +128,11 @@
fun layoutWithConstraints(constraints: Constraints, layoutDirection: LayoutDirection): Boolean {
val finalConstraints =
if (minLines > 1) {
- val localMin =
- MinLinesConstrainer.from(
- mMinLinesConstrainer,
- layoutDirection,
- style,
- density!!,
- fontFamilyResolver
- )
- .also { mMinLinesConstrainer = it }
- localMin.coerceMinLines(inConstraints = constraints, minLines = minLines)
+ useMinLinesConstrainer(constraints, layoutDirection)
} else {
constraints
}
+
if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
if (finalConstraints != prevConstraints) {
// ensure size and overflow is still accurate
@@ -152,6 +151,13 @@
}
return false
}
+
+ if (autoSize != null) {
+ autoSize!!.performAutoSize(finalConstraints, layoutDirection).also {
+ style = style.copy(fontSize = it)
+ }
+ }
+
paragraph =
layoutText(finalConstraints, layoutDirection).also {
prevConstraints = finalConstraints
@@ -167,6 +173,45 @@
return true
}
+ private fun useMinLinesConstrainer(
+ constraints: Constraints,
+ layoutDirection: LayoutDirection,
+ style: TextStyle = this.style
+ ): Constraints {
+ val localMin =
+ MinLinesConstrainer.from(
+ mMinLinesConstrainer,
+ layoutDirection,
+ style,
+ density!!,
+ fontFamilyResolver
+ )
+ .also { mMinLinesConstrainer = it }
+ return localMin.coerceMinLines(inConstraints = constraints, minLines = minLines)
+ }
+
+ /**
+ * Performs algorithm specified in [autoSize]
+ *
+ * @return The derived optimal font size
+ */
+ private fun AutoSize.performAutoSize(
+ finalConstraints: Constraints,
+ layoutDirection: LayoutDirection
+ ): TextUnit {
+ var optimalFontSize: TextUnit
+
+ fontSizeSearchScope.originalFontSize = style.fontSize
+ fontSizeSearchScope.layoutDirection = layoutDirection
+ fontSizeSearchScope.finalConstraints = finalConstraints
+
+ optimalFontSize = fontSizeSearchScope.getFontSize()
+ if (optimalFontSize.isEm)
+ optimalFontSize = fontSizeSearchScope.originalFontSize * optimalFontSize.value
+
+ return optimalFontSize
+ }
+
/** The natural height of text at [width] in [layoutDirection] */
fun intrinsicHeight(width: Int, layoutDirection: LayoutDirection): Int {
val localWidth = cachedIntrinsicHeightInputWidth
@@ -190,7 +235,8 @@
overflow: TextOverflow,
softWrap: Boolean,
maxLines: Int,
- minLines: Int
+ minLines: Int,
+ autoSize: AutoSize?
) {
this.text = text
this.style = style
@@ -199,6 +245,7 @@
this.softWrap = softWrap
this.maxLines = maxLines
this.minLines = minLines
+ this.autoSize = autoSize
markDirty()
}
@@ -247,9 +294,8 @@
overflow,
localParagraphIntrinsics.maxIntrinsicWidth
),
- // This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines(softWrap, overflow, maxLines),
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
}
@@ -261,7 +307,7 @@
constraints: Constraints,
layoutDirection: LayoutDirection
): Boolean {
- // paragarph and paragraphIntrinsics are from previous run
+ // paragraph and paragraphIntrinsics are from previous run
val localParagraph = paragraph ?: return true
val localParagraphIntrinsics = paragraphIntrinsics ?: return true
// no layout yet
@@ -338,7 +384,7 @@
),
finalConstraints,
maxLines,
- overflow == TextOverflow.Ellipsis
+ overflow
),
layoutSize
)
@@ -357,4 +403,89 @@
override fun toString(): String =
"ParagraphLayoutCache(paragraph=${if (paragraph != null) "<paragraph>" else "null"}, " +
"lastDensity=$lastDensity)"
+
+ /**
+ * [Paragraph] specific implementation of [FontSizeSearchScope].
+ *
+ * Uses [layoutText] in [ParagraphLayoutCache] to lay out the text and check for overflow. Also
+ * caches to [style], [paragraph], [prevConstraints] and [layoutSize]
+ */
+ private inner class FontSizeSearchScopeImpl : FontSizeSearchScope {
+ var finalConstraints: Constraints = Constraints.fixed(0, 0)
+ var layoutDirection: LayoutDirection = LayoutDirection.Ltr
+ var originalFontSize: TextUnit = TextUnit.Unspecified
+ var overflow: TextOverflow = TextOverflow.Clip
+
+ override val density
+ get() = [email protected]!!.density
+
+ override val fontScale
+ get() = [email protected]!!.fontScale
+
+ override fun performLayoutAndGetOverflow(fontSize: TextUnit): Boolean {
+ val doesOverflow: Boolean
+
+ var usedFontSize = fontSize
+ if (fontSize.isEm) {
+ if (originalFontSize == TextUnit.Unspecified) {
+ // workaround as DefaultFontSize is private in SpanStyle
+ // TODO(b/364858402): Make DefaultFontSize public
+ originalFontSize = resolveDefaults(TextStyle.Default, layoutDirection).fontSize
+ }
+ usedFontSize = originalFontSize * fontSize.value
+ }
+
+ val usedStyle = style.copy(fontSize = usedFontSize)
+ if (minLines > 1) {
+ finalConstraints =
+ useMinLinesConstrainer(finalConstraints, layoutDirection, usedStyle)
+ }
+
+ val localParagraphIntrinsics =
+ ParagraphIntrinsics(
+ text = text,
+ style = resolveDefaults(usedStyle, layoutDirection),
+ density = [email protected]!!,
+ fontFamilyResolver = fontFamilyResolver
+ )
+
+ Paragraph(
+ paragraphIntrinsics = localParagraphIntrinsics,
+ constraints =
+ finalConstraints(
+ finalConstraints,
+ softWrap,
+ TextOverflow.Clip,
+ localParagraphIntrinsics.maxIntrinsicWidth
+ ),
+ maxLines = finalMaxLines(softWrap, overflow, maxLines),
+ overflow = TextOverflow.Clip
+ )
+ .also {
+ val localSize =
+ finalConstraints.constrain(
+ IntSize(it.width.ceilToIntPx(), it.height.ceilToIntPx())
+ )
+ doesOverflow = localSize.width < it.width || localSize.height < it.height
+ }
+
+ return doesOverflow
+ }
+
+ override fun TextUnit.toPx(): Float {
+ if (isEm) {
+ check(!originalFontSize.isEm) {
+ "AutoSize -> toPx(): Cannot convert Em to Px when style.fontSize is Em\n" +
+ "Declare the composable's style.fontSize with Sp units instead."
+ }
+ if (originalFontSize == TextUnit.Unspecified) {
+ // workaround as DefaultFontSize is private in SpanStyle
+ // TODO(b/364858402): Make DefaultFontSize public
+ originalFontSize = resolveDefaults(TextStyle.Default, layoutDirection).fontSize
+ }
+ return originalFontSize.toPx() * value
+ }
+ return toDp().toPx()
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleElement.kt
index 5543ebc..7f174e1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleElement.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.modifiers
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.node.ModifierNodeElement
@@ -37,7 +38,8 @@
private val softWrap: Boolean = true,
private val maxLines: Int = Int.MAX_VALUE,
private val minLines: Int = DefaultMinLines,
- private val color: ColorProducer? = null
+ private val color: ColorProducer? = null,
+ private val autoSize: AutoSize? = null
) : ModifierNodeElement<TextStringSimpleNode>() {
override fun create(): TextStringSimpleNode =
@@ -49,7 +51,8 @@
softWrap,
maxLines,
minLines,
- color
+ color,
+ autoSize
)
override fun update(node: TextStringSimpleNode) {
@@ -63,7 +66,8 @@
maxLines = maxLines,
softWrap = softWrap,
fontFamilyResolver = fontFamilyResolver,
- overflow = overflow
+ overflow = overflow,
+ autoSize = autoSize
)
)
}
@@ -80,6 +84,7 @@
// these are equally unlikely to change
if (fontFamilyResolver != other.fontFamilyResolver) return false
+ if (autoSize != other.autoSize) return false
if (overflow != other.overflow) return false
if (softWrap != other.softWrap) return false
if (maxLines != other.maxLines) return false
@@ -97,6 +102,7 @@
result = 31 * result + maxLines
result = 31 * result + minLines
result = 31 * result + (color?.hashCode() ?: 0)
+ result = 31 * result + (autoSize?.hashCode() ?: 0)
return result
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index f10d62b..fc9a920 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.modifiers
import androidx.compose.foundation.internal.requirePreconditionNotNull
+import androidx.compose.foundation.text.AutoSize
import androidx.compose.foundation.text.DefaultMinLines
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -75,7 +76,8 @@
private var softWrap: Boolean = true,
private var maxLines: Int = Int.MAX_VALUE,
private var minLines: Int = DefaultMinLines,
- private var overrideColor: ColorProducer? = null
+ private var overrideColor: ColorProducer? = null,
+ private var autoSize: AutoSize? = null
) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
@Suppress("PrimitiveInCollection") // Map required for use in public API.
// Usages of this collection are so few that the gains of using
@@ -143,7 +145,8 @@
maxLines: Int,
softWrap: Boolean,
fontFamilyResolver: FontFamily.Resolver,
- overflow: TextOverflow
+ overflow: TextOverflow,
+ autoSize: AutoSize?
): Boolean {
var changed: Boolean
@@ -175,6 +178,11 @@
changed = true
}
+ if (this.autoSize != autoSize) {
+ this.autoSize = autoSize
+ changed = true
+ }
+
return changed
}
@@ -189,7 +197,8 @@
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
- minLines = minLines
+ minLines = minLines,
+ autoSize = autoSize
)
}
@@ -244,6 +253,7 @@
softWrap,
maxLines,
minLines,
+ autoSize
) ?: return false
} else {
val newTextSubstitution = TextSubstitutionValue(text, updatedText)
diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.commonStubs.kt
index 9667098..b4b223d 100644
--- a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.commonStubs.kt
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentDragAndDropNode.commonStubs.kt
@@ -18,9 +18,9 @@
import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
internal actual fun ReceiveContentDragAndDropNode(
receiveContentConfiguration: ReceiveContentConfiguration,
dragAndDropRequestPermission: (DragAndDropEvent) -> Unit
-): DragAndDropModifierNode = implementedInJetBrainsFork()
+): DragAndDropTargetModifierNode = implementedInJetBrainsFork()
diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.commonStubs.kt
new file mode 100644
index 0000000..f942926
--- /dev/null
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropSource.commonStubs.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.draganddrop
+
+import androidx.compose.foundation.implementedInJetBrainsFork
+import androidx.compose.ui.draw.CacheDrawScope
+import androidx.compose.ui.draw.DrawResult
+import androidx.compose.ui.graphics.drawscope.DrawScope
+
+internal actual object DragAndDropSourceDefaults {
+ actual val DefaultStartDetector: DragAndDropStartDetector = implementedInJetBrainsFork()
+}
+
+internal actual class CacheDrawScopeDragShadowCallback actual constructor() {
+ actual fun drawDragShadow(drawScope: DrawScope): Unit = implementedInJetBrainsFork()
+
+ actual fun cachePicture(scope: CacheDrawScope): DrawResult = implementedInJetBrainsFork()
+}
diff --git a/compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt
similarity index 73%
copy from compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt
copy to compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt
index 263e22e..253dfeb 100644
--- a/compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.commonStubs.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.compose.foundation.gestures
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable = implementedInJetBrainsFork()
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isDeepPress: Boolean
+ get() = false
diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.commonStubs.kt
index 73d4929..8a11a48 100644
--- a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.commonStubs.kt
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDragAndDropNode.commonStubs.kt
@@ -19,7 +19,7 @@
import androidx.compose.foundation.content.MediaType
import androidx.compose.foundation.implementedInJetBrainsFork
import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
+import androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.ClipMetadata
@@ -34,4 +34,4 @@
onChanged: ((event: DragAndDropEvent) -> Unit)?,
onExited: ((event: DragAndDropEvent) -> Unit)?,
onEnded: ((event: DragAndDropEvent) -> Unit)?,
-): DragAndDropModifierNode = implementedInJetBrainsFork()
+): DragAndDropTargetModifierNode = implementedInJetBrainsFork()
diff --git a/compose/integration-tests/demos/common/build.gradle b/compose/integration-tests/demos/common/build.gradle
index cf434ee..34c02706 100644
--- a/compose/integration-tests/demos/common/build.gradle
+++ b/compose/integration-tests/demos/common/build.gradle
@@ -26,7 +26,7 @@
api("androidx.activity:activity:1.2.0")
api("androidx.fragment:fragment-ktx:1.3.6")
- implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime"))
}
android {
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index 6d7990a..0a420bb 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -35,8 +35,8 @@
compileSdkVersion 35
defaultConfig {
applicationId "androidx.compose.material.catalog"
- versionCode 2400
- versionName "2.4.0"
+ versionCode 2500
+ versionName "2.5.0"
}
buildTypes {
release {
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index 4cf82b4..5cbd3bd 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -48,7 +48,7 @@
inceptionYear = "2024"
description = "Compose Material integration with Navigation"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:material:material-navigation-samples"))
+ samples(project(":compose:material:material-navigation-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 417619c..227c21a 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -41,12 +41,12 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
- api(project(":compose:foundation:foundation"))
+ api("androidx.compose.foundation:foundation:1.7.1")
api(project(":compose:runtime:runtime"))
implementation(project(":collection:collection"))
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.animation:animation:1.7.1")
+ implementation("androidx.compose.ui:ui-util:1.7.1")
}
}
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index b76e4f3..ec7ba65 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -41,17 +41,17 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
- api(project(":compose:animation:animation-core"))
+ api("androidx.compose.animation:animation-core:1.7.1")
api(project(":compose:foundation:foundation"))
- api(project(":compose:ui:ui-text"))
+ api("androidx.compose.ui:ui-text:1.7.1")
api(project(":compose:material:material-ripple"))
api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
+ api("androidx.compose.ui:ui:1.7.1")
- implementation(project(":compose:animation:animation-core"))
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.animation:animation-core:1.7.1")
+ implementation("androidx.compose.animation:animation:1.7.1")
+ implementation("androidx.compose.foundation:foundation-layout:1.7.1")
+ implementation("androidx.compose.ui:ui-util:1.7.1")
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AlertDialog.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AlertDialog.kt
index 625bee6..12ec8c2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AlertDialog.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AlertDialog.kt
@@ -428,7 +428,7 @@
val arrangement = Arrangement.Bottom
// TODO(soboleva): rtl support
// Handle vertical direction
- val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
+ val mainAxisPositions = IntArray(childrenMainAxisSizes.size)
with(arrangement) {
arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions)
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
index 9a7d402..da1918b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
@@ -370,7 +370,7 @@
val containerWidth =
placeables.fastFold(0) { maxWidth, placeable -> max(maxWidth, placeable.width) }
- val y = Array(placeables.size) { 0 }
+ val y = IntArray(placeables.size)
var containerHeight = 0
placeables.fastForEachIndexed { index, placeable ->
val toPreviousBaseline =
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
samples(project(":compose:material3:adaptive:adaptive-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt
new file mode 100644
index 0000000..cfdc5f5
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneMarginsTest.kt
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.Ruler
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class PaneMarginsModifierTest {
+ @Test
+ fun unspecifiedPaneMargins_alwaysUseMeasuredValue() {
+ assertThat(with(PaneMargins.Unspecified) { MockPlacementScope().getPaneLeft(50) })
+ .isEqualTo(50)
+ assertThat(with(PaneMargins.Unspecified) { MockPlacementScope().getPaneTop(70) })
+ .isEqualTo(70)
+ assertThat(
+ with(PaneMargins.Unspecified) {
+ MockPlacementScope().getPaneRight(30, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(30)
+ assertThat(
+ with(PaneMargins.Unspecified) {
+ MockPlacementScope().getPaneBottom(60, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneTop_noMarginsSet_useMeasuredTop() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneTop(50) }).isEqualTo(50)
+ }
+
+ @Test
+ fun getPaneTop_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneTop(0) }).isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneTop_multipleWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(0)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneTop_withFixedMarginsAndWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(0)
+ }
+ )
+ .isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneTop_whenMeasuredTopIsLarger_useMeasuredTop() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 100.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(mockInset1Top = 30, mockInset2Top = 60, mockInset3Top = 10)
+ .getPaneTop(140)
+ }
+ )
+ .isEqualTo(140)
+ }
+
+ @Test
+ fun getPaneBottom_noMarginsSet_useMeasuredBottom() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneBottom(850, MockLayoutHeight) }
+ )
+ .isEqualTo(850)
+ }
+
+ @Test
+ fun getPaneBottom_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 100.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneBottom(1024, MockLayoutHeight) }
+ )
+ .isEqualTo(MockLayoutHeight - 100)
+ }
+
+ @Test
+ fun getPaneBottom_multipleWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(1024, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneBottom_withFixedMarginsAndWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 200.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(1024, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(MockLayoutHeight - 200)
+ }
+
+ @Test
+ fun getPaneBottom_whenMeasuredBottomIsSmaller_useMeasuredBottom() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 0.dp, 200.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Bottom = 930,
+ mockInset2Bottom = 960,
+ mockInset3Bottom = 910
+ )
+ .getPaneBottom(800, MockLayoutHeight)
+ }
+ )
+ .isEqualTo(800)
+ }
+
+ @Test
+ fun getPaneLeft_noMarginsSet_useMeasuredLeft() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(50) }).isEqualTo(50)
+ }
+
+ @Test
+ fun getPaneLeft_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(0) }).isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneLeft_withRtlDirection_usePaddingEnd() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 110.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ LayoutDirection.Rtl
+ )
+ assertThat(with(mockPaneMargins) { MockPlacementScope().getPaneLeft(0) }).isEqualTo(110)
+ }
+
+ @Test
+ fun getPaneLeft_multipleWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(0)
+ }
+ )
+ .isEqualTo(60)
+ }
+
+ @Test
+ fun getPaneLeft_withFixedMarginsAndWindowInsets_useLargestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(0)
+ }
+ )
+ .isEqualTo(100)
+ }
+
+ @Test
+ fun getPaneLeft_whenMeasuredLeftIsLarger_useMeasuredLeft() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(100.dp, 0.dp, 0.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Left = 30,
+ mockInset2Left = 60,
+ mockInset3Left = 10
+ )
+ .getPaneLeft(140)
+ }
+ )
+ .isEqualTo(140)
+ }
+
+ @Test
+ fun getPaneRight_noMarginsSet_useMeasuredRight() {
+ val mockPaneMargins =
+ PaneMarginsImpl(PaddingValues(), emptyList(), MockDensity, MockLayoutDirection)
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(850, MockLayoutWidth) }
+ )
+ .isEqualTo(850)
+ }
+
+ @Test
+ fun getPaneRight_noWindowInsets_useFixedMargins() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 100.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(1280, MockLayoutWidth) }
+ )
+ .isEqualTo(MockLayoutWidth - 100)
+ }
+
+ @Test
+ fun getPaneRight_withRtlDirection_usePaddingStart() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(110.dp, 0.dp, 0.dp, 0.dp),
+ emptyList(),
+ MockDensity,
+ LayoutDirection.Rtl
+ )
+ assertThat(
+ with(mockPaneMargins) { MockPlacementScope().getPaneRight(1280, MockLayoutWidth) }
+ )
+ .isEqualTo(MockLayoutWidth - 110)
+ }
+
+ @Test
+ fun getPaneRight_multipleWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(1280, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneRight_withFixedMarginsAndWindowInsets_useSmallestOne() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 200.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(1280, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(910)
+ }
+
+ @Test
+ fun getPaneRight_whenMeasuredRightIsSmaller_useMeasuredRight() {
+ val mockPaneMargins =
+ PaneMarginsImpl(
+ PaddingValues(0.dp, 0.dp, 200.dp, 0.dp),
+ listOf(MockWindowInsetRulers1, MockWindowInsetRulers2, MockWindowInsetRulers3),
+ MockDensity,
+ MockLayoutDirection
+ )
+ assertThat(
+ with(mockPaneMargins) {
+ MockPlacementScope(
+ mockInset1Right = 930,
+ mockInset2Right = 960,
+ mockInset3Right = 910
+ )
+ .getPaneRight(800, MockLayoutWidth)
+ }
+ )
+ .isEqualTo(800)
+ }
+}
+
+private val MockDensity = Density(1f)
+private val MockLayoutDirection = LayoutDirection.Ltr
+private const val MockLayoutWidth = 1280
+private const val MockLayoutHeight = 1024
+
+private val MockWindowInsetRulers1 = WindowInsetsRulers()
+private val MockWindowInsetRulers2 = WindowInsetsRulers()
+private val MockWindowInsetRulers3 = WindowInsetsRulers()
+
+private class MockPlacementScope(
+ val mockInset1Left: Int = 0,
+ val mockInset1Top: Int = 0,
+ val mockInset1Right: Int = 0,
+ val mockInset1Bottom: Int = 0,
+ val mockInset2Left: Int = 0,
+ val mockInset2Top: Int = 0,
+ val mockInset2Right: Int = 0,
+ val mockInset2Bottom: Int = 0,
+ val mockInset3Left: Int = 0,
+ val mockInset3Top: Int = 0,
+ val mockInset3Right: Int = 0,
+ val mockInset3Bottom: Int = 0,
+) : Placeable.PlacementScope() {
+ override val parentWidth = MockLayoutWidth
+ override val parentLayoutDirection = MockLayoutDirection
+
+ override fun Ruler.current(defaultValue: Float): Float =
+ when (this) {
+ MockWindowInsetRulers1.left -> mockInset1Left.toFloat()
+ MockWindowInsetRulers1.top -> mockInset1Top.toFloat()
+ MockWindowInsetRulers1.right -> mockInset1Right.toFloat()
+ MockWindowInsetRulers1.bottom -> mockInset1Bottom.toFloat()
+ MockWindowInsetRulers2.left -> mockInset2Left.toFloat()
+ MockWindowInsetRulers2.top -> mockInset2Top.toFloat()
+ MockWindowInsetRulers2.right -> mockInset2Right.toFloat()
+ MockWindowInsetRulers2.bottom -> mockInset2Bottom.toFloat()
+ MockWindowInsetRulers3.left -> mockInset3Left.toFloat()
+ MockWindowInsetRulers3.top -> mockInset3Top.toFloat()
+ MockWindowInsetRulers3.right -> mockInset3Right.toFloat()
+ MockWindowInsetRulers3.bottom -> mockInset3Bottom.toFloat()
+ else -> 0f
+ }
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
new file mode 100644
index 0000000..c6e6f11
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneMargins.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.HorizontalRuler
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.VerticalRuler
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ParentDataModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+// TODO(conradchen): move the modifier declarations to PaneScaffoldPaneScope when we can publish it.
+/**
+ * This modifier specifies the associated pane's margins according to the provided
+ * [WindowInsetsRulers]. Note that if multiple window inset rulers are provided, the scaffold will
+ * decide the actual margins by taking the union of these insets - i.e. the one creating the largest
+ * margins will be used.
+ *
+ * @param windowInsets the window insets the pane wants to respect.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+internal fun Modifier.paneMargins(vararg windowInsets: WindowInsetsRulers) =
+ paneMargins(PaddingValues(), windowInsets.toList())
+
+// TODO(conradchen): move the modifier declarations to PaneScaffoldPaneScope when we can publish it.
+/**
+ * This modifier specifies the associated pane's margins according to specified fixed margins and
+ * the provided [WindowInsetsRulers], if any. Note that the scaffold will decide the actual margins
+ * by taking the union of the fixed margins and the provided insets - i.e. the one creating the
+ * largest margins will be used.
+ *
+ * @param fixedMargins fixed margins to use for the pane.
+ * @param windowInsets the window insets the pane wants to respect.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+internal fun Modifier.paneMargins(
+ fixedMargins: PaddingValues,
+ vararg windowInsets: WindowInsetsRulers
+) = paneMargins(fixedMargins, windowInsets.toList())
+
+@Composable
+private fun Modifier.paneMargins(
+ fixedMargins: PaddingValues,
+ windowInsets: List<WindowInsetsRulers>
+) =
+ this.then(
+ PaneMarginsElement(
+ PaneMarginsImpl(
+ fixedMargins,
+ windowInsets,
+ LocalDensity.current,
+ LocalLayoutDirection.current
+ )
+ )
+ )
+
+private data class PaneMarginsElement(val paneMargins: PaneMargins) :
+ ModifierNodeElement<PaneMarginsNode>() {
+ private val inspectorInfo = debugInspectorInfo {
+ name = "paneMargins"
+ properties["paneMargins"] = paneMargins
+ }
+
+ override fun create(): PaneMarginsNode {
+ return PaneMarginsNode(paneMargins)
+ }
+
+ override fun update(node: PaneMarginsNode) {
+ node.paneMargins = paneMargins
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ inspectorInfo()
+ }
+}
+
+private class PaneMarginsNode(var paneMargins: PaneMargins) :
+ ParentDataModifierNode, Modifier.Node() {
+ override fun Density.modifyParentData(parentData: Any?) =
+ ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData()).also {
+ it.paneMargins = paneMargins
+ }
+}
+
+@Immutable
+internal interface PaneMargins {
+ fun Placeable.PlacementScope.getPaneLeft(measuredLeft: Int) = measuredLeft
+
+ fun Placeable.PlacementScope.getPaneTop(measuredTop: Int) = measuredTop
+
+ fun Placeable.PlacementScope.getPaneRight(measuredRight: Int, parentRight: Int) = measuredRight
+
+ fun Placeable.PlacementScope.getPaneBottom(measuredBottom: Int, parentBottom: Int) =
+ measuredBottom
+
+ companion object {
+ val Unspecified = object : PaneMargins {}
+ }
+}
+
+@Immutable
+internal class PaneMarginsImpl(
+ fixedMargins: PaddingValues = PaddingValues(),
+ windowInsets: List<WindowInsetsRulers>,
+ density: Density,
+ layoutDirection: LayoutDirection
+) : PaneMargins {
+ private val fixedMarginLeft =
+ with(density) { fixedMargins.calculateLeftPadding(layoutDirection).roundToPx() }
+ private val fixedMarginTop = with(density) { fixedMargins.calculateTopPadding().roundToPx() }
+ private val fixedMarginRight =
+ with(density) { fixedMargins.calculateRightPadding(layoutDirection).roundToPx() }
+ private val fixedMarginBottom =
+ with(density) { fixedMargins.calculateBottomPadding().roundToPx() }
+ private val rulers = windowInsets
+
+ override fun Placeable.PlacementScope.getPaneLeft(measuredLeft: Int): Int =
+ maxOf(
+ measuredLeft,
+ fixedMarginLeft,
+ rulers.maxOfOrNull { it.left.current(0f).roundToInt() } ?: 0
+ )
+
+ override fun Placeable.PlacementScope.getPaneTop(measuredTop: Int): Int =
+ maxOf(
+ measuredTop,
+ fixedMarginTop,
+ rulers.maxOfOrNull { it.top.current(0f).roundToInt() } ?: 0
+ )
+
+ override fun Placeable.PlacementScope.getPaneRight(measuredRight: Int, parentRight: Int): Int =
+ minOf(
+ measuredRight,
+ parentRight - fixedMarginRight,
+ rulers.minOfOrNull { it.right.current(Float.MAX_VALUE).roundToInt() } ?: parentRight
+ )
+
+ override fun Placeable.PlacementScope.getPaneBottom(
+ measuredBottom: Int,
+ parentBottom: Int
+ ): Int =
+ minOf(
+ measuredBottom,
+ parentBottom - fixedMarginBottom,
+ rulers.minOfOrNull { it.bottom.current(Float.MAX_VALUE).roundToInt() } ?: parentBottom
+ )
+}
+
+// TODO(conradchen): Move to use the foundation definition when it's available
+internal class WindowInsetsRulers {
+ val left = VerticalRuler()
+ val top = HorizontalRuler()
+ val right = VerticalRuler()
+ val bottom = HorizontalRuler()
+}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 3fd930e..551f730 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -192,5 +192,6 @@
internal data class PaneScaffoldParentData(
var preferredWidth: Float? = null,
+ var paneMargins: PaneMargins = PaneMargins.Unspecified,
var isAnimatedPane: Boolean = false
)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index bebc466..37f09c3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -693,6 +693,8 @@
data.preferredWidth!!.toInt()
}
+ val margins: PaneMargins = data.paneMargins
+
val isAnimatedPane = data.isAnimatedPane
var measuredWidth = 0
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-render-strategy/api/current.txt b/compose/material3/adaptive/adaptive-render-strategy/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/biometric/biometric-ktx/api/res-current.txt b/compose/material3/adaptive/adaptive-render-strategy/api/res-current.txt
similarity index 100%
copy from biometric/biometric-ktx/api/res-current.txt
copy to compose/material3/adaptive/adaptive-render-strategy/api/res-current.txt
diff --git a/compose/material3/adaptive/adaptive-render-strategy/api/restricted_current.txt b/compose/material3/adaptive/adaptive-render-strategy/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/material3/adaptive/adaptive-render-strategy/build.gradle b/compose/material3/adaptive/adaptive-render-strategy/build.gradle
new file mode 100644
index 0000000..7634ff6
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/build.gradle
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+}
+
+androidXMultiplatform {
+ android()
+ jvmStubs()
+
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.8.1")
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidInstrumentedTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ commonStubsMain {
+ dependsOn(commonMain)
+ }
+
+ jvmStubsMain {
+ dependsOn(commonStubsMain)
+ }
+ }
+}
+
+android {
+ namespace "androidx.compose.material3.adaptive.render.strategy"
+}
+
+androidx {
+ name = "androidx.compose.material3.adaptive:adaptive-render-strategy"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Material AdaptiveRenderStrategy library"
+}
diff --git a/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md b/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md
new file mode 100644
index 0000000..0fb75e6
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-render-strategy/src/commonMain/kotlin/androidx/compose/material3/adaptive/androidx-compose-material3-adaptive-adaptive-render-strategy-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+androidx.compose.material3.adaptive adaptive-render-strategy
+
+# Package androidx.compose.material3.adaptive.render.strategy
+
+Material AdaptiveRenderStrategy library
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/samples/build.gradle b/compose/material3/adaptive/samples/build.gradle
index 08e4e4e..f42ef65 100644
--- a/compose/material3/adaptive/samples/build.gradle
+++ b/compose/material3/adaptive/samples/build.gradle
@@ -44,10 +44,9 @@
implementation(project(":compose:material3:material3"))
implementation(project(":compose:material3:material3-window-size-class"))
implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
+ implementation("androidx.compose.ui:ui-tooling:1.4.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
implementation("androidx.navigation:navigation-compose:2.7.7")
-
- debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
}
androidx {
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
index 2585509..2e7efb0 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
@@ -18,9 +18,14 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
-import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.LargeExtendedFloatingActionButton
+import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MediumExtendedFloatingActionButton
+import androidx.compose.material3.MediumFloatingActionButton
+import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.testutils.LayeredComposeTestCase
@@ -32,21 +37,21 @@
import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@LargeTest
-@RunWith(AndroidJUnit4::class)
-class FloatingActionButtonBenchmark {
+@RunWith(Parameterized::class)
+class FloatingActionButtonBenchmark(private val size: FabSize) {
@get:Rule val benchmarkRule = ComposeBenchmarkRule()
- private val fabTestCaseFactory = { FloatingActionButtonTestCase() }
- private val extendedFabTestCaseFactory = { ExtendedFloatingActionButtonTestCase() }
+ private val fabTestCaseFactory = { FloatingActionButtonTestCase(size) }
+ private val extendedFabTestCaseFactory = { ExtendedFloatingActionButtonTestCase(size) }
@Ignore
@Test
@@ -105,13 +110,33 @@
fun extendedFab_firstPixel() {
benchmarkRule.benchmarkToFirstPixel(extendedFabTestCaseFactory)
}
+
+ companion object {
+ @Parameterized.Parameters(name = "size = {0}")
+ @JvmStatic
+ fun parameters() = FabSize.values()
+ }
}
-internal class FloatingActionButtonTestCase : LayeredComposeTestCase() {
+internal class FloatingActionButtonTestCase(private val size: FabSize) : LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
- FloatingActionButton(onClick = { /*TODO*/ }) { Box(modifier = Modifier.size(24.dp)) }
+ when (size) {
+ FabSize.Small ->
+ FloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ FabSize.Medium ->
+ MediumFloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ FabSize.Large ->
+ LargeFloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ }
}
@Composable
@@ -120,15 +145,32 @@
}
}
-internal class ExtendedFloatingActionButtonTestCase : LayeredComposeTestCase() {
+internal class ExtendedFloatingActionButtonTestCase(private val size: FabSize) :
+ LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
- ExtendedFloatingActionButton(
- text = { Text(text = "Extended FAB") },
- icon = { Box(modifier = Modifier.size(24.dp)) },
- onClick = { /*TODO*/ }
- )
+ when (size) {
+ FabSize.Small ->
+ SmallExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ FabSize.Medium ->
+ MediumExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ FabSize.Large ->
+ LargeExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = { Box(modifier = Modifier.size(24.dp)) },
+ onClick = { /*TODO*/ }
+ )
+ }
}
@Composable
@@ -136,3 +178,9 @@
MaterialTheme { content() }
}
}
+
+enum class FabSize {
+ Small,
+ Medium,
+ Large,
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt
new file mode 100644
index 0000000..db004a2
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonMenuBenchmark.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FloatingActionButtonMenu
+import androidx.compose.material3.FloatingActionButtonMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ToggleFloatingActionButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FloatingActionButtonMenuBenchmark {
+
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val floatingActionButtonMenuTestCaseFactory = { FloatingActionButtonMenuTestCase() }
+
+ @Ignore
+ @Test
+ fun fabMenu_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_measure() {
+ benchmarkRule.benchmarkFirstMeasure(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_layout() {
+ benchmarkRule.benchmarkFirstLayout(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fabMenu_draw() {
+ benchmarkRule.benchmarkFirstDraw(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Test
+ fun fabMenu_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(floatingActionButtonMenuTestCaseFactory)
+ }
+
+ @Test
+ fun fabMenu_toggle_recomposeMeasureLayout() {
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
+ caseFactory = floatingActionButtonMenuTestCaseFactory,
+ assertOneRecomposition = false
+ )
+ }
+}
+
+internal class FloatingActionButtonMenuTestCase : LayeredComposeTestCase(), ToggleableTestCase {
+
+ private var state by mutableStateOf(false)
+
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ @Composable
+ override fun MeasuredContent() {
+ Box(Modifier.fillMaxSize()) {
+ FloatingActionButtonMenu(
+ modifier = Modifier.align(Alignment.BottomEnd),
+ expanded = state,
+ button = {
+ ToggleFloatingActionButton(
+ checked = state,
+ onCheckedChange = { /* Do nothing */ }
+ ) {
+ Spacer(Modifier.size(24.dp))
+ }
+ }
+ ) {
+ repeat(6) {
+ FloatingActionButtonMenuItem(
+ onClick = { /* Do nothing */ },
+ icon = { Spacer(Modifier.size(24.dp)) },
+ text = { Spacer(Modifier.size(24.dp)) },
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+
+ override fun toggleState() {
+ state = !state
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
index f59f5af..b7d74ba 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/IconToggleButtonBenchmark.kt
@@ -18,9 +18,12 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.IconButtonShapes
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconToggleButton
@@ -92,6 +95,7 @@
private var state by mutableStateOf(false)
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
override fun MeasuredContent() {
when (type) {
@@ -117,6 +121,58 @@
) {
Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
}
+ IconToggleButtonType.IconToggleButtonExpressive ->
+ IconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.FilledIconToggleButtonExpressive ->
+ FilledIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.FilledTonalIconToggleButtonExpressive ->
+ FilledTonalIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
+ IconToggleButtonType.OutlinedIconToggleButtonExpressive ->
+ OutlinedIconToggleButton(
+ checked = state,
+ onCheckedChange = { /* Do something! */ },
+ shapes =
+ IconButtonShapes(
+ shape = IconButtonDefaults.smallRoundShape,
+ pressedShape = IconButtonDefaults.smallPressedShape,
+ checkedShape = IconButtonDefaults.smallSquareShape
+ )
+ ) {
+ Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
+ }
}
}
@@ -134,5 +190,9 @@
IconToggleButton,
FilledIconToggleButton,
FilledTonalIconToggleButton,
- OutlinedIconToggleButton
+ OutlinedIconToggleButton,
+ IconToggleButtonExpressive,
+ FilledIconToggleButtonExpressive,
+ FilledTonalIconToggleButtonExpressive,
+ OutlinedIconToggleButtonExpressive
}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt
index f971352..24a3a86 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt
@@ -18,16 +18,16 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.DismissibleModalWideNavigationRail
+import androidx.compose.material3.DismissibleModalWideNavigationRailState
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalExpandedNavigationRail
-import androidx.compose.material3.ModalExpandedNavigationRailState
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.WideNavigationRail
import androidx.compose.material3.WideNavigationRailItem
-import androidx.compose.material3.rememberModalExpandedNavigationRailState
+import androidx.compose.material3.rememberDismissibleModalWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
@@ -204,15 +204,15 @@
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal class ModalExpandedRailTestCase() : LayeredComposeTestCase(), ToggleableTestCase {
- private lateinit var state: ModalExpandedNavigationRailState
+ private lateinit var state: DismissibleModalWideNavigationRailState
private lateinit var scope: CoroutineScope
@Composable
override fun MeasuredContent() {
- state = rememberModalExpandedNavigationRailState()
+ state = rememberDismissibleModalWideNavigationRailState()
scope = rememberCoroutineScope()
- ModalExpandedNavigationRail(
+ DismissibleModalWideNavigationRail(
onDismissRequest = {},
railState = state,
) {
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
index 57e571d..954f8fa 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
@@ -43,10 +43,9 @@
implementation(project(":compose:material3:material3"))
implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
+ implementation("androidx.compose.ui:ui-tooling:1.4.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
implementation("androidx.window:window-core:1.3.0")
-
- debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
}
androidx {
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index cff69e2..7ec2dce 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -43,12 +43,12 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
- implementation(project(":compose:ui:ui-util"))
- api(project(":compose:foundation:foundation"))
- api(project(":compose:foundation:foundation-layout"))
+ implementation("androidx.compose.ui:ui-util:1.7.1")
+ api("androidx.compose.foundation:foundation:1.7.1")
+ api("androidx.compose.foundation:foundation-layout:1.7.1")
api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui-graphics"))
- api(project(":compose:ui:ui-text"))
+ api("androidx.compose.ui:ui-graphics:1.7.1")
+ api("androidx.compose.ui:ui-text:1.7.1")
}
}
diff --git a/compose/material3/material3-common/samples/build.gradle b/compose/material3/material3-common/samples/build.gradle
index 54f689b..3036c4a 100644
--- a/compose/material3/material3-common/samples/build.gradle
+++ b/compose/material3/material3-common/samples/build.gradle
@@ -40,9 +40,8 @@
implementation(project(":compose:material3:material3-common"))
implementation("androidx.compose.runtime:runtime:1.2.1")
implementation("androidx.compose.ui:ui:1.2.1")
+ implementation("androidx.compose.ui:ui-tooling:1.4.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
-
- debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
}
androidx {
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 92988f3..04d992e 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -41,10 +41,10 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlib)
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.7.1")
api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-unit"))
+ api("androidx.compose.ui:ui:1.7.1")
+ api("androidx.compose.ui:ui-unit:1.7.1")
}
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index f6366cc..80d67dd 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -651,6 +651,47 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class DismissibleModalWideNavigationRailDefaults {
+ method public androidx.compose.material3.ModalWideNavigationRailProperties getProperties();
+ property public final androidx.compose.material3.ModalWideNavigationRailProperties Properties;
+ field public static final androidx.compose.material3.DismissibleModalWideNavigationRailDefaults INSTANCE;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class DismissibleModalWideNavigationRailState {
+ ctor public DismissibleModalWideNavigationRailState(androidx.compose.material3.DismissibleModalWideNavigationRailValue initialValue, androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange);
+ method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getAnimationSpec();
+ method public kotlin.jvm.functions.Function1<androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> getConfirmValueChange();
+ method public float getCurrentOffset();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getCurrentValue();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getInitialValue();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getTargetValue();
+ method public boolean isAnimationRunning();
+ method public boolean isOpen();
+ method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setConfirmValueChange(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean>);
+ method public void setInitialValue(androidx.compose.material3.DismissibleModalWideNavigationRailValue);
+ method public suspend Object? snapTo(androidx.compose.material3.DismissibleModalWideNavigationRailValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange;
+ property public final float currentOffset;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue currentValue;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue initialValue;
+ property public final boolean isAnimationRunning;
+ property public final boolean isOpen;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue targetValue;
+ field public static final androidx.compose.material3.DismissibleModalWideNavigationRailState.Companion Companion;
+ }
+
+ public static final class DismissibleModalWideNavigationRailState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissibleModalWideNavigationRailState,androidx.compose.material3.DismissibleModalWideNavigationRailValue> Saver(androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmStateChange);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public enum DismissibleModalWideNavigationRailValue {
+ enum_constant public static final androidx.compose.material3.DismissibleModalWideNavigationRailValue Closed;
+ enum_constant public static final androidx.compose.material3.DismissibleModalWideNavigationRailValue Open;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DisplayMode {
field public static final androidx.compose.material3.DisplayMode.Companion Companion;
}
@@ -875,7 +916,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class FloatingAppBarDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingAppBarScrollBehavior exitAlwaysScrollBehavior(int position, optional float screenOffset, optional androidx.compose.material3.FloatingAppBarState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingAppBarScrollBehavior exitAlwaysScrollBehavior(int exitDirection, optional androidx.compose.material3.FloatingAppBarState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
@@ -895,18 +936,11 @@
field public static final androidx.compose.material3.FloatingAppBarDefaults INSTANCE;
}
- public final class FloatingAppBarKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingAppBarState FloatingAppBarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingAppBarState rememberFloatingAppBarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class FloatingAppBarExitDirection {
+ field public static final androidx.compose.material3.FloatingAppBarExitDirection.Companion Companion;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class FloatingAppBarPosition {
- field public static final androidx.compose.material3.FloatingAppBarPosition.Companion Companion;
- }
-
- public static final class FloatingAppBarPosition.Companion {
+ public static final class FloatingAppBarExitDirection.Companion {
method public int getBottom();
method public int getEnd();
method public int getStart();
@@ -917,16 +951,21 @@
property public final int Top;
}
+ public final class FloatingAppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingAppBarState FloatingAppBarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingAppBarState rememberFloatingAppBarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public sealed interface FloatingAppBarScrollBehavior extends androidx.compose.ui.input.nestedscroll.NestedScrollConnection {
- method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier floatingScrollBehavior(androidx.compose.ui.Modifier);
+ method public androidx.compose.ui.Modifier floatingScrollBehavior(androidx.compose.ui.Modifier);
+ method public int getExitDirection();
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getFlingAnimationSpec();
- method public int getPosition();
- method public float getScreenOffset();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getSnapAnimationSpec();
method public androidx.compose.material3.FloatingAppBarState getState();
+ property public abstract int exitDirection;
property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec;
- property public abstract int position;
- property public abstract float screenOffset;
property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec;
property public abstract androidx.compose.material3.FloatingAppBarState state;
}
@@ -1183,15 +1222,15 @@
method public java.util.List<androidx.graphics.shapes.RoundedPolygon> getIndeterminateIndicatorPolygons();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
method public float getIndicatorSize();
- property @androidx.compose.runtime.Composable public final long ContainedContainerColor;
- property @androidx.compose.runtime.Composable public final long ContainedIndicatorColor;
property public final float ContainerHeight;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ContainerShape;
property public final float ContainerWidth;
property public final java.util.List<androidx.graphics.shapes.RoundedPolygon> DeterminateIndicatorPolygons;
property public final java.util.List<androidx.graphics.shapes.RoundedPolygon> IndeterminateIndicatorPolygons;
- property @androidx.compose.runtime.Composable public final long IndicatorColor;
property public final float IndicatorSize;
+ property @androidx.compose.runtime.Composable public final long containedContainerColor;
+ property @androidx.compose.runtime.Composable public final long containedIndicatorColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
+ property @androidx.compose.runtime.Composable public final long indicatorColor;
field public static final androidx.compose.material3.LoadingIndicatorDefaults INSTANCE;
}
@@ -1363,57 +1402,16 @@
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalExpandedNavigationRailDefaults {
- method public androidx.compose.material3.ModalExpandedNavigationRailProperties getProperties();
- property public final androidx.compose.material3.ModalExpandedNavigationRailProperties Properties;
- field public static final androidx.compose.material3.ModalExpandedNavigationRailDefaults INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalExpandedNavigationRailProperties {
- ctor public ModalExpandedNavigationRailProperties();
- ctor public ModalExpandedNavigationRailProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
- ctor public ModalExpandedNavigationRailProperties(optional boolean shouldDismissOnBackPress);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalWideNavigationRailProperties {
+ ctor public ModalWideNavigationRailProperties();
+ ctor public ModalWideNavigationRailProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
+ ctor public ModalWideNavigationRailProperties(optional boolean shouldDismissOnBackPress);
method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
method public boolean getShouldDismissOnBackPress();
property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
property public final boolean shouldDismissOnBackPress;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ModalExpandedNavigationRailState {
- ctor public ModalExpandedNavigationRailState(androidx.compose.material3.ModalExpandedNavigationRailValue initialValue, androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getAnimationSpec();
- method public kotlin.jvm.functions.Function1<androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> getConfirmValueChange();
- method public float getCurrentOffset();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getCurrentValue();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getInitialValue();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getTargetValue();
- method public boolean isAnimationRunning();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void setConfirmValueChange(kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean>);
- method public void setInitialValue(androidx.compose.material3.ModalExpandedNavigationRailValue);
- method public suspend Object? snapTo(androidx.compose.material3.ModalExpandedNavigationRailValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec;
- property public final kotlin.jvm.functions.Function1<androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange;
- property public final float currentOffset;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue currentValue;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue initialValue;
- property public final boolean isAnimationRunning;
- property public final boolean isOpen;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue targetValue;
- field public static final androidx.compose.material3.ModalExpandedNavigationRailState.Companion Companion;
- }
-
- public static final class ModalExpandedNavigationRailState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.ModalExpandedNavigationRailState,androidx.compose.material3.ModalExpandedNavigationRailValue> Saver(androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmStateChange);
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public enum ModalExpandedNavigationRailValue {
- enum_constant public static final androidx.compose.material3.ModalExpandedNavigationRailValue Closed;
- enum_constant public static final androidx.compose.material3.ModalExpandedNavigationRailValue Open;
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public interface MotionScheme {
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> defaultEffectsSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> defaultSpatialSpec();
@@ -2939,9 +2937,9 @@
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
@@ -2959,16 +2957,16 @@
}
@androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
- ctor public WideNavigationRailColors(long containerColor, long contentColor, long expandedModalContainerColor, long expandedModalScrimColor);
- method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long expandedModalContainerColor, optional long modalScrimColor);
+ ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
+ method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
method public long getContainerColor();
method public long getContentColor();
- method public long getExpandedModalContainerColor();
- method public long getExpandedModalScrimColor();
+ method public long getModalContainerColor();
+ method public long getModalScrimColor();
property public final long containerColor;
property public final long contentColor;
- property public final long expandedModalContainerColor;
- property public final long expandedModalScrimColor;
+ property public final long modalContainerColor;
+ property public final long modalScrimColor;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
@@ -2991,13 +2989,14 @@
}
public final class WideNavigationRailKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalExpandedNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.ModalExpandedNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalExpandedNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void DismissibleModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DismissibleModalWideNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalWideNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> scrimOnClick, optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public final class WideNavigationRailStateKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.material3.ModalExpandedNavigationRailState rememberModalExpandedNavigationRailState(optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange);
+ method @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissibleModalWideNavigationRailState rememberDismissibleModalWideNavigationRailState(optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange);
}
}
@@ -3070,9 +3069,11 @@
method @Deprecated @androidx.compose.runtime.Composable public long getIndicatorColor();
method @androidx.compose.runtime.Composable public long getLoadingIndicatorColor();
method @androidx.compose.runtime.Composable public long getLoadingIndicatorContainerColor();
+ method public float getLoadingIndicatorElevation();
method public float getPositionalThreshold();
method public androidx.compose.ui.graphics.Shape getShape();
property public final float Elevation;
+ property public final float LoadingIndicatorElevation;
property public final float PositionalThreshold;
property @Deprecated @androidx.compose.runtime.Composable public final long containerColor;
property @Deprecated @androidx.compose.runtime.Composable public final long indicatorColor;
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index f6366cc..80d67dd 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -651,6 +651,47 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class DismissibleModalWideNavigationRailDefaults {
+ method public androidx.compose.material3.ModalWideNavigationRailProperties getProperties();
+ property public final androidx.compose.material3.ModalWideNavigationRailProperties Properties;
+ field public static final androidx.compose.material3.DismissibleModalWideNavigationRailDefaults INSTANCE;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class DismissibleModalWideNavigationRailState {
+ ctor public DismissibleModalWideNavigationRailState(androidx.compose.material3.DismissibleModalWideNavigationRailValue initialValue, androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange);
+ method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getAnimationSpec();
+ method public kotlin.jvm.functions.Function1<androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> getConfirmValueChange();
+ method public float getCurrentOffset();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getCurrentValue();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getInitialValue();
+ method public androidx.compose.material3.DismissibleModalWideNavigationRailValue getTargetValue();
+ method public boolean isAnimationRunning();
+ method public boolean isOpen();
+ method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setConfirmValueChange(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean>);
+ method public void setInitialValue(androidx.compose.material3.DismissibleModalWideNavigationRailValue);
+ method public suspend Object? snapTo(androidx.compose.material3.DismissibleModalWideNavigationRailValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange;
+ property public final float currentOffset;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue currentValue;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue initialValue;
+ property public final boolean isAnimationRunning;
+ property public final boolean isOpen;
+ property public final androidx.compose.material3.DismissibleModalWideNavigationRailValue targetValue;
+ field public static final androidx.compose.material3.DismissibleModalWideNavigationRailState.Companion Companion;
+ }
+
+ public static final class DismissibleModalWideNavigationRailState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissibleModalWideNavigationRailState,androidx.compose.material3.DismissibleModalWideNavigationRailValue> Saver(androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmStateChange);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public enum DismissibleModalWideNavigationRailValue {
+ enum_constant public static final androidx.compose.material3.DismissibleModalWideNavigationRailValue Closed;
+ enum_constant public static final androidx.compose.material3.DismissibleModalWideNavigationRailValue Open;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DisplayMode {
field public static final androidx.compose.material3.DisplayMode.Companion Companion;
}
@@ -875,7 +916,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class FloatingAppBarDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingAppBarScrollBehavior exitAlwaysScrollBehavior(int position, optional float screenOffset, optional androidx.compose.material3.FloatingAppBarState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingAppBarScrollBehavior exitAlwaysScrollBehavior(int exitDirection, optional androidx.compose.material3.FloatingAppBarState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
@@ -895,18 +936,11 @@
field public static final androidx.compose.material3.FloatingAppBarDefaults INSTANCE;
}
- public final class FloatingAppBarKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingAppBarState FloatingAppBarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingAppBarState rememberFloatingAppBarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class FloatingAppBarExitDirection {
+ field public static final androidx.compose.material3.FloatingAppBarExitDirection.Companion Companion;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class FloatingAppBarPosition {
- field public static final androidx.compose.material3.FloatingAppBarPosition.Companion Companion;
- }
-
- public static final class FloatingAppBarPosition.Companion {
+ public static final class FloatingAppBarExitDirection.Companion {
method public int getBottom();
method public int getEnd();
method public int getStart();
@@ -917,16 +951,21 @@
property public final int Top;
}
+ public final class FloatingAppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingAppBarState FloatingAppBarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingAppBar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingAppBarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingAppBarState rememberFloatingAppBarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Stable public sealed interface FloatingAppBarScrollBehavior extends androidx.compose.ui.input.nestedscroll.NestedScrollConnection {
- method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier floatingScrollBehavior(androidx.compose.ui.Modifier);
+ method public androidx.compose.ui.Modifier floatingScrollBehavior(androidx.compose.ui.Modifier);
+ method public int getExitDirection();
method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getFlingAnimationSpec();
- method public int getPosition();
- method public float getScreenOffset();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getSnapAnimationSpec();
method public androidx.compose.material3.FloatingAppBarState getState();
+ property public abstract int exitDirection;
property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> flingAnimationSpec;
- property public abstract int position;
- property public abstract float screenOffset;
property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec;
property public abstract androidx.compose.material3.FloatingAppBarState state;
}
@@ -1183,15 +1222,15 @@
method public java.util.List<androidx.graphics.shapes.RoundedPolygon> getIndeterminateIndicatorPolygons();
method @androidx.compose.runtime.Composable public long getIndicatorColor();
method public float getIndicatorSize();
- property @androidx.compose.runtime.Composable public final long ContainedContainerColor;
- property @androidx.compose.runtime.Composable public final long ContainedIndicatorColor;
property public final float ContainerHeight;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ContainerShape;
property public final float ContainerWidth;
property public final java.util.List<androidx.graphics.shapes.RoundedPolygon> DeterminateIndicatorPolygons;
property public final java.util.List<androidx.graphics.shapes.RoundedPolygon> IndeterminateIndicatorPolygons;
- property @androidx.compose.runtime.Composable public final long IndicatorColor;
property public final float IndicatorSize;
+ property @androidx.compose.runtime.Composable public final long containedContainerColor;
+ property @androidx.compose.runtime.Composable public final long containedIndicatorColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
+ property @androidx.compose.runtime.Composable public final long indicatorColor;
field public static final androidx.compose.material3.LoadingIndicatorDefaults INSTANCE;
}
@@ -1363,57 +1402,16 @@
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalExpandedNavigationRailDefaults {
- method public androidx.compose.material3.ModalExpandedNavigationRailProperties getProperties();
- property public final androidx.compose.material3.ModalExpandedNavigationRailProperties Properties;
- field public static final androidx.compose.material3.ModalExpandedNavigationRailDefaults INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalExpandedNavigationRailProperties {
- ctor public ModalExpandedNavigationRailProperties();
- ctor public ModalExpandedNavigationRailProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
- ctor public ModalExpandedNavigationRailProperties(optional boolean shouldDismissOnBackPress);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public final class ModalWideNavigationRailProperties {
+ ctor public ModalWideNavigationRailProperties();
+ ctor public ModalWideNavigationRailProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
+ ctor public ModalWideNavigationRailProperties(optional boolean shouldDismissOnBackPress);
method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
method public boolean getShouldDismissOnBackPress();
property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
property public final boolean shouldDismissOnBackPress;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ModalExpandedNavigationRailState {
- ctor public ModalExpandedNavigationRailState(androidx.compose.material3.ModalExpandedNavigationRailValue initialValue, androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getAnimationSpec();
- method public kotlin.jvm.functions.Function1<androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> getConfirmValueChange();
- method public float getCurrentOffset();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getCurrentValue();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getInitialValue();
- method public androidx.compose.material3.ModalExpandedNavigationRailValue getTargetValue();
- method public boolean isAnimationRunning();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public void setConfirmValueChange(kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean>);
- method public void setInitialValue(androidx.compose.material3.ModalExpandedNavigationRailValue);
- method public suspend Object? snapTo(androidx.compose.material3.ModalExpandedNavigationRailValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec;
- property public final kotlin.jvm.functions.Function1<androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange;
- property public final float currentOffset;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue currentValue;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue initialValue;
- property public final boolean isAnimationRunning;
- property public final boolean isOpen;
- property public final androidx.compose.material3.ModalExpandedNavigationRailValue targetValue;
- field public static final androidx.compose.material3.ModalExpandedNavigationRailState.Companion Companion;
- }
-
- public static final class ModalExpandedNavigationRailState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.ModalExpandedNavigationRailState,androidx.compose.material3.ModalExpandedNavigationRailValue> Saver(androidx.compose.ui.unit.Density density, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmStateChange);
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public enum ModalExpandedNavigationRailValue {
- enum_constant public static final androidx.compose.material3.ModalExpandedNavigationRailValue Closed;
- enum_constant public static final androidx.compose.material3.ModalExpandedNavigationRailValue Open;
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Immutable public interface MotionScheme {
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> defaultEffectsSpec();
method public <T> androidx.compose.animation.core.FiniteAnimationSpec<T> defaultSpatialSpec();
@@ -2939,9 +2937,9 @@
}
public final class WavyProgressIndicatorKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional @FloatRange(from=0.0, to=1.0) float amplitude, optional float wavelength, optional float waveSpeed);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
@@ -2959,16 +2957,16 @@
}
@androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
- ctor public WideNavigationRailColors(long containerColor, long contentColor, long expandedModalContainerColor, long expandedModalScrimColor);
- method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long expandedModalContainerColor, optional long modalScrimColor);
+ ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
+ method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
method public long getContainerColor();
method public long getContentColor();
- method public long getExpandedModalContainerColor();
- method public long getExpandedModalScrimColor();
+ method public long getModalContainerColor();
+ method public long getModalScrimColor();
property public final long containerColor;
property public final long contentColor;
- property public final long expandedModalContainerColor;
- property public final long expandedModalScrimColor;
+ property public final long modalContainerColor;
+ property public final long modalScrimColor;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
@@ -2991,13 +2989,14 @@
}
public final class WideNavigationRailKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalExpandedNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.ModalExpandedNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalExpandedNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void DismissibleModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DismissibleModalWideNavigationRailState railState, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional boolean gesturesEnabled, optional androidx.compose.material3.ModalWideNavigationRailProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(kotlin.jvm.functions.Function0<kotlin.Unit> scrimOnClick, optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional boolean expanded, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
public final class WideNavigationRailStateKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.material3.ModalExpandedNavigationRailState rememberModalExpandedNavigationRailState(optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.ModalExpandedNavigationRailValue,java.lang.Boolean> confirmValueChange);
+ method @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissibleModalWideNavigationRailState rememberDismissibleModalWideNavigationRailState(optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissibleModalWideNavigationRailValue,java.lang.Boolean> confirmValueChange);
}
}
@@ -3070,9 +3069,11 @@
method @Deprecated @androidx.compose.runtime.Composable public long getIndicatorColor();
method @androidx.compose.runtime.Composable public long getLoadingIndicatorColor();
method @androidx.compose.runtime.Composable public long getLoadingIndicatorContainerColor();
+ method public float getLoadingIndicatorElevation();
method public float getPositionalThreshold();
method public androidx.compose.ui.graphics.Shape getShape();
property public final float Elevation;
+ property public final float LoadingIndicatorElevation;
property public final float PositionalThreshold;
property @Deprecated @androidx.compose.runtime.Composable public final long containerColor;
property @Deprecated @androidx.compose.runtime.Composable public final long indicatorColor;
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index fd903f1..273efca 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -43,15 +43,15 @@
implementation(libs.kotlinStdlib)
// Keep pinned unless there is a need for tip of tree behavior
implementation("androidx.collection:collection:1.4.2")
- implementation(project(":compose:animation:animation-core"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.animation:animation-core:1.7.1")
+ implementation("androidx.compose.ui:ui-util:1.7.1")
api(project(":compose:foundation:foundation"))
- api(project(":compose:foundation:foundation-layout"))
- api(project(":compose:material:material-ripple"))
+ api("androidx.compose.foundation:foundation-layout:1.7.1")
+ api("androidx.compose.material:material-ripple:1.7.1")
api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-text"))
- api(project(":graphics:graphics-shapes"))
+ api("androidx.compose.ui:ui:1.7.1")
+ api("androidx.compose.ui:ui-text:1.7.1")
+ api("androidx.graphics:graphics-shapes:1.0.1")
}
}
@@ -107,7 +107,7 @@
implementation(project(':compose:foundation:foundation'))
implementation("androidx.compose.material:material-icons-core:1.6.8")
implementation(project(":test:screenshot:screenshot"))
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 4b3c7ab..d5987aa 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -58,6 +58,7 @@
import androidx.compose.material3.samples.DenseTextFieldContentPadding
import androidx.compose.material3.samples.DeterminateContainedLoadingIndicatorSample
import androidx.compose.material3.samples.DeterminateLoadingIndicatorSample
+import androidx.compose.material3.samples.DismissibleModalWideNavigationRailSample
import androidx.compose.material3.samples.DismissibleNavigationDrawerSample
import androidx.compose.material3.samples.DockedSearchBarSample
import androidx.compose.material3.samples.EditableExposedDropdownMenuSample
@@ -126,8 +127,8 @@
import androidx.compose.material3.samples.MenuSample
import androidx.compose.material3.samples.MenuWithScrollStateSample
import androidx.compose.material3.samples.ModalBottomSheetSample
-import androidx.compose.material3.samples.ModalExpandedNavigationRailSample
import androidx.compose.material3.samples.ModalNavigationDrawerSample
+import androidx.compose.material3.samples.ModalWideNavigationRailSample
import androidx.compose.material3.samples.MultiAutocompleteExposedDropdownMenuSample
import androidx.compose.material3.samples.MultiSelectConnectedButtonGroupSample
import androidx.compose.material3.samples.NavigationBarItemWithBadge
@@ -1231,11 +1232,18 @@
WideNavigationRailResponsiveSample()
},
Example(
- name = "ModalExpandedNavigationRailSample",
+ name = "ModalWideNavigationRailSample",
description = NavigationRailExampleDescription,
sourceUrl = NavigationRailExampleSourceUrl,
) {
- ModalExpandedNavigationRailSample()
+ ModalWideNavigationRailSample()
+ },
+ Example(
+ name = "DismissibleModalWideNavigationRailSample",
+ description = NavigationRailExampleDescription,
+ sourceUrl = NavigationRailExampleSourceUrl,
+ ) {
+ DismissibleModalWideNavigationRailSample()
},
Example(
name = "WideNavigationRailCollapsedSample",
diff --git a/compose/material3/material3/lint-baseline.xml b/compose/material3/material3/lint-baseline.xml
index 6270830..36d385f 100644
--- a/compose/material3/material3/lint-baseline.xml
+++ b/compose/material3/material3/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="BanThreadSleep"
@@ -169,6 +169,15 @@
errorLine1=" Thread.sleep(300)"
errorLine2=" ~~~~~">
<location
+ file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt"/>
+ </issue>
+
+ <issue
+ id="BanThreadSleep"
+ message="Uses Thread.sleep()"
+ errorLine1=" Thread.sleep(300)"
+ errorLine2=" ~~~~~">
+ <location
file="src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt"/>
</issue>
@@ -290,6 +299,15 @@
</issue>
<issue
+ id="ListIterator"
+ message="Creating an unnecessary Iterator to iterate through a List"
+ errorLine1=" perVertexRounding = buildList { for (p in actualPoints) add(p.r) },"
+ errorLine2=" ~~">
+ <location
+ file="src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt"/>
+ </issue>
+
+ <issue
id="PrimitiveInCollection"
message="variable crossAxisSizes with type List<Integer>: replace with IntList"
errorLine1=" val crossAxisSizes = mutableListOf<Int>()"
diff --git a/compose/material3/material3/samples/build.gradle b/compose/material3/material3/samples/build.gradle
index c514173..107bcc8 100644
--- a/compose/material3/material3/samples/build.gradle
+++ b/compose/material3/material3/samples/build.gradle
@@ -47,10 +47,9 @@
implementation("androidx.compose.ui:ui:1.2.1")
implementation("androidx.compose.ui:ui-text:1.2.1")
implementation("androidx.savedstate:savedstate-ktx:1.2.1")
+ implementation("androidx.compose.ui:ui-tooling:1.4.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
- implementation("androidx.graphics:graphics-shapes:1.0.0-beta01")
-
- debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
+ implementation("androidx.graphics:graphics-shapes:1.0.1")
}
androidx {
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingAppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingAppBarSamples.kt
index cb7e259..bfda681 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingAppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingAppBarSamples.kt
@@ -38,8 +38,8 @@
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingAppBarDefaults
import androidx.compose.material3.FloatingAppBarDefaults.ScreenOffset
-import androidx.compose.material3.FloatingAppBarPosition.Companion.Bottom
-import androidx.compose.material3.FloatingAppBarPosition.Companion.End
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.Bottom
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.End
import androidx.compose.material3.HorizontalFloatingAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -123,7 +123,7 @@
}
val listState = rememberLazyListState()
val exitAlwaysScrollBehavior =
- FloatingAppBarDefaults.exitAlwaysScrollBehavior(position = Bottom)
+ FloatingAppBarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
Scaffold(
modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior),
content = { innerPadding ->
@@ -227,7 +227,8 @@
am.isEnabled && am.isTouchExplorationEnabled
}
val listState = rememberLazyListState()
- val exitAlwaysScrollBehavior = FloatingAppBarDefaults.exitAlwaysScrollBehavior(position = End)
+ val exitAlwaysScrollBehavior =
+ FloatingAppBarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
Scaffold(
modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior),
content = { innerPadding ->
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
index c5e71bb..b5f95b2 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
@@ -38,17 +38,18 @@
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
+import androidx.compose.material3.DismissibleModalWideNavigationRail
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.ModalExpandedNavigationRail
+import androidx.compose.material3.ModalWideNavigationRail
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text
import androidx.compose.material3.WideNavigationRail
import androidx.compose.material3.WideNavigationRailArrangement
import androidx.compose.material3.WideNavigationRailItem
-import androidx.compose.material3.rememberModalExpandedNavigationRailState
+import androidx.compose.material3.rememberDismissibleModalWideNavigationRailState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -145,7 +146,83 @@
@Preview
@Sampled
@Composable
-fun ModalExpandedNavigationRailSample() {
+fun ModalWideNavigationRailSample() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ val items = listOf("Home", "Search", "Settings")
+ val selectedIcons = listOf(Icons.Filled.Home, Icons.Filled.Favorite, Icons.Filled.Star)
+ val unselectedIcons =
+ listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
+ var expanded by remember { mutableStateOf(false) }
+
+ Row(Modifier.fillMaxWidth()) {
+ ModalWideNavigationRail(
+ expanded = expanded,
+ scrimOnClick = { expanded = false },
+ // Note: the value of expandedHeaderTopPadding depends on the layout of your screen in
+ // order to achieve the best alignment.
+ expandedHeaderTopPadding = 64.dp,
+ header = {
+ IconButton(
+ modifier =
+ Modifier.padding(start = 24.dp).semantics {
+ // The button must announce the expanded or collapsed state of the rail
+ // for accessibility.
+ stateDescription = if (expanded) "Expanded" else "Collapsed"
+ },
+ onClick = { expanded = !expanded }
+ ) {
+ if (expanded) Icon(Icons.AutoMirrored.Filled.MenuOpen, "Collapse rail")
+ else Icon(Icons.Filled.Menu, "Expand rail")
+ }
+ }
+ ) {
+ items.forEachIndexed { index, item ->
+ WideNavigationRailItem(
+ railExpanded = expanded,
+ icon = {
+ Icon(
+ if (selectedItem == index) selectedIcons[index]
+ else unselectedIcons[index],
+ contentDescription = item
+ )
+ },
+ label = { Text(item) },
+ selected = selectedItem == index,
+ onClick = { selectedItem = index }
+ )
+ }
+ }
+
+ val textString = if (expanded) "expanded" else "collapsed"
+ Column {
+ Text(modifier = Modifier.padding(16.dp), text = "The rail is $textString.")
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text =
+ "Note: The orientation of this demo has been locked to portrait mode, because" +
+ " landscape mode may result in a compact height in certain devices. For" +
+ " any compact screen dimensions, use a Navigation Bar instead."
+ )
+ }
+
+ // Lock the orientation for this demo as the navigation rail may look cut off in landscape
+ // in smaller screens.
+ val context = LocalContext.current
+ DisposableEffect(context) {
+ (context as? Activity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ onDispose {
+ (context as? Activity)?.requestedOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun DismissibleModalWideNavigationRailSample() {
var selectedItem by remember { mutableIntStateOf(0) }
val items = listOf("Home", "Search", "Settings")
val selectedIcons = listOf(Icons.Filled.Home, Icons.Filled.Favorite, Icons.Filled.Star)
@@ -153,12 +230,12 @@
listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
var openModalRail by rememberSaveable { mutableStateOf(false) }
var dismissRailOnItemSelection by rememberSaveable { mutableStateOf(true) }
- val modalRailState = rememberModalExpandedNavigationRailState()
+ val modalRailState = rememberDismissibleModalWideNavigationRailState()
val scope = rememberCoroutineScope()
Row(Modifier.fillMaxSize()) {
if (openModalRail) {
- ModalExpandedNavigationRail(
+ DismissibleModalWideNavigationRail(
onDismissRequest = { openModalRail = false },
railState = modalRailState
) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 0bd5601..7ce7054 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -102,7 +102,7 @@
textContentColor = Color.DarkGray
)
}
-
+ rule.waitForIdle()
// Assert background
rule
.onNode(isDialog())
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 61654d2..3e166ef 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -30,6 +30,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
@@ -40,6 +41,7 @@
import androidx.compose.material3.internal.getString
import androidx.compose.material3.tokens.SheetBottomTokens
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -84,9 +86,11 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.width
import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -96,6 +100,8 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import junit.framework.TestCase
+import junit.framework.TestCase.assertEquals
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -903,4 +909,121 @@
with(density!!) { rule.rootHeight().toPx() - peekHeight.toPx() - snackbarSize!!.height }
assertThat(snackbarBottomOffset).isWithin(1f).of(expectedSnackbarBottomOffset)
}
+
+ @Test
+ fun bottomSheetScaffold_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ BottomSheetScaffold(
+ sheetContent = {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ },
+ scaffoldState =
+ BottomSheetScaffoldState(state, remember { SnackbarHostState() })
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ fun modalBottomSheet_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ ModalBottomSheet({}, sheetState = state) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ }
+ }
+
+ fun LayoutCoordinates.root(): LayoutCoordinates =
+ if (parentLayoutCoordinates != null) parentLayoutCoordinates!!.root() else this
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+ val rootCoords = sheetCoords!!.root()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingAppBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingAppBarTest.kt
index ad4ec7b9..8b7fa2c 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingAppBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingAppBarTest.kt
@@ -20,10 +20,11 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.offset
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.FloatingAppBarPosition.Companion.Bottom
-import androidx.compose.material3.FloatingAppBarPosition.Companion.End
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.Bottom
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.End
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
@@ -67,14 +68,20 @@
var containerColor = Color.Unspecified
val scrollHeightOffsetDp = 20.dp
var scrollHeightOffsetPx = 0f
+ var containerSizePx = 0f
+ val screenOffsetDp = FloatingAppBarDefaults.ScreenOffset
+ var screenOffsetPx = 0f
rule.setMaterialContent(lightColorScheme()) {
backgroundColor = MaterialTheme.colorScheme.background
containerColor = FloatingAppBarDefaults.ContainerColor
- scrollBehavior = FloatingAppBarDefaults.exitAlwaysScrollBehavior(position = Bottom)
+ scrollBehavior = FloatingAppBarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+ containerSizePx =
+ with(LocalDensity.current) { FloatingAppBarDefaults.ContainerSize.toPx() }
+ screenOffsetPx = with(LocalDensity.current) { screenOffsetDp.toPx() }
HorizontalFloatingAppBar(
- modifier = Modifier.testTag(FloatingAppBarTestTag),
+ modifier = Modifier.testTag(FloatingAppBarTestTag).offset(y = -screenOffsetDp),
expanded = false,
scrollBehavior = scrollBehavior,
shape = RectangleShape,
@@ -86,6 +93,7 @@
)
}
+ assertThat(scrollBehavior.state.offsetLimit).isEqualTo(-(containerSizePx + screenOffsetPx))
// Simulate scrolled content.
rule.runOnIdle {
scrollBehavior.state.offset = -scrollHeightOffsetPx
@@ -93,10 +101,11 @@
}
rule.waitForIdle()
rule.onNodeWithTag(FloatingAppBarTestTag).captureToImage().assertPixels(null) { pos ->
+ val scrolled = (scrollHeightOffsetPx - screenOffsetPx).roundToInt()
when (pos.y) {
0 -> backgroundColor
- scrollHeightOffsetPx.roundToInt() - 2 -> backgroundColor
- scrollHeightOffsetPx.roundToInt() -> containerColor
+ scrolled - 2 -> backgroundColor
+ scrolled -> containerColor
else -> null
}
}
@@ -109,14 +118,20 @@
var containerColor = Color.Unspecified
val scrollHeightOffsetDp = 20.dp
var scrollHeightOffsetPx = 0f
+ var containerSizePx = 0f
+ val screenOffsetDp = FloatingAppBarDefaults.ScreenOffset
+ var screenOffsetPx = 0f
rule.setMaterialContent(lightColorScheme()) {
backgroundColor = MaterialTheme.colorScheme.background
containerColor = FloatingAppBarDefaults.ContainerColor
- scrollBehavior = FloatingAppBarDefaults.exitAlwaysScrollBehavior(position = End)
+ scrollBehavior = FloatingAppBarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+ containerSizePx =
+ with(LocalDensity.current) { FloatingAppBarDefaults.ContainerSize.toPx() }
+ screenOffsetPx = with(LocalDensity.current) { screenOffsetDp.toPx() }
VerticalFloatingAppBar(
- modifier = Modifier.testTag(FloatingAppBarTestTag),
+ modifier = Modifier.testTag(FloatingAppBarTestTag).offset(x = -screenOffsetDp),
expanded = false,
scrollBehavior = scrollBehavior,
shape = RectangleShape,
@@ -128,6 +143,7 @@
)
}
+ assertThat(scrollBehavior.state.offsetLimit).isEqualTo(-(containerSizePx + screenOffsetPx))
// Simulate scrolled content.
rule.runOnIdle {
scrollBehavior.state.offset = -scrollHeightOffsetPx
@@ -135,10 +151,11 @@
}
rule.waitForIdle()
rule.onNodeWithTag(FloatingAppBarTestTag).captureToImage().assertPixels(null) { pos ->
+ val scrolled = (scrollHeightOffsetPx - screenOffsetPx).roundToInt()
when (pos.x) {
0 -> backgroundColor
- scrollHeightOffsetPx.roundToInt() - 2 -> backgroundColor
- scrollHeightOffsetPx.roundToInt() -> containerColor
+ scrolled - 2 -> backgroundColor
+ scrolled -> containerColor
else -> null
}
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index d57c4a2..da0d64f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -24,6 +24,8 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -190,7 +192,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -202,7 +204,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -274,7 +276,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -290,7 +292,7 @@
onCheckedChange = { /* doSomething() */ },
enabled = false
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -302,7 +304,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -377,7 +379,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -392,7 +394,7 @@
checked = false,
onCheckedChange = { /* doSomething() */ }
) {
- Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
}
}
}
@@ -434,10 +436,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -449,10 +448,7 @@
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
@@ -464,10 +460,7 @@
rule.setMaterialContent(darkColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
OutlinedIconButton(onClick = { /* doSomething() */ }) {
- Icon(
- Icons.Outlined.FavoriteBorder,
- contentDescription = "Localized description"
- )
+ Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
}
}
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt
deleted file mode 100644
index a3a1596..0000000
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailScreenshotTest.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import android.os.Build
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.Home
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.isDialog
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-class ModalExpandedNavigationRailScreenshotTest {
-
- @get:Rule val composeTestRule = createComposeRule()
-
- @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
-
- @Test
- fun modalExpandedNavigationRail_lightTheme_defaultColors() {
- composeTestRule.setMaterialContent(lightColorScheme()) {
- DefaultModalExpandedNavigationRail()
- }
-
- assertModalExpandedNavigationRailMatches(
- goldenIdentifier = "wideNavigationRail_lightTheme_defaultColors"
- )
- }
-
- @Test
- fun modalExpandedNavigationRail_darkTheme_defaultColors() {
- composeTestRule.setMaterialContent(darkColorScheme()) {
- DefaultModalExpandedNavigationRail()
- }
-
- assertModalExpandedNavigationRailMatches(
- goldenIdentifier = "wideNavigationRail_darkTheme_defaultColors"
- )
- }
-
- /**
- * Asserts that the ModalExpandedNavigationRail matches the screenshot with identifier
- * [goldenIdentifier].
- *
- * @param goldenIdentifier the identifier for the corresponding screenshot
- */
- private fun assertModalExpandedNavigationRailMatches(goldenIdentifier: String) {
- // Capture and compare screenshots.
- composeTestRule
- .onNode(isDialog())
- .captureToImage()
- .assertAgainstGolden(screenshotRule, goldenIdentifier)
- }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-private fun DefaultModalExpandedNavigationRail() {
- Box(Modifier.fillMaxSize()) {
- ModalExpandedNavigationRail(
- onDismissRequest = {},
- ) {
- WideNavigationRailItem(
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("Favorites") },
- selected = true,
- onClick = {},
- )
- WideNavigationRailItem(
- railExpanded = true,
- icon = { Icon(Icons.Filled.Home, null) },
- label = { Text("Home") },
- selected = false,
- onClick = {}
- )
- WideNavigationRailItem(
- railExpanded = true,
- icon = { Icon(Icons.Filled.Search, null) },
- label = { Text("Search") },
- selected = false,
- onClick = {}
- )
- }
- }
-}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailTest.kt
deleted file mode 100644
index 97d4380..0000000
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalExpandedNavigationRailTest.kt
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material3.internal.Strings
-import androidx.compose.material3.internal.getString
-import androidx.compose.material3.tokens.NavigationRailExpandedTokens
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertHasClickAction
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.isDisplayed
-import androidx.compose.ui.test.isNotDisplayed
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onParent
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class ModalExpandedNavigationRailTest {
-
- @get:Rule val rule = createComposeRule()
- private val restorationTester = StateRestorationTester(rule)
-
- @Test
- fun modalRail_defaultSemantics() {
- rule.setMaterialContent(lightColorScheme()) {
- ModalExpandedNavigationRail(onDismissRequest = {}) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- rule
- .onNodeWithTag("item")
- .onParent()
- .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.SelectableGroup))
- }
-
- @Test
- fun modalRail_closes() {
- val railWidth = NavigationRailExpandedTokens.ContainerWidthMinimum
- lateinit var railState: ModalExpandedNavigationRailState
- lateinit var scope: CoroutineScope
-
- rule.setMaterialContentForSizeAssertions {
- railState = rememberModalExpandedNavigationRailState()
- scope = rememberCoroutineScope()
-
- ModalExpandedNavigationRail(onDismissRequest = {}, railState = railState) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- // Rail starts as open.
- assertThat(railState.isOpen).isTrue()
- // Close rail.
- scope.launch { railState.close() }
- rule.waitForIdle()
-
- // Assert rail is not open.
- assertThat(railState.isOpen).isFalse()
- // Assert rail is not displayed.
- rule.onNodeWithTag("item").onParent().isNotDisplayed()
- // Assert rail's offset.
- rule.onNodeWithTag("item").onParent().assertLeftPositionInRootIsEqualTo(-railWidth)
- }
-
- @Test
- fun modalRail_opens() {
- lateinit var railState: ModalExpandedNavigationRailState
- lateinit var scope: CoroutineScope
-
- rule.setMaterialContentForSizeAssertions {
- railState = rememberModalExpandedNavigationRailState()
- railState.initialValue = ModalExpandedNavigationRailValue.Closed
- scope = rememberCoroutineScope()
-
- ModalExpandedNavigationRail(onDismissRequest = {}, railState = railState) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- scope.launch { railState.close() }
- }
-
- scope.launch { railState.open() }
- rule.waitForIdle()
-
- // Assert rail is open.
- assertThat(railState.isOpen).isTrue()
- // Assert rail is displayed.
- rule.onNodeWithTag("item").onParent().isDisplayed()
- // Assert rail's offset.
- rule.onNodeWithTag("item").onParent().assertLeftPositionInRootIsEqualTo(0.dp)
- }
-
- @Test
- fun modalRail_closes_bySwiping() {
- lateinit var railState: ModalExpandedNavigationRailState
-
- rule.setMaterialContentForSizeAssertions {
- railState = rememberModalExpandedNavigationRailState()
-
- ModalExpandedNavigationRail(onDismissRequest = {}, railState = railState) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
- rule.waitForIdle()
-
- // Assert rail is not open.
- assertThat(railState.isOpen).isFalse()
- // Assert rail is not displayed.
- rule.onNodeWithTag("item").onParent().isNotDisplayed()
- }
-
- @Test
- fun modalRail_doesNotClose_bySwiping_gesturesDisabled() {
- lateinit var railState: ModalExpandedNavigationRailState
-
- rule.setMaterialContentForSizeAssertions {
- railState = rememberModalExpandedNavigationRailState()
-
- ModalExpandedNavigationRail(
- gesturesEnabled = false,
- onDismissRequest = {},
- railState = railState,
- ) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
- rule.waitForIdle()
-
- // Assert rail is still open.
- assertThat(railState.isOpen).isTrue()
- // Assert rail is still displayed.
- rule.onNodeWithTag("item").onParent().isDisplayed()
- }
-
- @Test
- fun modalRail_closes_byScrimClick() {
- lateinit var closeRail: String
- lateinit var railState: ModalExpandedNavigationRailState
- rule.setMaterialContentForSizeAssertions {
- closeRail = getString(Strings.CloseRail)
- railState = rememberModalExpandedNavigationRailState()
-
- ModalExpandedNavigationRail(
- gesturesEnabled = false,
- onDismissRequest = {},
- railState = railState,
- ) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- // The rail should be open.
- assertThat(railState.isOpen).isTrue()
-
- rule
- .onNodeWithContentDescription(closeRail)
- .assertHasClickAction()
- .performSemanticsAction(SemanticsActions.OnClick)
- rule.waitForIdle()
-
- // Assert rail is not open.
- assertThat(railState.isOpen).isFalse()
- // Assert rail is not displayed.
- rule.onNodeWithTag("item").onParent().isNotDisplayed()
- }
-
- @Test
- fun modalRail_hasPaneTitle() {
- lateinit var paneTitle: String
-
- rule.setMaterialContentForSizeAssertions {
- paneTitle = getString(Strings.WideNavigationRailPaneTitle)
- ModalExpandedNavigationRail(
- onDismissRequest = {},
- ) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- rule
- .onNodeWithTag("item")
- .onParent() // rail.
- .onParent() // dialog window.
- .onParent() // parent container that holds dialog and scrim.
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.PaneTitle, paneTitle))
- }
-
- @Test
- fun modalRailState_savesAndRestores() {
- lateinit var railState: ModalExpandedNavigationRailState
-
- restorationTester.setContent { railState = rememberModalExpandedNavigationRailState() }
-
- assertThat(railState.currentValue).isEqualTo(ModalExpandedNavigationRailValue.Closed)
- restorationTester.emulateSavedInstanceStateRestore()
- assertThat(railState.currentValue).isEqualTo(ModalExpandedNavigationRailValue.Closed)
- }
-
- @Test
- fun modalRailState_respectsConfirmStateChange() {
- lateinit var railState: ModalExpandedNavigationRailState
-
- restorationTester.setContent {
- railState =
- rememberModalExpandedNavigationRailState(
- confirmValueChange = { it != ModalExpandedNavigationRailValue.Closed }
- )
-
- ModalExpandedNavigationRail(onDismissRequest = {}, railState = railState) {
- WideNavigationRailItem(
- modifier = Modifier.testTag("item"),
- railExpanded = true,
- icon = { Icon(Icons.Filled.Favorite, null) },
- label = { Text("ItemText") },
- selected = true,
- onClick = {}
- )
- }
- }
-
- rule.runOnIdle {
- assertThat(railState.currentValue).isEqualTo(ModalExpandedNavigationRailValue.Open)
- }
- rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
- rule.waitForIdle()
-
- rule.runOnIdle {
- assertThat(railState.currentValue).isEqualTo(ModalExpandedNavigationRailValue.Open)
- }
- // Assert rail is still open.
- assertThat(railState.isOpen).isTrue()
- // Assert rail is still displayed.
- rule.onNodeWithTag("item").onParent().isDisplayed()
- }
-}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailScreenshotTest.kt
new file mode 100644
index 0000000..a7ddd5a
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailScreenshotTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [ModalWideNavigationRail] and [DismissibleModalWideNavigationRail]. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class ModalWideNavigationRailScreenshotTest {
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ @Test
+ fun modalExpandedNavigationRail_lightTheme_defaultColors() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ DefaultDismissibleModalWideNavigationRail()
+ }
+
+ assertModalExpandedNavigationRailMatches(
+ goldenIdentifier =
+ "wideNavigationRail_dismissibleModalWideNavigationRail_lightTheme_defaultColors"
+ )
+ }
+
+ @Test
+ fun modalExpandedNavigationRail_darkTheme_defaultColors() {
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ DefaultDismissibleModalWideNavigationRail()
+ }
+
+ assertModalExpandedNavigationRailMatches(
+ goldenIdentifier =
+ "wideNavigationRail_dismissibleModalWideNavigationRail_darkTheme_defaultColors"
+ )
+ }
+
+ @Test
+ fun wideNavigationRail_modalWideNavigationRail_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) { DefaultModalWideNavigationRail() }
+
+ assertModalExpandedNavigationRailMatches(
+ goldenIdentifier = "wideNavigationRail_modalWideNavigationRail_lightTheme_defaultColors"
+ )
+ }
+
+ @Test
+ fun wideNavigationRail_modalWideNavigationRail_darkTheme() {
+ composeTestRule.setMaterialContent(darkColorScheme()) { DefaultModalWideNavigationRail() }
+
+ assertModalExpandedNavigationRailMatches(
+ goldenIdentifier = "wideNavigationRail_modalWideNavigationRail_darkTheme_defaultColors"
+ )
+ }
+
+ /**
+ * Asserts that the ModalExpandedNavigationRail matches the screenshot with identifier
+ * [goldenIdentifier].
+ *
+ * @param goldenIdentifier the identifier for the corresponding screenshot
+ */
+ private fun assertModalExpandedNavigationRailMatches(goldenIdentifier: String) {
+ // Capture and compare screenshots.
+ composeTestRule
+ .onNode(isDialog())
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenIdentifier)
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun DefaultDismissibleModalWideNavigationRail() {
+ Box(Modifier.fillMaxSize()) {
+ DismissibleModalWideNavigationRail(
+ onDismissRequest = {},
+ ) {
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("Favorites") },
+ selected = true,
+ onClick = {},
+ )
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Home, null) },
+ label = { Text("Home") },
+ selected = false,
+ onClick = {}
+ )
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Search, null) },
+ label = { Text("Search") },
+ selected = false,
+ onClick = {}
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun DefaultModalWideNavigationRail() {
+ ModalWideNavigationRail(
+ expanded = true,
+ scrimOnClick = {},
+ expandedHeaderTopPadding = 64.dp,
+ header = {
+ Column {
+ IconButton(modifier = Modifier.padding(start = 24.dp), onClick = {}) {
+ Icon(Icons.Filled.Menu, "Menu")
+ }
+ }
+ }
+ ) {
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("Favorites") },
+ selected = true,
+ onClick = {},
+ )
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Home, null) },
+ label = { Text("Home") },
+ selected = false,
+ onClick = {}
+ )
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Search, null) },
+ label = { Text("Search") },
+ selected = false,
+ onClick = {}
+ )
+ }
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailTest.kt
new file mode 100644
index 0000000..69cf5af
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalWideNavigationRailTest.kt
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.internal.Strings
+import androidx.compose.material3.internal.getString
+import androidx.compose.material3.tokens.NavigationRailCollapsedTokens
+import androidx.compose.material3.tokens.NavigationRailExpandedTokens
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.isDisplayed
+import androidx.compose.ui.test.isNotDisplayed
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [ModalWideNavigationRail] and [DismissibleModalWideNavigationRail]. */
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ModalWideNavigationRailTest {
+
+ @get:Rule val rule = createComposeRule()
+ private val restorationTester = StateRestorationTester(rule)
+
+ /** Tests for [ModalWideNavigationRail]. */
+ @Test
+ fun modalWideRail_opens() {
+ lateinit var expanded: MutableState<Boolean>
+ rule.setMaterialContentForSizeAssertions {
+ expanded = remember { mutableStateOf(false) }
+
+ ModalWideNavigationRail(
+ expanded = expanded.value,
+ scrimOnClick = {},
+ header = {
+ Button(
+ modifier = Modifier.testTag("header"),
+ onClick = { expanded.value = !expanded.value }
+ ) {}
+ }
+ ) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // Click on header to collapse.
+ rule.onNodeWithTag("header").performClick()
+
+ // Assert rail is collapsed.
+ assertThat(expanded.value).isTrue()
+ // Assert width changed to collapse width.
+ rule
+ .onNodeWithTag("item")
+ .onParent()
+ .assertWidthIsEqualTo(NavigationRailExpandedTokens.ContainerWidthMinimum)
+ }
+
+ @Test
+ fun modalWideRail_closes() {
+ lateinit var expanded: MutableState<Boolean>
+ rule.setMaterialContentForSizeAssertions {
+ expanded = remember { mutableStateOf(true) }
+
+ ModalWideNavigationRail(
+ modifier = Modifier.testTag("rail"),
+ expanded = expanded.value,
+ scrimOnClick = {},
+ header = {
+ Button(
+ modifier = Modifier.testTag("header"),
+ onClick = { expanded.value = !expanded.value }
+ ) {}
+ }
+ ) {
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // Click on header to collapse.
+ rule.onNodeWithTag("header").performClick()
+
+ // Assert rail is collapsed.
+ assertThat(expanded.value).isFalse()
+ // Assert width changed to collapse width.
+ rule
+ .onNodeWithTag("rail")
+ .assertWidthIsEqualTo(NavigationRailCollapsedTokens.ContainerWidth)
+ }
+
+ @Test
+ fun modalWideRail_closes_byScrimClick() {
+ lateinit var closeRail: String
+ lateinit var expanded: MutableState<Boolean>
+
+ rule.setMaterialContentForSizeAssertions {
+ expanded = remember { mutableStateOf(true) }
+ closeRail = getString(Strings.CloseRail)
+
+ ModalWideNavigationRail(
+ modifier = Modifier.testTag("rail"),
+ scrimOnClick = { expanded.value = false },
+ expanded = expanded.value,
+ ) {
+ WideNavigationRailItem(
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule
+ .onNodeWithContentDescription(closeRail)
+ .assertHasClickAction()
+ .performSemanticsAction(SemanticsActions.OnClick)
+ rule.waitForIdle()
+
+ // Assert rail is collapsed.
+ assertThat(expanded.value).isFalse()
+ // Assert width changed to collapse width.
+ rule
+ .onNodeWithTag("rail")
+ .assertWidthIsEqualTo(NavigationRailCollapsedTokens.ContainerWidth)
+ }
+
+ @Test
+ fun modalWideRail_hasPaneTitle() {
+ lateinit var paneTitle: String
+
+ rule.setMaterialContentForSizeAssertions {
+ paneTitle = getString(Strings.WideNavigationRailPaneTitle)
+ ModalWideNavigationRail(expanded = true, scrimOnClick = {}) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule
+ .onNodeWithTag("item")
+ .onParent() // rail.
+ .onParent() // dialog window.
+ .onParent() // parent container that holds dialog and scrim.
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.PaneTitle, paneTitle))
+ }
+
+ /** Tests for [DismissibleModalWideNavigationRail]. */
+ @Test
+ fun dismissibleModalRail_defaultSemantics() {
+ rule.setMaterialContent(lightColorScheme()) {
+ DismissibleModalWideNavigationRail(onDismissRequest = {}) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule
+ .onNodeWithTag("item")
+ .onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.SelectableGroup))
+ }
+
+ @Test
+ fun dismissibleModalRail_closes() {
+ val railWidth = NavigationRailExpandedTokens.ContainerWidthMinimum
+ lateinit var railState: DismissibleModalWideNavigationRailState
+ lateinit var scope: CoroutineScope
+
+ rule.setMaterialContentForSizeAssertions {
+ railState = rememberDismissibleModalWideNavigationRailState()
+ scope = rememberCoroutineScope()
+
+ DismissibleModalWideNavigationRail(onDismissRequest = {}, railState = railState) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // Rail starts as open.
+ assertThat(railState.isOpen).isTrue()
+ // Close rail.
+ scope.launch { railState.close() }
+ rule.waitForIdle()
+
+ // Assert rail is not open.
+ assertThat(railState.isOpen).isFalse()
+ // Assert rail is not displayed.
+ rule.onNodeWithTag("item").onParent().isNotDisplayed()
+ // Assert rail's offset.
+ rule.onNodeWithTag("item").onParent().assertLeftPositionInRootIsEqualTo(-railWidth)
+ }
+
+ @Test
+ fun dismissibleModalRail_opens() {
+ lateinit var railState: DismissibleModalWideNavigationRailState
+ lateinit var scope: CoroutineScope
+
+ rule.setMaterialContentForSizeAssertions {
+ railState = rememberDismissibleModalWideNavigationRailState()
+ railState.initialValue = DismissibleModalWideNavigationRailValue.Closed
+ scope = rememberCoroutineScope()
+
+ DismissibleModalWideNavigationRail(onDismissRequest = {}, railState = railState) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ scope.launch { railState.close() }
+ }
+
+ scope.launch { railState.open() }
+ rule.waitForIdle()
+
+ // Assert rail is open.
+ assertThat(railState.isOpen).isTrue()
+ // Assert rail is displayed.
+ rule.onNodeWithTag("item").onParent().isDisplayed()
+ // Assert rail's offset.
+ rule.onNodeWithTag("item").onParent().assertLeftPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun dismissibleModalRail_closes_bySwiping() {
+ lateinit var railState: DismissibleModalWideNavigationRailState
+
+ rule.setMaterialContentForSizeAssertions {
+ railState = rememberDismissibleModalWideNavigationRailState()
+
+ DismissibleModalWideNavigationRail(onDismissRequest = {}, railState = railState) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
+ rule.waitForIdle()
+
+ // Assert rail is not open.
+ assertThat(railState.isOpen).isFalse()
+ // Assert rail is not displayed.
+ rule.onNodeWithTag("item").onParent().isNotDisplayed()
+ }
+
+ @Test
+ fun dismissibleModalRail_doesNotClose_bySwiping_gesturesDisabled() {
+ lateinit var railState: DismissibleModalWideNavigationRailState
+
+ rule.setMaterialContentForSizeAssertions {
+ railState = rememberDismissibleModalWideNavigationRailState()
+
+ DismissibleModalWideNavigationRail(
+ gesturesEnabled = false,
+ onDismissRequest = {},
+ railState = railState,
+ ) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
+ rule.waitForIdle()
+
+ // Assert rail is still open.
+ assertThat(railState.isOpen).isTrue()
+ // Assert rail is still displayed.
+ rule.onNodeWithTag("item").onParent().isDisplayed()
+ }
+
+ @Test
+ fun dismissibleModalRail_closes_byScrimClick() {
+ lateinit var closeRail: String
+ lateinit var railState: DismissibleModalWideNavigationRailState
+ rule.setMaterialContentForSizeAssertions {
+ closeRail = getString(Strings.CloseRail)
+ railState = rememberDismissibleModalWideNavigationRailState()
+
+ DismissibleModalWideNavigationRail(
+ gesturesEnabled = false,
+ onDismissRequest = {},
+ railState = railState,
+ ) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // The rail should be open.
+ assertThat(railState.isOpen).isTrue()
+
+ rule
+ .onNodeWithContentDescription(closeRail)
+ .assertHasClickAction()
+ .performSemanticsAction(SemanticsActions.OnClick)
+ rule.waitForIdle()
+
+ // Assert rail is not open.
+ assertThat(railState.isOpen).isFalse()
+ // Assert rail is not displayed.
+ rule.onNodeWithTag("item").onParent().isNotDisplayed()
+ }
+
+ @Test
+ fun dismissibleModalRail_hasPaneTitle() {
+ lateinit var paneTitle: String
+
+ rule.setMaterialContentForSizeAssertions {
+ paneTitle = getString(Strings.WideNavigationRailPaneTitle)
+ DismissibleModalWideNavigationRail(
+ onDismissRequest = {},
+ ) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule
+ .onNodeWithTag("item")
+ .onParent() // rail.
+ .onParent() // dialog window.
+ .onParent() // parent container that holds dialog and scrim.
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.PaneTitle, paneTitle))
+ }
+
+ @Test
+ fun modalRailState_savesAndRestores() {
+ lateinit var railState: DismissibleModalWideNavigationRailState
+
+ restorationTester.setContent {
+ railState = rememberDismissibleModalWideNavigationRailState()
+ }
+
+ assertThat(railState.currentValue).isEqualTo(DismissibleModalWideNavigationRailValue.Closed)
+ restorationTester.emulateSavedInstanceStateRestore()
+ assertThat(railState.currentValue).isEqualTo(DismissibleModalWideNavigationRailValue.Closed)
+ }
+
+ @Test
+ fun modalRailState_respectsConfirmStateChange() {
+ lateinit var railState: DismissibleModalWideNavigationRailState
+
+ restorationTester.setContent {
+ railState =
+ rememberDismissibleModalWideNavigationRailState(
+ confirmValueChange = { it != DismissibleModalWideNavigationRailValue.Closed }
+ )
+
+ DismissibleModalWideNavigationRail(onDismissRequest = {}, railState = railState) {
+ WideNavigationRailItem(
+ modifier = Modifier.testTag("item"),
+ railExpanded = true,
+ icon = { Icon(Icons.Filled.Favorite, null) },
+ label = { Text("ItemText") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(railState.currentValue)
+ .isEqualTo(DismissibleModalWideNavigationRailValue.Open)
+ }
+ rule.onNodeWithTag("item").onParent().performTouchInput { swipeLeft() }
+ rule.waitForIdle()
+
+ rule.runOnIdle {
+ assertThat(railState.currentValue)
+ .isEqualTo(DismissibleModalWideNavigationRailValue.Open)
+ }
+ // Assert rail is still open.
+ assertThat(railState.isOpen).isTrue()
+ // Assert rail is still displayed.
+ rule.onNodeWithTag("item").onParent().isDisplayed()
+ }
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 06b13ec..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -23,7 +23,8 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
@@ -33,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -52,6 +54,8 @@
private val wrap = Modifier.wrapContentSize(Alignment.Center)
private val wrapperTestTag = "splitButtonWrapper"
+ private val leadingButtonTag = "leadingButton"
+ private val trailingButtonTag = "trailingButton"
@Test
fun splitButton() {
@@ -63,7 +67,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
contentDescription = "Localized description",
)
@@ -100,7 +104,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -136,7 +140,7 @@
onTrailingButtonClick = {},
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -145,7 +149,7 @@
},
trailingContent = {
Icon(
- Icons.Outlined.KeyboardArrowDown,
+ Icons.Filled.KeyboardArrowDown,
modifier =
Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
this.rotationZ = 180f
@@ -170,7 +174,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -203,7 +207,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -236,7 +240,7 @@
checked = false,
leadingContent = {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -269,7 +273,7 @@
onClick = { /* Do Nothing */ },
) {
Icon(
- Icons.Outlined.Edit,
+ Icons.Filled.Edit,
contentDescription = "Localized description",
Modifier.size(SplitButtonDefaults.LeadingIconSize)
)
@@ -322,6 +326,82 @@
assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
}
+ @Test
+ fun splitButton_leadingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ modifier = Modifier.testTag(leadingButtonTag),
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+ }
+
+ @Test
+ fun splitButton_trailingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ modifier = Modifier.testTag(trailingButtonTag),
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+ }
+
private fun assertAgainstGolden(goldenName: String) {
rule
.onNodeWithTag(wrapperTestTag)
@@ -329,6 +409,21 @@
.assertAgainstGolden(screenshotRule, goldenName)
}
+ private fun assertPressed(tag: String, goldenName: String) {
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+ // synchronization. Instead just wait until after the ripples are finished animating.
+ Thread.sleep(300)
+
+ assertAgainstGolden(goldenName)
+ }
+
// Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
// This makes sure that the default method name and the initial Scuba image generated
// name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
index d08e5fb..622ad65 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -30,7 +30,9 @@
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsProperties.SelectableGroup
+import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -681,6 +683,39 @@
}
@Test
+ fun clockFace_12Hour_traversalIndex() {
+ val state =
+ AnalogTimePickerState(
+ TimePickerState(initialHour = 0, initialMinute = 0, is24Hour = false)
+ )
+
+ rule.setMaterialContent(lightColorScheme()) {
+ ClockFace(state, TimePickerDefaults.colors(), autoSwitchToMinute = true)
+ }
+
+ repeat(12) { number ->
+ val hour =
+ when {
+ number == 0 -> 12
+ else -> number
+ }
+
+ rule
+ .onNodeWithTimeValue(hour, TimePickerSelectionMode.Hour)
+ .assert(
+ SemanticsMatcher("Index of nodes in timepicker") {
+ it.config.getOrNull(SemanticsProperties.TraversalIndex) == number + 1f
+ }
+ )
+ .performClick()
+ rule.runOnIdle {
+ state.selection = TimePickerSelectionMode.Hour
+ assertThat(state.hour).isEqualTo(number)
+ }
+ }
+ }
+
+ @Test
fun clockFace_12Hour_initAtNoon() {
val state = TimePickerState(initialHour = 12, initialMinute = 0, is24Hour = false)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
index d6dcd91..a699d92 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -448,7 +449,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -466,7 +467,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}, enabled = false) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -484,7 +485,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
@@ -539,7 +540,7 @@
Box(Modifier.testTag(wrapperTestTag)) {
ToggleButton(checked = false, onCheckedChange = {}) {
Icon(
- Icons.Filled.Favorite,
+ Icons.Outlined.Favorite,
contentDescription = "Localized description",
modifier = Modifier.size(ToggleButtonDefaults.IconSize)
)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
index 0cfc573..71e8228 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -16,6 +16,14 @@
package androidx.compose.material3
+import android.os.Build
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.View
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
@@ -29,6 +37,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
@@ -52,6 +61,7 @@
import androidx.compose.ui.window.PopupPositionProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
@@ -420,6 +430,107 @@
assertThat(state.isVisible).isFalse()
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun plainTooltip_longPress_deepPress_showsTooltip() {
+ lateinit var view: View
+ lateinit var state: TooltipState
+ var changedToVisible = false
+ rule.mainClock.autoAdvance = false
+ rule.setMaterialContent(lightColorScheme()) {
+ view = LocalView.current
+ state = rememberTooltipState()
+ LaunchedEffect(true) {
+ snapshotFlow { state.isVisible }
+ .collectLatest {
+ if (it) {
+ changedToVisible = true
+ }
+ }
+ }
+ Box(Modifier.testTag("tooltip")) {
+ PlainTooltipTest(tooltipContent = { Text(text = "Test") }, tooltipState = state)
+ }
+ }
+
+ assertThat(changedToVisible).isFalse()
+
+ val pointerProperties =
+ arrayOf(
+ MotionEvent.PointerProperties().also {
+ it.id = 0
+ it.toolType = MotionEvent.TOOL_TYPE_FINGER
+ }
+ )
+
+ val downEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 0,
+ /* action = */ ACTION_DOWN,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 5f
+ y = 5f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_NONE
+ )
+
+ view.dispatchTouchEvent(downEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ rule.runOnIdle {
+ assertThat(changedToVisible).isFalse()
+ assertThat(state.isVisible).isFalse()
+ }
+
+ val deepPressMoveEvent =
+ MotionEvent.obtain(
+ /* downTime = */ 0,
+ /* eventTime = */ 50,
+ /* action = */ ACTION_MOVE,
+ /* pointerCount = */ 1,
+ /* pointerProperties = */ pointerProperties,
+ /* pointerCoords = */ arrayOf(
+ MotionEvent.PointerCoords().apply {
+ x = 10f
+ y = 10f
+ }
+ ),
+ /* metaState = */ 0,
+ /* buttonState = */ 0,
+ /* xPrecision = */ 0f,
+ /* yPrecision = */ 0f,
+ /* deviceId = */ 0,
+ /* edgeFlags = */ 0,
+ /* source = */ InputDevice.SOURCE_TOUCHSCREEN,
+ /* displayId = */ 0,
+ /* flags = */ 0,
+ /* classification = */ CLASSIFICATION_DEEP_PRESS
+ )
+
+ view.dispatchTouchEvent(deepPressMoveEvent)
+ rule.mainClock.advanceTimeBy(50)
+
+ // Even though the timeout didn't pass, the deep press should immediately show the tooltip
+ rule.runOnIdle {
+ assertThat(changedToVisible).isTrue()
+ assertThat(state.isVisible).isTrue()
+ }
+ }
+
@Test
fun plainTooltip_longPress_keepsTooltipVisible() {
lateinit var state: TooltipState
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
index 5852853..90f46c5 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
@@ -79,6 +79,18 @@
}
@Test
+ fun linearWavyProgressIndicator_indeterminate_lowerAmplitude() {
+ rule.mainClock.autoAdvance = false
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) { LinearWavyProgressIndicator(amplitude = 0.5f) }
+ }
+ rule.mainClock.advanceTimeBy(1200)
+ assertIndicatorAgainstGolden(
+ "linearWavyProgressIndicator_indeterminate_lowerAmplitude_${scheme.name}"
+ )
+ }
+
+ @Test
fun linearWavyProgressIndicator_midProgress_determinate() {
rule.setMaterialContent(scheme.colorScheme) {
Box(wrap.testTag(wrapperTestTag)) { LinearWavyProgressIndicator(progress = { 0.5f }) }
@@ -253,6 +265,18 @@
}
@Test
+ fun circularWavyProgressIndicator_indeterminate_lowerAmplitude() {
+ rule.mainClock.autoAdvance = false
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) { CircularWavyProgressIndicator(amplitude = 0.5f) }
+ }
+ rule.mainClock.advanceTimeBy(500)
+ assertIndicatorAgainstGolden(
+ "circularWavyProgressIndicator_indeterminate_lowerAmplitude_${scheme.name}"
+ )
+ }
+
+ @Test
fun circularWavyProgressIndicator_determinate_customCapAndTrack() {
rule.setMaterialContent(scheme.colorScheme) {
Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
index 73586b6..b4fa43b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
@@ -208,7 +208,7 @@
}
}
- rule.mainClock.advanceTimeBy(200)
+ rule.mainClock.advanceTimeBy(300)
rule.onNodeWithTag(tag).captureToImage().toPixelMap().let {
assertEquals(expectedSize.width, it.width)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..72c2356 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
@@ -39,16 +40,22 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.withFrameNanos
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.StateRestorationTester
@@ -59,11 +66,13 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertEquals
import kotlin.math.roundToInt
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -697,7 +706,7 @@
initialValue = A,
defaultPositionalThreshold,
defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
+ animationSpec = defaultAnimationSpec
)
anchoredDraggableState.updateAnchors(
DraggableAnchors {
@@ -735,40 +744,6 @@
dragJob.cancel()
}
- @Test
- fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
- val anchoredDraggableState =
- AnchoredDraggableState(
- initialValue = B,
- defaultPositionalThreshold,
- defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
- confirmValueChange = { false }
- )
- anchoredDraggableState.updateAnchors(
- DraggableAnchors {
- A at 0f
- B at 200f
- }
- )
-
- assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
- val unexpectedTarget = A
- val targetUpdates = Channel<Float>()
- val dragJob =
- launch(Dispatchers.Unconfined) {
- anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
- targetUpdates.send(anchors.positionOf(latestTarget))
- suspendIndefinitely()
- }
- }
-
- val firstTarget = targetUpdates.receive()
- assertThat(firstTarget).isEqualTo(200f)
- dragJob.cancel()
- }
-
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
@@ -1019,6 +994,64 @@
)
}
+ @Test
+ fun draggableAnchors_draggableOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var coords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state =
+ AnchoredDraggableState(
+ initialValue = 0,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold,
+ animationSpec = { spring() }
+ )
+ var value by mutableIntStateOf(0)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(value) { state.snapTo(value) }
+ Box(
+ Modifier.draggableAnchors(state, Orientation.Vertical) { _, _ ->
+ DraggableAnchors { repeat(5) { it at it * 100f } } to 0
+ }
+ .fillMaxSize()
+ ) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coords = it })
+ }
+ }
+ }
+
+ repeat(5) {
+ value = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> {}
private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
index 94554e1..f853256 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/WideNavigationRail.android.kt
@@ -75,7 +75,7 @@
// Logic forked from androidx.compose.ui.window.DialogProperties. Removed dismissOnClickOutside
// and usePlatformDefaultWidth as they are not relevant for fullscreen experience.
/**
- * Properties used to customize the behavior of a [ModalExpandedNavigationRail].
+ * Properties used to customize the behavior of a [DismissibleModalWideNavigationRail].
*
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the modal
* navigation rail's window.
@@ -84,7 +84,7 @@
*/
@Immutable
@ExperimentalMaterial3ExpressiveApi
-actual class ModalExpandedNavigationRailProperties(
+actual class ModalWideNavigationRailProperties(
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@get:Suppress("GetterSetterNames") actual val shouldDismissOnBackPress: Boolean = true,
) {
@@ -97,7 +97,7 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is ModalExpandedNavigationRailProperties) return false
+ if (other !is ModalWideNavigationRailProperties) return false
if (securePolicy != other.securePolicy) return false
return true
@@ -112,10 +112,10 @@
@Immutable
@ExperimentalMaterial3ExpressiveApi
-actual object ModalExpandedNavigationRailDefaults {
+actual object DismissibleModalWideNavigationRailDefaults {
- /** Properties used to customize the behavior of a [ModalExpandedNavigationRail]. */
- actual val Properties = ModalExpandedNavigationRailProperties()
+ /** Properties used to customize the behavior of a [DismissibleModalWideNavigationRail]. */
+ actual val Properties = ModalWideNavigationRailProperties()
}
// Fork of androidx.compose.ui.window.AndroidDialog_androidKt.Dialog
@@ -124,7 +124,7 @@
@Composable
internal actual fun ModalWideNavigationRailDialog(
onDismissRequest: () -> Unit,
- properties: ModalExpandedNavigationRailProperties,
+ properties: ModalWideNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
predictiveBackState: RailPredictiveBackState,
@@ -330,7 +330,7 @@
@ExperimentalMaterial3ExpressiveApi
private class ModalWideNavigationRailDialogWrapper(
private var onDismissRequest: () -> Unit,
- private var properties: ModalExpandedNavigationRailProperties,
+ private var properties: ModalWideNavigationRailProperties,
private val composeView: View,
layoutDirection: LayoutDirection,
density: Density,
@@ -456,7 +456,7 @@
fun updateParameters(
onDismissRequest: () -> Unit,
- properties: ModalExpandedNavigationRailProperties,
+ properties: ModalWideNavigationRailProperties,
layoutDirection: LayoutDirection
) {
this.onDismissRequest = onDismissRequest
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
index cbfaeb3..4040b26 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
@@ -18,6 +18,7 @@
package androidx.compose.material3.internal
+import android.view.MotionEvent
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.R
import androidx.compose.foundation.gestures.awaitEachGesture
@@ -30,10 +31,13 @@
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
@@ -41,6 +45,8 @@
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.util.fastAll
+import androidx.compose.ui.util.fastAny
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
@@ -164,20 +170,13 @@
// Long press will finish before or after show so keep track of it, in a
// flow to handle both cases
val isLongPressedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
- val longPressTimeout = viewConfiguration.longPressTimeoutMillis
val pass = PointerEventPass.Initial
// wait for the first down press
val inputType = awaitFirstDown(pass = pass).type
if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
- try {
- // listen to if there is up gesture
- // within the longPressTimeout limit
- withTimeout(longPressTimeout) {
- waitForUpOrCancellation(pass = pass)
- }
- } catch (_: PointerEventTimeoutCancellationException) {
+ if (waitForLongPress(pass = pass)) {
// handle long press - Show the tooltip
launch(start = CoroutineStart.UNDISPATCHED) {
try {
@@ -196,9 +195,8 @@
// Long press may still be in progress
val upEvent = waitForUpOrCancellation(pass = pass)
upEvent?.consume()
- } finally {
- isLongPressedFlow.tryEmit(false)
}
+ isLongPressedFlow.tryEmit(false)
}
}
}
@@ -244,3 +242,45 @@
)
}
} else this
+
+// TODO: b/305997392 move to use foundation API for tooltip gestures and remove this
+/** @return true if long press occurred, false otherwise */
+private suspend fun AwaitPointerEventScope.waitForLongPress(
+ pass: PointerEventPass = PointerEventPass.Main
+): Boolean {
+ var result = false
+ try {
+ withTimeout(viewConfiguration.longPressTimeoutMillis) {
+ while (true) {
+ val event = awaitPointerEvent(pass)
+ if (event.changes.fastAll { it.changedToUp() }) {
+ // All pointers are up
+ break
+ }
+
+ if (event.classification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
+ result = true
+ break
+ }
+
+ if (
+ event.changes.fastAny {
+ it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)
+ }
+ ) {
+ break
+ }
+
+ // Check for cancel by position consumption. We can look on the Final pass of the
+ // existing pointer event because it comes after the pass we checked above.
+ val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+ if (consumeCheck.changes.fastAny { it.isConsumed }) {
+ break
+ }
+ }
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ return true
+ }
+ return result
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
index 6d6a123..091e453 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
@@ -430,7 +430,7 @@
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
}
val arrangement = Arrangement.End
- val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
+ val mainAxisPositions = IntArray(childrenMainAxisSizes.size)
with(arrangement) {
arrange(
mainAxisLayoutSize,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
index 29c9337..8ef2938 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
@@ -308,10 +308,7 @@
// min anchor. This is done to avoid showing a gap when the sheet opens and bounces
// when it's applied with a bouncy motion. Note that the content inside the Surface
// is scaled back down to maintain its aspect ratio (see below).
- .verticalScaleUp(
- { state.anchoredDraggableState.offset },
- { state.anchoredDraggableState.anchors.minAnchor() }
- ),
+ .verticalScaleUp(state),
shape = shape,
color = containerColor,
contentColor = contentColor,
@@ -323,10 +320,7 @@
// Scale the content down in case the sheet offset overflows below the min anchor.
// The wrapping Surface is scaled up, so this is done to maintain the content's
// aspect ratio.
- .verticalScaleDown(
- { state.anchoredDraggableState.offset },
- { state.anchoredDraggableState.anchors.minAnchor() }
- )
+ .verticalScaleDown(state)
) {
if (dragHandle != null) {
val partialExpandActionLabel =
@@ -443,42 +437,40 @@
}
/**
- * A [Modifier] that scales up the drawing layer on the Y axis in case the [sheetOffset] overflows
- * below the min anchor coordinates. The scaling will ensure that there is no visible gap between
- * the sheet and the edge of the screen in case the sheet bounces when it opens due to a more
- * expressive motion setting.
+ * A [Modifier] that scales up the drawing layer on the Y axis in case the [SheetState]'s
+ * anchoredDraggableState offset overflows below the min anchor coordinates. The scaling will ensure
+ * that there is no visible gap between the sheet and the edge of the screen in case the sheet
+ * bounces when it opens due to a more expressive motion setting.
*
* A [verticalScaleDown] should be applied to the content of the sheet to maintain the content
* aspect ratio as the container scales up.
*
- * @param sheetOffset a lambda that provides the current sheet's offset
- * @param minAnchor a lambda that provides the sheet's min anchor coordinate
+ * @param state a [SheetState]
* @see verticalScaleDown
*/
-internal fun Modifier.verticalScaleUp(sheetOffset: () -> Float, minAnchor: () -> Float) =
- graphicsLayer {
- val offset = sheetOffset()
- val anchor = minAnchor()
- val overflow = if (offset < anchor) anchor - offset else 0f
- scaleY = if (overflow > 0f) (size.height + overflow) / size.height else 1f
- transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
- }
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun Modifier.verticalScaleUp(state: SheetState) = graphicsLayer {
+ val offset = state.anchoredDraggableState.offset
+ val anchor = state.anchoredDraggableState.anchors.minAnchor()
+ val overflow = if (offset < anchor) anchor - offset else 0f
+ scaleY = if (overflow > 0f) (size.height + overflow) / size.height else 1f
+ transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
+}
/**
- * A [Modifier] that scales down the drawing layer on the Y axis in case the [sheetOffset] overflows
- * below the min anchor coordinates. This modifier should be applied to the content inside a
- * component that was scaled up with a [verticalScaleUp] modifier. It will ensure that the content
- * maintains its aspect ratio as the container scales up.
+ * A [Modifier] that scales down the drawing layer on the Y axis in case the [SheetState]'s
+ * anchoredDraggableState offset overflows below the min anchor coordinates. This modifier should be
+ * applied to the content inside a component that was scaled up with a [verticalScaleUp] modifier.
+ * It will ensure that the content maintains its aspect ratio as the container scales up.
*
- * @param sheetOffset a lambda that provides the current sheet's offset
- * @param minAnchor a lambda that provides the sheet's min anchor coordinate
+ * @param state a [SheetState]
* @see verticalScaleUp
*/
-internal fun Modifier.verticalScaleDown(sheetOffset: () -> Float, minAnchor: () -> Float) =
- graphicsLayer {
- val offset = sheetOffset()
- val anchor = minAnchor()
- val overflow = if (offset < anchor) anchor - offset else 0f
- scaleY = if (overflow > 0f) 1 / ((size.height + overflow) / size.height) else 1f
- transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
- }
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun Modifier.verticalScaleDown(state: SheetState) = graphicsLayer {
+ val offset = state.anchoredDraggableState.offset
+ val anchor = state.anchoredDraggableState.anchors.minAnchor()
+ val overflow = if (offset < anchor) anchor - offset else 0f
+ scaleY = if (overflow > 0f) 1 / ((size.height + overflow) / size.height) else 1f
+ transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
index dcf62a2..ace32d4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ButtonGroup.kt
@@ -391,7 +391,7 @@
// Compute the row size and position the children.
val mainAxisLayoutSize = max((fixedSpace + weightedSpace).coerceAtLeast(0), mainAxisMin)
- val mainAxisPositions = IntArray(size) { 0 }
+ val mainAxisPositions = IntArray(size)
val measureScope = this
with(horizontalArrangement) {
measureScope.arrange(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingAppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingAppBar.kt
index 794b6b6..b94b3bc 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingAppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingAppBar.kt
@@ -28,9 +28,9 @@
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -44,10 +44,10 @@
import androidx.compose.material3.FloatingAppBarDefaults.horizontalExitTransition
import androidx.compose.material3.FloatingAppBarDefaults.verticalEnterTransition
import androidx.compose.material3.FloatingAppBarDefaults.verticalExitTransition
-import androidx.compose.material3.FloatingAppBarPosition.Companion.Bottom
-import androidx.compose.material3.FloatingAppBarPosition.Companion.End
-import androidx.compose.material3.FloatingAppBarPosition.Companion.Start
-import androidx.compose.material3.FloatingAppBarPosition.Companion.Top
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.Bottom
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.End
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.Start
+import androidx.compose.material3.FloatingAppBarExitDirection.Companion.Top
import androidx.compose.material3.tokens.ColorSchemeKeyTokens
import androidx.compose.material3.tokens.ElevationTokens
import androidx.compose.material3.tokens.MotionSchemeKeyTokens
@@ -69,8 +69,11 @@
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
@@ -86,7 +89,8 @@
* @param containerColor the color used for the background of this FloatingAppBar. Use
* [Color.Transparent] to have no color.
* @param contentPadding the padding applied to the content of this FloatingAppBar.
- * @param scrollBehavior a [FloatingAppBarScrollBehavior].
+ * @param scrollBehavior a [FloatingAppBarScrollBehavior]. If null, this FloatingAppBar will not
+ * automatically react to scrolling.
* @param shape the shape used for this FloatingAppBar.
* @param leadingContent the leading content of this FloatingAppBar. The default layout here is a
* [Row], so content inside will be placed horizontally. Only showing if [expanded] is true.
@@ -155,7 +159,8 @@
* @param containerColor the color used for the background of this FloatingAppBar. Use
* Color.Transparent] to have no color.
* @param contentPadding the padding applied to the content of this FloatingAppBar.
- * @param scrollBehavior a [FloatingAppBarScrollBehavior].
+ * @param scrollBehavior a [FloatingAppBarScrollBehavior]. If null, this FloatingAppBar will not
+ * automatically react to scrolling.
* @param shape the shape used for this FloatingAppBar.
* @param leadingContent the leading content of this FloatingAppBar. The default layout here is a
* [Column], so content inside will be placed vertically. Only showing if [expanded] is true.
@@ -222,11 +227,8 @@
@Stable
sealed interface FloatingAppBarScrollBehavior : NestedScrollConnection {
- /** Indicates the position relative to the screen. */
- val position: FloatingAppBarPosition
-
- /** The offset from the edge of the screen. */
- val screenOffset: Dp
+ /** Indicates the direction towards which the floating app bar exits the screen. */
+ val exitDirection: FloatingAppBarExitDirection
/**
* A [FloatingAppBarState] that is attached to this behavior and is read and updated when
@@ -247,7 +249,7 @@
val flingAnimationSpec: DecayAnimationSpec<Float>
/** A [Modifier] that is attached to this behavior. */
- @Composable fun Modifier.floatingScrollBehavior(): Modifier
+ fun Modifier.floatingScrollBehavior(): Modifier
}
/**
@@ -258,8 +260,7 @@
* collapse when the nested content is pulled up, and will immediately appear when the content is
* pulled down.
*
- * @param position indicates the position relative to the screen
- * @param screenOffset offset from the edge of the screen
+ * @param exitDirection indicates the direction towards which the floating app bar exits the screen
* @param state a [FloatingAppBarState]
* @param snapAnimationSpec an [AnimationSpec] that defines how the floating app bar snaps to either
* fully collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
@@ -269,8 +270,7 @@
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private class ExitAlwaysFloatingAppBarScrollBehavior(
- override val position: FloatingAppBarPosition,
- override val screenOffset: Dp,
+ override val exitDirection: FloatingAppBarExitDirection,
override val state: FloatingAppBarState,
override val snapAnimationSpec: AnimationSpec<Float>,
override val flingAnimationSpec: DecayAnimationSpec<Float>,
@@ -299,31 +299,34 @@
settleFloatingAppBar(state, available.y, snapAnimationSpec, flingAnimationSpec)
}
- @Composable
override fun Modifier.floatingScrollBehavior(): Modifier {
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ var isRtl = false
val orientation =
- when (position) {
+ when (exitDirection) {
Start,
End -> Orientation.Horizontal
else -> Orientation.Vertical
}
+ val draggableState = DraggableState { delta ->
+ val offset = if (exitDirection in listOf(Start, End) && isRtl) -delta else delta
+ when (exitDirection) {
+ Start,
+ Top -> state.offset += offset
+ End,
+ Bottom -> state.offset -= offset
+ }
+ }
return this.layout { measurable, constraints ->
+ isRtl = layoutDirection == LayoutDirection.Rtl
+
// Sets the app bar's offset to collapse the entire bar's when content scrolled.
val placeable = measurable.measure(constraints)
- val limit =
- when (position) {
- Start,
- End -> placeable.width + screenOffset.toPx()
- else -> placeable.height + screenOffset.toPx()
- }
- state.offsetLimit = -limit
-
val offset =
- if (position in listOf(Start, End) && isRtl) -state.offset else state.offset
+ if (exitDirection in listOf(Start, End) && isRtl) -state.offset
+ else state.offset
layout(placeable.width, placeable.height) {
- when (position) {
+ when (exitDirection) {
Start -> placeable.placeWithLayer(offset.roundToInt(), 0)
End -> placeable.placeWithLayer(-offset.roundToInt(), 0)
Top -> placeable.placeWithLayer(0, offset.roundToInt())
@@ -333,20 +336,28 @@
}
.draggable(
orientation = orientation,
- state =
- rememberDraggableState { delta ->
- val offset = if (position in listOf(Start, End) && isRtl) -delta else delta
- when (position) {
- Start,
- Top -> state.offset += offset
- End,
- Bottom -> state.offset -= offset
- }
- },
+ state = draggableState,
onDragStopped = { velocity ->
settleFloatingAppBar(state, velocity, snapAnimationSpec, flingAnimationSpec)
}
)
+ .onGloballyPositioned { coordinates ->
+ // Updates the app bar's offsetLimit relative to the parent.
+ val parentOffset = coordinates.positionInParent()
+ val parentSize = coordinates.parentLayoutCoordinates?.size ?: IntSize.Zero
+ val width = coordinates.size.width
+ val height = coordinates.size.height
+ val limit =
+ when (exitDirection) {
+ Start ->
+ if (isRtl) parentSize.width - parentOffset.x else width + parentOffset.x
+ End ->
+ if (isRtl) width + parentOffset.x else parentSize.width - parentOffset.x
+ Top -> height + parentOffset.y
+ else -> parentSize.height - parentOffset.y
+ }
+ state.offsetLimit = -(limit - state.offset)
+ }
}
}
@@ -382,15 +393,16 @@
val ScreenOffset = 16.dp
// TODO: note that this scroll behavior may impact assistive technologies making the component
- // inaccessible. See @sample androidx.compose.material3.samples.HorizontalFloatingAppBar on how
+ // inaccessible.
+ // See @sample androidx.compose.material3.samples.ScrollableHorizontalFloatingAppBar on how
// to disable scrolling when touch exploration is enabled.
/**
* Returns a [FloatingAppBarScrollBehavior]. A floating app bar that is set up with this
* [FloatingAppBarScrollBehavior] will immediately collapse when the content is pulled up, and
* will immediately appear when the content is pulled down.
*
- * @param position indicates the position relative to the screen
- * @param screenOffset offset from the edge of the screen
+ * @param exitDirection indicates the direction towards which the floating app bar exits the
+ * screen
* @param state the state object to be used to control or observe the floating app bar's scroll
* state. See [rememberFloatingAppBarState] for a state that is remembered across
* compositions.
@@ -404,16 +416,14 @@
@ExperimentalMaterial3ExpressiveApi
@Composable
fun exitAlwaysScrollBehavior(
- position: FloatingAppBarPosition,
- screenOffset: Dp = ScreenOffset,
+ exitDirection: FloatingAppBarExitDirection,
state: FloatingAppBarState = rememberFloatingAppBarState(),
snapAnimationSpec: AnimationSpec<Float> = MotionSchemeKeyTokens.DefaultEffects.value(),
flingAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
): FloatingAppBarScrollBehavior =
- remember(position, screenOffset, state, snapAnimationSpec, flingAnimationSpec) {
+ remember(exitDirection, state, snapAnimationSpec, flingAnimationSpec) {
ExitAlwaysFloatingAppBarScrollBehavior(
- position = position,
- screenOffset = screenOffset,
+ exitDirection = exitDirection,
state = state,
snapAnimationSpec = snapAnimationSpec,
flingAnimationSpec = flingAnimationSpec
@@ -629,33 +639,33 @@
}
/**
- * The possible positions for a [HorizontalFloatingAppBar] or [VerticalFloatingAppBar], used to
- * determine the direction when a [FloatingAppBarScrollBehavior] is attached.
+ * The possible directions for a [HorizontalFloatingAppBar] or [VerticalFloatingAppBar], used to
+ * determine the exit direction when a [FloatingAppBarScrollBehavior] is attached.
*/
@ExperimentalMaterial3ExpressiveApi
@kotlin.jvm.JvmInline
-value class FloatingAppBarPosition
+value class FloatingAppBarExitDirection
internal constructor(@Suppress("unused") private val value: Int) {
companion object {
- /** Position FloatingAppBar at the bottom of the screen */
- val Bottom = FloatingAppBarPosition(0)
+ /** FloatingAppBar exits towards the bottom of the screen */
+ val Bottom = FloatingAppBarExitDirection(0)
- /** Position FloatingAppBar at the top of the screen */
- val Top = FloatingAppBarPosition(1)
+ /** FloatingAppBar exits towards the top of the screen */
+ val Top = FloatingAppBarExitDirection(1)
- /** Position FloatingAppBar at the start of the screen */
- val Start = FloatingAppBarPosition(2)
+ /** FloatingAppBar exits towards the start of the screen */
+ val Start = FloatingAppBarExitDirection(2)
- /** Position FloatingAppBar at the end of the screen */
- val End = FloatingAppBarPosition(3)
+ /** FloatingAppBar exits towards the end of the screen */
+ val End = FloatingAppBarExitDirection(3)
}
override fun toString(): String {
return when (this) {
- Bottom -> "FloatingAppBarPosition.Bottom"
- Top -> "FloatingAppBarPosition.Top"
- Start -> "FloatingAppBarPosition.Start"
- else -> "FloatingAppBarPosition.End"
+ Bottom -> "FloatingAppBarExitDirection.Bottom"
+ Top -> "FloatingAppBarExitDirection.Top"
+ Start -> "FloatingAppBarExitDirection.Start"
+ else -> "FloatingAppBarExitDirection.End"
}
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
index 8e937b3..4ed2648 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/LoadingIndicator.kt
@@ -94,7 +94,7 @@
fun LoadingIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
- color: Color = LoadingIndicatorDefaults.IndicatorColor,
+ color: Color = LoadingIndicatorDefaults.indicatorColor,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.DeterminateIndicatorPolygons
) =
LoadingIndicatorImpl(
@@ -102,7 +102,7 @@
modifier = modifier,
containerColor = Color.Unspecified,
indicatorColor = color,
- containerShape = LoadingIndicatorDefaults.ContainerShape,
+ containerShape = LoadingIndicatorDefaults.containerShape,
indicatorPolygons = polygons,
)
@@ -129,14 +129,14 @@
@Composable
fun LoadingIndicator(
modifier: Modifier = Modifier,
- color: Color = LoadingIndicatorDefaults.IndicatorColor,
+ color: Color = LoadingIndicatorDefaults.indicatorColor,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons,
) =
LoadingIndicatorImpl(
modifier = modifier,
containerColor = Color.Unspecified,
indicatorColor = color,
- containerShape = LoadingIndicatorDefaults.ContainerShape,
+ containerShape = LoadingIndicatorDefaults.containerShape,
indicatorPolygons = polygons,
)
@@ -175,9 +175,9 @@
fun ContainedLoadingIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
- containerColor: Color = LoadingIndicatorDefaults.ContainedContainerColor,
- indicatorColor: Color = LoadingIndicatorDefaults.ContainedIndicatorColor,
- containerShape: Shape = LoadingIndicatorDefaults.ContainerShape,
+ containerColor: Color = LoadingIndicatorDefaults.containedContainerColor,
+ indicatorColor: Color = LoadingIndicatorDefaults.containedIndicatorColor,
+ containerShape: Shape = LoadingIndicatorDefaults.containerShape,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.DeterminateIndicatorPolygons
) =
LoadingIndicatorImpl(
@@ -215,9 +215,9 @@
@Composable
fun ContainedLoadingIndicator(
modifier: Modifier = Modifier,
- containerColor: Color = LoadingIndicatorDefaults.ContainedContainerColor,
- indicatorColor: Color = LoadingIndicatorDefaults.ContainedIndicatorColor,
- containerShape: Shape = LoadingIndicatorDefaults.ContainerShape,
+ containerColor: Color = LoadingIndicatorDefaults.containedContainerColor,
+ indicatorColor: Color = LoadingIndicatorDefaults.containedIndicatorColor,
+ containerShape: Shape = LoadingIndicatorDefaults.containerShape,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons,
) =
LoadingIndicatorImpl(
@@ -473,25 +473,25 @@
val IndicatorSize = LoadingIndicatorTokens.ActiveSize
/** A [LoadingIndicator] default container [Shape]. */
- val ContainerShape: Shape
+ val containerShape: Shape
@Composable get() = LoadingIndicatorTokens.ContainerShape.value
/**
* A [LoadingIndicator] default active indicator [Color] when using an uncontained
* [LoadingIndicator].
*/
- val IndicatorColor: Color
+ val indicatorColor: Color
@Composable get() = LoadingIndicatorTokens.ActiveIndicatorColor.value
/**
* A [LoadingIndicator] default active indicator [Color] when using a
* [ContainedLoadingIndicator].
*/
- val ContainedIndicatorColor: Color
+ val containedIndicatorColor: Color
@Composable get() = LoadingIndicatorTokens.ContainedActiveColor.value
/** A [LoadingIndicator] default container [Color] when using a [ContainedLoadingIndicator]. */
- val ContainedContainerColor: Color
+ val containedContainerColor: Color
@Composable get() = LoadingIndicatorTokens.ContainedContainerColor.value
/**
@@ -594,28 +594,6 @@
}
/**
- * Calculates a scale factor that will be used when scaling the provided [Morph] into a specified
- * sized container.
- *
- * Since the morph may rotate, a simple [Morph.calculateBounds] is not enough to determine the size
- * the morph will occupy as it rotates. Using the simple bounds calculation may result in a clipped
- * shape.
- *
- * This function calculates and returns a scale factor by utilizing the [Morph.calculateMaxBounds]
- * and comparing its result to the [Morph.calculateBounds]. The scale factor can later be used when
- * calling [processPath].
- */
-private fun calculateScaleFactor(morph: Morph): Float {
- val bounds = morph.calculateBounds()
- val maxBounds = morph.calculateMaxBounds()
- val scaleX = bounds.width() / maxBounds.width()
- val scaleY = bounds.height() / maxBounds.height()
- // We use max(scaleX, scaleY) to handle cases like a pill-shape that can throw off the
- // entire calculation.
- return max(scaleX, scaleY)
-}
-
-/**
* Returns the width value from the [FloatArray] that was calculated by a
* [RoundedPolygon.calculateBounds] or [[RoundedPolygon.calculateMaxBounds]].
*/
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
index b0ac90c..24ab941 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialShapes.kt
@@ -29,16 +29,11 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastFlatMap
-import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastMaxBy
import androidx.graphics.shapes.CornerRounding
import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
-import androidx.graphics.shapes.TransformResult
import androidx.graphics.shapes.circle
-import androidx.graphics.shapes.pill
import androidx.graphics.shapes.rectangle
import androidx.graphics.shapes.star
import kotlin.math.PI
@@ -126,20 +121,15 @@
companion object {
// Cache various roundings for use below
- private val cornerRound10 = CornerRounding(radius = .1f)
private val cornerRound15 = CornerRounding(radius = .15f)
private val cornerRound20 = CornerRounding(radius = .2f)
private val cornerRound30 = CornerRounding(radius = .3f)
- private val cornerRound40 = CornerRounding(radius = .4f)
private val cornerRound50 = CornerRounding(radius = .5f)
private val cornerRound100 = CornerRounding(radius = 1f)
private val rotateNeg45 = Matrix().apply { rotateZ(-45f) }
- private val rotate45 = Matrix().apply { rotateZ(45f) }
private val rotateNeg90 = Matrix().apply { rotateZ(-90f) }
- private val rotate90 = Matrix().apply { rotateZ(90f) }
private val rotateNeg135 = Matrix().apply { rotateZ(-135f) }
- private val unrounded = CornerRounding.Unrounded
private var _circle: RoundedPolygon? = null
private var _square: RoundedPolygon? = null
@@ -326,14 +316,13 @@
}
internal fun slanted(): RoundedPolygon {
- return RoundedPolygon(
- numVertices = 4,
- rounding = CornerRounding(radius = 0.3f, smoothing = 0.5f)
- )
- .transformed(rotateNeg45)
- .transformed { x, y ->
- TransformResult(x - 0.1f * y, y) // Compose's matrix doesn't support skew!?
- }
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.926f, 0.970f), CornerRounding(0.189f, 0.811f)),
+ PointNRound(Offset(-0.021f, 0.967f), CornerRounding(0.187f, 0.057f))
+ ),
+ 2
+ )
}
internal fun arch(): RoundedPolygon {
@@ -346,35 +335,27 @@
}
internal fun fan(): RoundedPolygon {
- return RoundedPolygon(
- numVertices = 4,
- perVertexRounding =
- listOf(cornerRound100, cornerRound20, cornerRound20, cornerRound20)
- )
- .transformed(rotateNeg45)
- }
-
- internal fun arrow(): RoundedPolygon {
- return triangleChip(
- innerRadius = .3375f,
- rounding = CornerRounding(radius = .25f, smoothing = .48f)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(1.004f, 1.000f), CornerRounding(0.148f, 0.417f)),
+ PointNRound(Offset(0.000f, 1.000f), CornerRounding(0.151f)),
+ PointNRound(Offset(0.000f, -0.003f), CornerRounding(0.148f)),
+ PointNRound(Offset(0.978f, 0.020f), CornerRounding(0.803f))
+ ),
+ 1
)
}
- internal fun triangleChip(innerRadius: Float, rounding: CornerRounding): RoundedPolygon {
- val topR = 0.888f
- val points =
- floatArrayOf(
- radialToCartesian(radius = topR, 270f.toRadians()).x,
- radialToCartesian(radius = topR, 270f.toRadians()).y,
- radialToCartesian(radius = 1f, 30f.toRadians()).x,
- radialToCartesian(radius = 1f, 30f.toRadians()).y,
- radialToCartesian(radius = innerRadius, 90f.toRadians()).x,
- radialToCartesian(radius = innerRadius, 90f.toRadians()).y,
- radialToCartesian(radius = 1f, 150f.toRadians()).x,
- radialToCartesian(radius = 1f, 150f.toRadians()).y
- )
- return RoundedPolygon(points, rounding)
+ internal fun arrow(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.892f), CornerRounding(0.313f)),
+ PointNRound(Offset(-0.216f, 1.050f), CornerRounding(0.207f)),
+ PointNRound(Offset(0.499f, -0.160f), CornerRounding(0.215f, 1.000f)),
+ PointNRound(Offset(1.225f, 1.060f), CornerRounding(0.211f))
+ ),
+ 1
+ )
}
internal fun semiCircle(): RoundedPolygon {
@@ -386,13 +367,21 @@
)
}
- internal fun oval(scaleX: Float = 1f, scaleY: Float = .7f): RoundedPolygon {
- val m = Matrix().apply { scale(x = scaleX, y = scaleY) }
+ internal fun oval(): RoundedPolygon {
+ val m = Matrix().apply { scale(1f, 0.64f) }
return RoundedPolygon.circle().transformed(m).transformed(rotateNeg45)
}
- internal fun pill(width: Float = 1.25f, height: Float = 1f): RoundedPolygon {
- return RoundedPolygon.pill(width = width, height = height).transformed(rotateNeg45)
+ internal fun pill(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.961f, 0.039f), CornerRounding(0.426f)),
+ PointNRound(Offset(1.001f, 0.428f)),
+ PointNRound(Offset(1.000f, 0.609f), CornerRounding(1.000f))
+ ),
+ reps = 2,
+ mirroring = true
+ )
}
internal fun triangle(): RoundedPolygon {
@@ -400,77 +389,50 @@
.transformed(rotateNeg90)
}
- internal fun diamond(scaleX: Float = 1f, scaleY: Float = 1.2f): RoundedPolygon {
- return RoundedPolygon(numVertices = 4, rounding = cornerRound30)
- .transformed(Matrix().apply { scale(x = scaleX, y = scaleY) })
+ internal fun diamond(): RoundedPolygon {
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 1.096f), CornerRounding(0.151f, 0.524f)),
+ PointNRound(Offset(0.040f, 0.500f), CornerRounding(0.159f))
+ ),
+ 2
+ )
}
internal fun clamShell(): RoundedPolygon {
- val cornerInset = .6f
- val edgeInset = .4f
- val height = .7f
- val hexPoints =
- floatArrayOf(
- 1f,
- 0f,
- cornerInset,
- height,
- edgeInset,
- height,
- -edgeInset,
- height,
- -cornerInset,
- height,
- -1f,
- 0f,
- -cornerInset,
- -height,
- -edgeInset,
- -height,
- edgeInset,
- -height,
- cornerInset,
- -height,
- )
- val pvRounding =
+ return customPolygon(
listOf(
- cornerRound30,
- cornerRound30,
- unrounded,
- unrounded,
- cornerRound30,
- cornerRound30,
- cornerRound30,
- unrounded,
- unrounded,
- cornerRound30,
- )
- return RoundedPolygon(hexPoints, perVertexRounding = pvRounding)
+ PointNRound(Offset(0.171f, 0.841f), CornerRounding(0.159f)),
+ PointNRound(Offset(-0.020f, 0.500f), CornerRounding(0.140f)),
+ PointNRound(Offset(0.170f, 0.159f), CornerRounding(0.159f))
+ ),
+ 2
+ )
}
internal fun pentagon(): RoundedPolygon {
- return RoundedPolygon(numVertices = 5, rounding = cornerRound30)
- .transformed(Matrix().apply { rotateZ(-360f / 20f) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, -0.009f), CornerRounding(0.172f)),
+ PointNRound(Offset(1.030f, 0.365f), CornerRounding(0.164f)),
+ PointNRound(Offset(0.828f, 0.970f), CornerRounding(0.169f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
internal fun gem(): RoundedPolygon {
- // irregular hexagon (right narrower than left, then rotated)
- // First, generate a standard hexagon
- val numVertices = 6
- val radius = 1f
- val points = FloatArray(numVertices * 2)
- var index = 0
- for (i in 0 until numVertices) {
- val vertex = radialToCartesian(radius, (PI.toFloat() / numVertices * 2 * i))
- points[index++] = vertex.x
- points[index++] = vertex.y
- }
- // Now adjust-in the points at the top (next-to-last and second vertices, post rotation)
- points[2] -= .1f
- points[3] -= .1f
- points[10] -= .1f
- points[11] += .1f
- return RoundedPolygon(points, cornerRound40).transformed(rotateNeg90)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.499f, 1.023f), CornerRounding(0.241f, 0.778f)),
+ PointNRound(Offset(-0.005f, 0.792f), CornerRounding(0.208f)),
+ PointNRound(Offset(0.073f, 0.258f), CornerRounding(0.228f)),
+ PointNRound(Offset(0.433f, -0.000f), CornerRounding(0.491f))
+ ),
+ 1,
+ mirroring = true
+ )
}
internal fun sunny(): RoundedPolygon {
@@ -482,30 +444,34 @@
}
internal fun verySunny(): RoundedPolygon {
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- innerRadius = .65f,
- rounding = cornerRound15
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 1.080f), CornerRounding(0.085f)),
+ PointNRound(Offset(0.358f, 0.843f), CornerRounding(0.085f))
+ ),
+ 8
)
}
internal fun cookie4(): RoundedPolygon {
- return RoundedPolygon.star(
- numVerticesPerRadius = 4,
- innerRadius = .5f,
- rounding = cornerRound30
- )
- .transformed(rotateNeg45)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(1.237f, 1.236f), CornerRounding(0.258f)),
+ PointNRound(Offset(0.500f, 0.918f), CornerRounding(0.233f))
+ ),
+ 4
+ )
}
internal fun cookie6(): RoundedPolygon {
// 6-point cookie
- return RoundedPolygon.star(
- numVerticesPerRadius = 6,
- innerRadius = .75f,
- rounding = cornerRound50
- )
- .transformed(rotateNeg90)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.723f, 0.884f), CornerRounding(0.394f)),
+ PointNRound(Offset(0.500f, 1.099f), CornerRounding(0.398f))
+ ),
+ 6
+ )
}
internal fun cookie7(): RoundedPolygon {
@@ -537,303 +503,199 @@
}
internal fun ghostish(): RoundedPolygon {
- val inset = .46f
- val h = 1.2f
- val points = floatArrayOf(-1f, -h, 1f, -h, 1f, h, 0f, inset, -1f, h)
- val pvRounding =
- listOf(cornerRound100, cornerRound100, cornerRound50, cornerRound100, cornerRound50)
- return RoundedPolygon(points, perVertexRounding = pvRounding)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0f), CornerRounding(1.000f)),
+ PointNRound(Offset(1f, 0f), CornerRounding(1.000f)),
+ PointNRound(Offset(1f, 1.140f), CornerRounding(0.254f, 0.106f)),
+ PointNRound(Offset(0.575f, 0.906f), CornerRounding(0.253f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
internal fun clover4(): RoundedPolygon {
- // (no inner rounding)
- return RoundedPolygon.star(
- numVerticesPerRadius = 4,
- innerRadius = .2f,
- rounding = cornerRound40,
- innerRounding = unrounded
- )
- .transformed(rotate45)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.074f)),
+ PointNRound(Offset(0.725f, -0.099f), CornerRounding(0.476f))
+ ),
+ reps = 4,
+ mirroring = true
+ )
}
internal fun clover8(): RoundedPolygon {
- // (no inner rounding)
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- innerRadius = .65f,
- rounding = cornerRound30,
- innerRounding = unrounded
- )
- .transformed(Matrix().apply { rotateZ(360f / 16) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.036f)),
+ PointNRound(Offset(0.758f, -0.101f), CornerRounding(0.209f))
+ ),
+ reps = 8
+ )
}
internal fun burst(): RoundedPolygon {
- return RoundedPolygon.star(numVerticesPerRadius = 12, innerRadius = .7f)
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, -0.006f), CornerRounding(0.006f)),
+ PointNRound(Offset(0.592f, 0.158f), CornerRounding(0.006f))
+ ),
+ reps = 12
+ )
}
internal fun softBurst(): RoundedPolygon {
- return RoundedPolygon.star(
- radius = 1f,
- numVerticesPerRadius = 10,
- innerRadius = .65f,
- rounding = cornerRound10,
- innerRounding = cornerRound10
- )
- .transformed(Matrix().apply { rotateZ(360f / 20) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.193f, 0.277f), CornerRounding(0.053f)),
+ PointNRound(Offset(0.176f, 0.055f), CornerRounding(0.053f))
+ ),
+ reps = 10
+ )
}
internal fun boom(): RoundedPolygon {
- return RoundedPolygon.star(numVerticesPerRadius = 15, innerRadius = .42f)
- .transformed(Matrix().apply { rotateZ(360f / 60) })
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.457f, 0.296f), CornerRounding(0.007f)),
+ PointNRound(Offset(0.500f, -0.051f), CornerRounding(0.007f))
+ ),
+ reps = 15
+ )
}
internal fun softBoom(): RoundedPolygon {
- val points =
- arrayOf(
- Offset(0.456f, 0.224f),
- Offset(0.460f, 0.170f),
- Offset(0.500f, 0.100f),
- Offset(0.540f, 0.170f),
- Offset(0.544f, 0.224f),
- Offset(0.538f, 0.308f)
- )
- val actualPoints = doRepeat(points, 16, center = Offset(0.5f, 0.5f))
- val roundings =
+ return customPolygon(
listOf(
- CornerRounding(radius = 0.020f),
- CornerRounding(radius = 0.143f),
- CornerRounding(radius = 0.025f),
- CornerRounding(radius = 0.143f),
- CornerRounding(radius = 0.190f),
- CornerRounding(radius = 0f)
- )
- .let { l -> (0 until 16).flatMap { l } }
-
- return RoundedPolygon(
- actualPoints,
- perVertexRounding = roundings,
- centerX = 0.5f,
- centerY = 0.5f
+ PointNRound(Offset(0.733f, 0.454f)),
+ PointNRound(Offset(0.839f, 0.437f), CornerRounding(0.532f)),
+ PointNRound(Offset(0.949f, 0.449f), CornerRounding(0.439f, 1.000f)),
+ PointNRound(Offset(0.998f, 0.478f), CornerRounding(0.174f))
+ ),
+ reps = 16,
+ mirroring = true
)
}
internal fun flower(): RoundedPolygon {
- val smoothRound = CornerRounding(radius = .12f, smoothing = .48f)
- return RoundedPolygon.star(
- numVerticesPerRadius = 8,
- radius = 1f,
- innerRadius = .588f,
- rounding = smoothRound,
- innerRounding = unrounded
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.370f, 0.187f)),
+ PointNRound(Offset(0.416f, 0.049f), CornerRounding(0.381f)),
+ PointNRound(Offset(0.479f, 0.001f), CornerRounding(0.095f))
+ ),
+ reps = 8,
+ mirroring = true
)
}
internal fun puffy(): RoundedPolygon {
- val pnr =
- listOf(
- PointNRound(Offset(0.500f, 0.260f), CornerRounding.Unrounded),
- PointNRound(Offset(0.526f, 0.188f), CornerRounding(0.095f)),
- PointNRound(Offset(0.676f, 0.226f), CornerRounding(0.095f)),
- PointNRound(Offset(0.660f, 0.300f), CornerRounding.Unrounded),
- PointNRound(Offset(0.734f, 0.230f), CornerRounding(0.095f)),
- PointNRound(Offset(0.838f, 0.350f), CornerRounding(0.095f)),
- PointNRound(Offset(0.782f, 0.418f), CornerRounding.Unrounded),
- PointNRound(Offset(0.874f, 0.414f), CornerRounding(0.095f)),
+ val m = Matrix().apply { scale(1f, 0.742f) }
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.053f)),
+ PointNRound(Offset(0.545f, -0.040f), CornerRounding(0.405f)),
+ PointNRound(Offset(0.670f, -0.035f), CornerRounding(0.426f)),
+ PointNRound(Offset(0.717f, 0.066f), CornerRounding(0.574f)),
+ PointNRound(Offset(0.722f, 0.128f)),
+ PointNRound(Offset(0.777f, 0.002f), CornerRounding(0.360f)),
+ PointNRound(Offset(0.914f, 0.149f), CornerRounding(0.660f)),
+ PointNRound(Offset(0.926f, 0.289f), CornerRounding(0.660f)),
+ PointNRound(Offset(0.881f, 0.346f)),
+ PointNRound(Offset(0.940f, 0.344f), CornerRounding(0.126f)),
+ PointNRound(Offset(1.003f, 0.437f), CornerRounding(0.255f)),
+ ),
+ reps = 2,
+ mirroring = true
)
- val actualPoints =
- doRepeat(pnr, reps = 4, center = Offset(0.5f, 0.5f), mirroring = true)
-
- return RoundedPolygon(
- actualPoints.fastFlatMap { listOf(it.o.x, it.o.y) }.toFloatArray(),
- perVertexRounding = actualPoints.fastMap { it.r },
- centerX = 0.5f,
- centerY = 0.5f
- )
+ .transformed(m)
}
internal fun puffyDiamond(): RoundedPolygon {
- val points =
- arrayOf(
- Offset(0.390f, 0.260f),
- Offset(0.390f, 0.130f),
- Offset(0.610f, 0.130f),
- Offset(0.610f, 0.260f),
- Offset(0.740f, 0.260f)
- )
- val actualPoints = doRepeat(points, reps = 4, center = Offset(0.5f, 0.5f))
- val roundings =
+ return customPolygon(
listOf(
- CornerRounding(radius = 0.000f),
- CornerRounding(radius = 0.104f),
- CornerRounding(radius = 0.104f),
- CornerRounding(radius = 0.000f),
- CornerRounding(radius = 0.104f)
- )
- .let { l -> (0 until 4).flatMap { l } }
-
- return RoundedPolygon(
- actualPoints,
- perVertexRounding = roundings,
- centerX = 0.5f,
- centerY = 0.5f
+ PointNRound(Offset(0.870f, 0.130f), CornerRounding(0.146f)),
+ PointNRound(Offset(0.818f, 0.357f)),
+ PointNRound(Offset(1.000f, 0.332f), CornerRounding(0.853f))
+ ),
+ reps = 4,
+ mirroring = true
)
}
@Suppress("ListIterator", "PrimitiveInCollection")
internal fun pixelCircle(): RoundedPolygon {
- val main = 0.4f
- val holes = listOf(Offset(0.28f, 0.14f), Offset(0.16f, 0.16f), Offset(0.16f, 0.3f))
- var p = Offset(main, -1f)
- val corner = buildList {
- add(p)
- holes.fastForEach { delta ->
- p += Offset(0f, delta.y)
- add(p)
- p += Offset(delta.x, 0f)
- add(p)
- }
- }
- val half = corner + corner.fastMap { Offset(it.x, -it.y) }.reversed()
- val points = half + half.fastMap { Offset(-it.x, it.y) }.reversed()
- return RoundedPolygon(
- points.fastFlatMap { listOf(it.x, it.y) }.toFloatArray(),
- unrounded
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.500f, 0.000f)),
+ PointNRound(Offset(0.704f, 0.000f)),
+ PointNRound(Offset(0.704f, 0.065f)),
+ PointNRound(Offset(0.843f, 0.065f)),
+ PointNRound(Offset(0.843f, 0.148f)),
+ PointNRound(Offset(0.926f, 0.148f)),
+ PointNRound(Offset(0.926f, 0.296f)),
+ PointNRound(Offset(1.000f, 0.296f))
+ ),
+ reps = 2,
+ mirroring = true
)
}
@Suppress("ListIterator", "PrimitiveInCollection")
internal fun pixelTriangle(): RoundedPolygon {
- var point = Offset(0f, 0f)
- val points = mutableListOf<Offset>()
- points.add(point)
- val sizes = listOf(56f, 28f, 44f, 26f, 44f, 32f, 38f, 26f, 38f, 32f)
- sizes.chunked(2).forEach { (dx, dy) ->
- point += Offset(dx, 0f)
- points.add(point)
- point += Offset(0f, dy)
- points.add(point)
- }
- point += Offset(32f, 0f)
- points.add(point)
- point += Offset(0f, 38f)
- points.add(point)
- point += Offset(-32f, 0f)
- points.add(point)
- sizes.reversed().chunked(2).forEach { (dy, dx) ->
- point += Offset(0f, dy)
- points.add(point)
- point += Offset(-dx, 0f)
- points.add(point)
- }
- val centerX = points.fastMaxBy { it.x }!!.x / 2
- val centerY = points.fastMaxBy { it.y }!!.y / 2
-
- return RoundedPolygon(
- points.fastFlatMap { listOf(it.x, it.y) }.toFloatArray(),
- centerX = centerX,
- centerY = centerY,
+ return customPolygon(
+ listOf(
+ PointNRound(Offset(0.110f, 0.500f)),
+ PointNRound(Offset(0.113f, 0.000f)),
+ PointNRound(Offset(0.287f, 0.000f)),
+ PointNRound(Offset(0.287f, 0.087f)),
+ PointNRound(Offset(0.421f, 0.087f)),
+ PointNRound(Offset(0.421f, 0.170f)),
+ PointNRound(Offset(0.560f, 0.170f)),
+ PointNRound(Offset(0.560f, 0.265f)),
+ PointNRound(Offset(0.674f, 0.265f)),
+ PointNRound(Offset(0.675f, 0.344f)),
+ PointNRound(Offset(0.789f, 0.344f)),
+ PointNRound(Offset(0.789f, 0.439f)),
+ PointNRound(Offset(0.888f, 0.439f))
+ ),
+ reps = 1,
+ mirroring = true
)
}
internal fun bun(): RoundedPolygon {
- // Basically, two pills stacked on each other
- val inset = .4f
- val sandwichPoints =
- floatArrayOf(
- 1f,
- 1f,
- inset,
- 1f,
- -inset,
- 1f,
- -1f,
- 1f,
- -1f,
- 0f,
- -inset,
- 0f,
- -1f,
- 0f,
- -1f,
- -1f,
- -inset,
- -1f,
- inset,
- -1f,
- 1f,
- -1f,
- 1f,
- 0f,
- inset,
- 0f,
- 1f,
- 0f
- )
- val pvRounding =
+ return customPolygon(
listOf(
- cornerRound100,
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100
- )
- return RoundedPolygon(sandwichPoints, perVertexRounding = pvRounding)
+ PointNRound(Offset(0.796f, 0.500f)),
+ PointNRound(Offset(0.853f, 0.518f), CornerRounding(1f)),
+ PointNRound(Offset(0.992f, 0.631f), CornerRounding(1f)),
+ PointNRound(Offset(0.968f, 1.000f), CornerRounding(1f))
+ ),
+ reps = 2,
+ mirroring = true
+ )
}
internal fun heart(): RoundedPolygon {
- val points =
- floatArrayOf(
- .2f,
- 0f,
- -.4f,
- .5f,
- -1f,
- 1f,
- -1.5f,
- .5f,
- -1f,
- 0f,
- -1.5f,
- -.5f,
- -1f,
- -1f,
- -.4f,
- -.5f
- )
- val pvRounding =
+ return customPolygon(
listOf(
- unrounded,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded,
- cornerRound100,
- cornerRound100,
- unrounded
- )
- return RoundedPolygon(points, perVertexRounding = pvRounding).transformed(rotate90)
+ PointNRound(Offset(0.500f, 0.268f), CornerRounding(0.016f)),
+ PointNRound(Offset(0.792f, -0.066f), CornerRounding(0.958f)),
+ PointNRound(Offset(1.064f, 0.276f), CornerRounding(1.000f)),
+ PointNRound(Offset(0.501f, 0.946f), CornerRounding(0.129f))
+ ),
+ reps = 1,
+ mirroring = true
+ )
}
- private data class PointNRound(val o: Offset, val r: CornerRounding)
-
- private fun doRepeat(points: Array<Offset>, reps: Int, center: Offset) =
- points.size.let { np ->
- (0 until np * reps)
- .flatMap {
- val point = points[it % np].rotateDegrees((it / np) * 360f / reps, center)
- listOf(point.x, point.y)
- }
- .toFloatArray()
- }
+ private data class PointNRound(
+ val o: Offset,
+ val r: CornerRounding = CornerRounding.Unrounded
+ )
@Suppress("PrimitiveInCollection")
private fun doRepeat(
@@ -846,8 +708,9 @@
buildList {
val angles = points.fastMap { (it.o - center).angleDegrees() }
val distances = points.fastMap { (it.o - center).getDistance() }
- val sectionAngle = 360f / reps
- repeat(reps) {
+ val actualReps = reps * 2
+ val sectionAngle = 360f / actualReps
+ repeat(actualReps) {
points.indices.forEach { index ->
val i = if (it % 2 == 0) index else points.lastIndex - index
if (i > 0 || it % 2 == 0) {
@@ -883,13 +746,22 @@
private fun Offset.angleDegrees() = atan2(y, x) * 180f / PI.toFloat()
- private fun directionVector(angleRadians: Float) =
- Offset(cos(angleRadians), sin(angleRadians))
-
- private fun radialToCartesian(
- radius: Float,
- angleRadians: Float,
- center: Offset = Offset.Zero
- ) = directionVector(angleRadians) * radius + center
+ private fun customPolygon(
+ pnr: List<PointNRound>,
+ reps: Int,
+ center: Offset = Offset(0.5f, 0.5f),
+ mirroring: Boolean = false
+ ): RoundedPolygon {
+ val actualPoints = doRepeat(pnr, reps, center, mirroring)
+ return RoundedPolygon(
+ vertices =
+ FloatArray(actualPoints.size * 2) { ix ->
+ actualPoints[ix / 2].o.let { if (ix % 2 == 0) it.x else it.y }
+ },
+ perVertexRounding = buildList { for (p in actualPoints) add(p.r) },
+ centerX = center.x,
+ centerY = center.y
+ )
+ }
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index d1ac45a..4185410f7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -298,10 +298,7 @@
// min anchor. This is done to avoid showing a gap when the sheet opens and bounces
// when it's applied with a bouncy motion. Note that the content inside the Surface
// is scaled back down to maintain its aspect ratio (see below).
- .verticalScaleUp(
- { sheetState.anchoredDraggableState.offset },
- { sheetState.anchoredDraggableState.anchors.minAnchor() }
- ),
+ .verticalScaleUp(sheetState),
shape = shape,
color = containerColor,
contentColor = contentColor,
@@ -324,10 +321,7 @@
// Scale the content down in case the sheet offset overflows below the min anchor.
// The wrapping Surface is scaled up, so this is done to maintain the content's
// aspect ratio.
- .verticalScaleDown(
- { sheetState.anchoredDraggableState.offset },
- { sheetState.anchoredDraggableState.anchors.minAnchor() }
- )
+ .verticalScaleDown(sheetState)
) {
if (dragHandle != null) {
val collapseActionLabel = getString(Strings.BottomSheetPartialExpandDescription)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 4cf6819..63959a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
/** Default size for the leading button end corners and trailing button start corners */
// TODO update token to dp size and use it here
val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
- private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+ private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
/**
* Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index bc9c449..b0fe245 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -956,7 +956,8 @@
autoSwitchToMinute: Boolean
) {
Row(
- modifier = modifier.padding(bottom = ClockFaceBottomMargin),
+ modifier =
+ modifier.semantics { isTraversalGroup = true }.padding(bottom = ClockFaceBottomMargin),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalClockDisplay(state, colors)
@@ -1573,7 +1574,7 @@
screen[index] % 12
}
ClockText(
- modifier = Modifier.semantics { traversalIndex = index.toFloat() },
+ modifier = Modifier.semantics { traversalIndex = index.toFloat() + 1f },
state = state,
value = outerValue,
autoSwitchToMinute = autoSwitchToMinute
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
index 953dcda..70fea23 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
@@ -25,7 +25,6 @@
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
@@ -284,7 +283,13 @@
* @param stroke a [Stroke] that will be used to draw this indicator
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
* @param wavelength the length of a wave
+ * @param waveSpeed the speed in which the wave will move when the [amplitude] is greater than zero.
+ * The value here represents a DP per seconds, and by default it's matched to the [wavelength] to
+ * render an animation that moves the wave by one wave length per second.
* @sample androidx.compose.material3.samples.IndeterminateLinearWavyProgressIndicatorSample
*/
@ExperimentalMaterial3ExpressiveApi
@@ -296,8 +301,41 @@
stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
- wavelength: Dp = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float = 1f,
+ wavelength: Dp = WavyProgressIndicatorDefaults.LinearIndeterminateWavelength,
+ waveSpeed: Dp = wavelength // Match to 1 wavelength per second
) {
+ // Progress offset animation for the waves that is determined by the wave speed and wavelength.
+ val lastOffsetValue = remember { mutableFloatStateOf(0f) }
+ val offsetAnimatable =
+ remember(waveSpeed, wavelength) { Animatable(lastOffsetValue.floatValue) }
+ LaunchedEffect(waveSpeed, wavelength) {
+ if (waveSpeed > 0.dp) {
+ // Compute the duration as a Dp per second.
+ val durationMillis = ((wavelength / waveSpeed) * 1000).toInt()
+ if (durationMillis > 0) {
+ // Update the bounds to start from the current value and to end at the value plus 1.
+ // This will ensure that there are no jumps in the animation in case the wave's
+ // speed or length are changing.
+ offsetAnimatable.updateBounds(
+ lastOffsetValue.floatValue,
+ lastOffsetValue.floatValue + 1
+ )
+ offsetAnimatable.animateTo(
+ lastOffsetValue.floatValue + 1,
+ animationSpec =
+ infiniteRepeatable(
+ animation = tween(durationMillis, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart,
+ )
+ ) {
+ lastOffsetValue.floatValue = value % 1f
+ }
+ }
+ }
+ }
+
+ // Head and tail animation for the two progress lines we will be displaying.
val infiniteTransition = rememberInfiniteTransition()
val firstLineHead =
infiniteTransition.animateFloat(
@@ -324,25 +362,13 @@
animationSpec = linearIndeterminateSecondLineTailAnimationSpec
)
- val waveOffset =
- infiniteTransition.animateFloat(
- 0f,
- 1f,
- infiniteRepeatable(
- animation =
- keyframes {
- durationMillis = LinearAnimationDuration
- 1f at LinearAnimationDuration using LinearIndeterminateProgressEasing
- }
- )
- )
-
// Holds the start and end progress fractions (each two consecutive numbers in the array hold
// the start and the end fractions for a single path)
// In this case of an indeterminate progress indicator, we have 2 progress paths that can
// appear at the same time while the indicator animates.
val progressFractions = floatArrayOf(0f, 0f, 0f, 0f)
val progressDrawingCache = remember { LinearProgressDrawingCache() }
+ val coercedAmplitude = amplitude.coerceIn(0f, 1f)
Spacer(
modifier
.then(IncreaseVerticalSemanticsBounds)
@@ -364,8 +390,8 @@
size = size,
wavelength = wavelength.toPx(),
progressFractions = progressFractions,
- amplitude = 1f,
- waveOffset = (waveOffset.value * 3) % 1f,
+ amplitude = coercedAmplitude,
+ waveOffset = if (coercedAmplitude > 0f) lastOffsetValue.floatValue else 0f,
gapSize = max(0f, gapSize.toPx()),
stroke = stroke,
trackStroke = trackStroke
@@ -485,8 +511,7 @@
progress = progress,
// Resolves the Path from the morph by using the amplitude value as a Morph's progress.
// Ensure that the Path is created with `repeatPath = supportMotion` to allow us to offset
- // the
- // progress later to simulate motion, if enabled.
+ // the progress later to simulate motion, if enabled.
progressPath = {
progressAmplitude,
progressWavelength,
@@ -539,8 +564,16 @@
* @param stroke a [Stroke] that will be used to draw this indicator
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
* @param wavelength the length of a wave in this circular indicator. Note that the actual
* wavelength may be different to ensure a continuous wave shape.
+ * @param waveSpeed the speed in which the wave will move when the [amplitude] is greater than zero.
+ * The value here represents a DP per seconds, and by default it's matched to the [wavelength] to
+ * render an animation that moves the wave by one wave length per second. Note that the actual
+ * speed may be slightly different, as the [wavelength] can be adjusted to ensure a continuous
+ * wave shape.
* @sample androidx.compose.material3.samples.IndeterminateCircularWavyProgressIndicatorSample
*/
@ExperimentalMaterial3ExpressiveApi
@@ -552,15 +585,61 @@
stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
- wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float = 1f,
+ wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength,
+ waveSpeed: Dp = wavelength // Match to 1 wavelength per second
) {
val circularShapes = remember { CircularShapes() }
+ val lastOffsetValue = remember { mutableFloatStateOf(0f) }
+ val offsetAnimatable =
+ remember(waveSpeed, wavelength) { Animatable(lastOffsetValue.floatValue) }
+
+ with(circularShapes) {
+ // Have the LaunchedEffect execute whenever a change in the currentVertexCount state
+ // happens.
+ if (currentVertexCount.intValue >= MinCircularVertexCount) {
+ LaunchedEffect(waveSpeed) {
+ if (waveSpeed > 0.dp) {
+ // Computes the duration as a Dp per second, and take into account the vertex
+ // count. We use the currentVertexCount to compute the duration for the wave's
+ // motion to be as close as possible to the requested speed.
+ val durationMillis =
+ ((wavelength / waveSpeed) * 1000 * currentVertexCount.intValue).toInt()
+ if (durationMillis > 0) {
+ // Update the bounds to start from the current value and to end at the value
+ // plus 1. This will ensure that there are no jumps in the animation in case
+ // the wave's speed or length are changing.
+ offsetAnimatable.updateBounds(
+ lowerBound = lastOffsetValue.floatValue,
+ upperBound = lastOffsetValue.floatValue + 1
+ )
+ offsetAnimatable.animateTo(
+ lastOffsetValue.floatValue + 1,
+ animationSpec =
+ infiniteRepeatable(
+ animation = tween(durationMillis, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart,
+ )
+ ) {
+ lastOffsetValue.floatValue = value % 1f
+ }
+ }
+ }
+ }
+ }
+ }
PathProgressIndicator(
modifier = modifier.size(WavyProgressIndicatorDefaults.CircularContainerSize),
// Resolves the Path from a RoundedPolygon that represents the active indicator.
- progressPath = { _, progressWavelength, strokeWidth, size, _, path ->
+ progressPath = { _, progressWavelength, strokeWidth, size, supportMotion, path ->
circularShapes.update(size, progressWavelength, strokeWidth)
- circularShapes.activeIndicatorPolygon!!.toPath(path)
+ circularShapes.activeIndicatorMorph!!.toPath(
+ progress = amplitude,
+ path = path,
+ repeatPath = supportMotion,
+ rotationPivotX = 0.5f,
+ rotationPivotY = 0.5f,
+ )
},
// Resolves the Path from a RoundedPolygon that represents the track.
trackPath = { _, progressWavelength, strokeWidth, size, path ->
@@ -571,10 +650,13 @@
trackColor = trackColor,
stroke = stroke,
trackStroke = trackStroke,
+ amplitude = amplitude.coerceIn(0f, 1f),
+ waveOffset = { lastOffsetValue.floatValue },
wavelength = wavelength,
gapSize = gapSize,
progressStart = CircularIndeterminateMinProgress,
- progressEnd = CircularIndeterminateMaxProgress
+ progressEnd = CircularIndeterminateMaxProgress,
+ enableProgressMotion = true
)
}
@@ -747,6 +829,12 @@
* @param stroke a [Stroke] that will be used to draw this indicator's progress
* @param trackStroke a [Stroke] that will be used to draw the indicator's track
* @param gapSize the gap between the track and the progress parts of the indicator
+ * @param amplitude the wave's amplitude. 0.0 represents no amplitude, and 1.0 represents an
+ * amplitude that will take the full height of the progress indicator. Values outside of this
+ * range are coerced into the range.
+ * @param waveOffset a lambda that controls the offset of the drawn wave and can be used to apply
+ * motion to the wave. The expected value is between 0.0 to 1.0. Values outside of this range are
+ * coerced into the range. An offset will only be applied when [enableProgressMotion] is true.
* @param wavelength the length of a wave in this circular indicator. Note that the actual
* wavelength may be different to ensure a continuous wave shape.
* @param progressStart the progress value that this indeterminate indicator will start animating
@@ -755,6 +843,10 @@
* @param progressEnd the progress value that this indeterminate indicator will progress towards
* when animating from the [progressStart]. This value is expected to be between 0.0 and 1.0, and
* greater than [progressStart].
+ * @param enableProgressMotion indicates if a progress motion should be enabled for the provided
+ * progress. When enabled, the calls to the [progressPath] will be made with a `supportMotion =
+ * true`, and the generated [Path] will need to be repeated to allow drawing it while shifting the
+ * start and stop points, as well as rotating it, in order to simulate a progress motion.
*/
@Composable
private fun PathProgressIndicator(
@@ -775,9 +867,12 @@
stroke: Stroke,
trackStroke: Stroke,
gapSize: Dp,
+ @FloatRange(from = 0.0, to = 1.0) amplitude: Float,
+ waveOffset: () -> Float,
wavelength: Dp,
@FloatRange(from = 0.0, to = 1.0) progressStart: Float,
- @FloatRange(from = 0.0, to = 1.0) progressEnd: Float
+ @FloatRange(from = 0.0, to = 1.0) progressEnd: Float,
+ enableProgressMotion: Boolean
) {
require(progressEnd > progressStart) {
"Expecting a progress end that is greater than the progress start"
@@ -829,11 +924,16 @@
size = size,
progressPath = progressPath,
trackPath = trackPath,
- enableProgressMotion = false,
+ enableProgressMotion = enableProgressMotion,
startProgress = 0f,
endProgress = progressAnimation.value,
- amplitude = 1f,
- waveOffset = 0f,
+ amplitude = amplitude,
+ waveOffset =
+ if (amplitude > 0f) {
+ waveOffset().coerceIn(0f, 1f)
+ } else {
+ 0f
+ },
wavelength = wavelength.toPx(),
gapSize = trackGapSize,
stroke = stroke,
@@ -980,7 +1080,7 @@
*/
val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
- /** A function that returns the indicator's amplitude for a given progress */
+ /** A function that returns a determinate indicator's amplitude for a given progress. */
val indicatorAmplitude: (progress: Float) -> Float = { progress ->
// Sets the amplitude to the max on 10%, and back to zero on 95% of the progress.
if (progress <= 0.1f || progress >= 0.95f) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index 65e07ad..3cbed88 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -21,16 +21,20 @@
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
@@ -53,7 +57,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -117,14 +123,17 @@
*
* @sample androidx.compose.material3.samples.WideNavigationRailExpandedSample
*
- * Finally, the [WideNavigationRail] also supports automatically animating between the collapsed and
- * expanded values. That can be done like so:
+ * The [WideNavigationRail] also supports automatically animating between the collapsed and expanded
+ * values. That can be done like so:
*
* @sample androidx.compose.material3.samples.WideNavigationRailResponsiveSample
*
- * The [WideNavigationRail] supports setting an [WideNavigationRailArrangement] for the items, so
- * that the items can be grouped at the top (the default), at the middle, or at the bottom of the
- * rail. The header will always be at the top.
+ * For modal variations of the wide navigation rail, see [ModalWideNavigationRail] and
+ * [DismissibleModalWideNavigationRail].
+ *
+ * Finally, the [WideNavigationRail] supports setting a [WideNavigationRailArrangement] for the
+ * items, so that the items can be grouped at the top (the default), at the middle, or at the bottom
+ * of the rail. The header will always be at the top.
*
* See [WideNavigationRailItem] for configuration specific to each item, and not the overall
* [WideNavigationRail] component.
@@ -138,8 +147,6 @@
* @param windowInsets a window insets of the wide navigation rail
* @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
* @param content the content of this wide navigation rail, typically [WideNavigationRailItem]s
- *
- * TODO: Implement modal expanded option and add relevant params.
*/
@ExperimentalMaterial3ExpressiveApi
@Composable
@@ -190,15 +197,16 @@
// TODO: Load the motionScheme tokens from the component tokens file.
val animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value<Dp>()
+ val modalAnimationSpec = MotionSchemeKeyTokens.FastSpatial.value<Dp>()
val minWidth by
animateDpAsState(
targetValue = if (!expanded) CollapsedRailWidth else ExpandedRailMinWidth,
- animationSpec = animationSpec
+ animationSpec = if (!isModal) animationSpec else modalAnimationSpec
)
val widthFullRange by
animateDpAsState(
targetValue = if (!expanded) CollapsedRailWidth else ExpandedRailMaxWidth,
- animationSpec = animationSpec
+ animationSpec = if (!isModal) animationSpec else modalAnimationSpec
)
val itemVerticalSpacedBy by
animateDpAsState(
@@ -212,7 +220,7 @@
)
Surface(
- color = if (!isModal) colors.containerColor else colors.expandedModalContainerColor,
+ color = if (!isModal) colors.containerColor else colors.modalContainerColor,
contentColor = colors.contentColor,
shape = shape,
modifier = modifier,
@@ -381,61 +389,212 @@
}
/**
- * A standalone modal expanded wide navigation rail.
+ * Material design modal wide navigation rail.
*
* Wide navigation rails provide access to primary destinations in apps when using tablet and
* desktop screens.
*
- * The modal expanded rail blocks interaction with the rest of an app’s content with a scrim. It is
- * elevated above most of the app’s UI and doesn't affect the screen’s layout grid.
+ * The modal wide navigation rail should be used to display multiple [WideNavigationRailItem]s, each
+ * representing a singular app destination, and, optionally, a header containing a menu button, a
+ * [FloatingActionButton], and/or a logo. Each destination is typically represented by an icon and a
+ * text label.
*
- * The modal expanded wide navigation rail should be used to display at least three
+ * The [ModalWideNavigationRail] when collapsed behaves like a collapsed [WideNavigationRail]. When
+ * [expanded], the modal wide navigation rail blocks interaction with the rest of an app’s content
+ * with a scrim. It is elevated above the app’s UI and doesn't affect the screen’s layout grid. That
+ * can be achieved like so:
+ *
+ * @sample androidx.compose.material3.samples.ModalWideNavigationRailSample
+ *
+ * For a dismissible modal wide rail, that enters from offscreen instead of expanding from the
+ * collapsed rail, see [DismissibleModalWideNavigationRail].
+ *
+ * See [WideNavigationRailItem] for configuration specific to each item, and not the overall
+ * [ModalWideNavigationRail] component.
+ *
+ * @param scrimOnClick executes when the scrim is clicked. Usually it should be a function that
+ * instructs the rail to collapse
+ * @param modifier the [Modifier] to be applied to this wide navigation rail
+ * @param expanded whether this wide navigation rail is expanded or collapsed (default).
+ * @param collapsedShape the shape of this wide navigation rail's container when it's collapsed.
+ * @param expandedShape the shape of this wide navigation rail's container when it's [expanded]
+ * @param colors [WideNavigationRailColors] that will be used to resolve the colors used for this
+ * wide navigation rail. See [WideNavigationRailDefaults.colors]
+ * @param header optional header that may hold a [FloatingActionButton] or a logo
+ * @param expandedHeaderTopPadding the padding to be applied to the top of the rail. It's usually
+ * needed in order to align the content of the rail between the collapsed and expanded animation
+ * @param windowInsets a window insets of the wide navigation rail
+ * @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
+ * @param expandedProperties [ModalWideNavigationRailProperties] for further customization of the
+ * expanded modal wide navigation rail's window behavior
+ * @param content the content of this modal wide navigation rail, usually [WideNavigationRailItem]s
+ */
+@ExperimentalMaterial3ExpressiveApi
+@Composable
+fun ModalWideNavigationRail(
+ scrimOnClick: (() -> Unit),
+ modifier: Modifier = Modifier,
+ expanded: Boolean = false,
+ collapsedShape: Shape = WideNavigationRailDefaults.containerShape,
+ expandedShape: Shape = WideNavigationRailDefaults.modalContainerShape,
+ colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
+ header: @Composable (() -> Unit)? = null,
+ expandedHeaderTopPadding: Dp = 0.dp,
+ windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
+ arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
+ expandedProperties: ModalWideNavigationRailProperties =
+ DismissibleModalWideNavigationRailDefaults.Properties,
+ content: @Composable () -> Unit
+) {
+ val rememberContent = remember(content) { movableContentOf(content) }
+ val railState = rememberDismissibleModalWideNavigationRailState()
+ val positionProgress =
+ animateFloatAsState(
+ targetValue = if (!expanded) 0f else 1f,
+ // TODO: Load the motionScheme tokens from the component tokens file.
+ animationSpec = MotionSchemeKeyTokens.DefaultEffects.value()
+ )
+ val isCollapsed by remember { derivedStateOf { positionProgress.value == 0f } }
+ val modalExpanded by remember { derivedStateOf { positionProgress.value >= 0.3f } }
+ val onDismissRequest: suspend () -> Unit = { scrimOnClick() }
+
+ // Display a non modal rail when collapsed.
+ if (isCollapsed) {
+ WideNavigationRailLayout(
+ modifier = modifier,
+ isModal = false,
+ expanded = false,
+ colors = colors,
+ shape = collapsedShape,
+ header = header,
+ windowInsets = windowInsets,
+ arrangement = arrangement,
+ content = rememberContent
+ )
+ }
+ // Display a modal container when expanded.
+ if (!isCollapsed) {
+ // Have a spacer the size of the collapsed rail so that screen content doesn't shift.
+ Box(modifier = Modifier.background(color = colors.containerColor, shape = collapsedShape)) {
+ Spacer(modifier = modifier.widthIn(min = CollapsedRailWidth).fillMaxHeight())
+ }
+ val scope = rememberCoroutineScope()
+ val predictiveBackProgress = remember { Animatable(initialValue = 0f) }
+ val predictiveBackState = remember { RailPredictiveBackState() }
+
+ ModalWideNavigationRailDialog(
+ properties = expandedProperties,
+ onDismissRequest = { scope.launch { onDismissRequest() } },
+ onPredictiveBack = { backEvent ->
+ scope.launch { predictiveBackProgress.snapTo(backEvent) }
+ },
+ onPredictiveBackCancelled = { scope.launch { predictiveBackProgress.animateTo(0f) } },
+ predictiveBackState = predictiveBackState
+ ) {
+ Box(modifier = Modifier.fillMaxSize().imePadding()) {
+ Scrim(
+ color = colors.modalScrimColor,
+ onDismissRequest = onDismissRequest,
+ visible = modalExpanded
+ )
+ ModalWideNavigationRailContent(
+ expanded = modalExpanded,
+ isStandaloneModal = false,
+ predictiveBackProgress = predictiveBackProgress,
+ predictiveBackState = predictiveBackState,
+ settleToDismiss = {},
+ modifier = modifier,
+ railState = railState,
+ colors = colors,
+ shape = expandedShape,
+ openModalRailMaxWidth = ExpandedRailMaxWidth,
+ header = {
+ Column {
+ Spacer(Modifier.height(expandedHeaderTopPadding))
+ header?.invoke()
+ }
+ },
+ windowInsets = windowInsets,
+ gesturesEnabled = false,
+ arrangement = arrangement,
+ content = rememberContent
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(isCollapsed) {
+ if (isCollapsed) {
+ railState.close()
+ } else {
+ railState.open()
+ }
+ }
+}
+
+/**
+ * A dismissible modal wide navigation rail.
+ *
+ * Wide navigation rails provide access to primary destinations in apps when using tablet and
+ * desktop screens.
+ *
+ * The dismissible modal wide navigation rail blocks interaction with the rest of an app’s content
+ * with a scrim when expanded. It is elevated above most of the app’s UI and doesn't affect the
+ * screen’s layout grid. When collapsed, the rail is hidden.
+ *
+ * The dismissible modal wide navigation rai should be used to display at least three
* [WideNavigationRailItem]s with their icon position set to [NavigationItemIconPosition.Start],
* each representing a singular app destination, and, optionally, a header containing a menu button,
* a [FloatingActionButton], and/or a logo. Each destination is typically represented by an icon and
* a text label. A simple example looks like:
*
- * @sample androidx.compose.material3.samples.ModalExpandedNavigationRailSample
+ * @sample androidx.compose.material3.samples.DismissibleModalWideNavigationRailSample
+ *
+ * For a modal rail that expands from a collapsed rail, instead of entering from offscreen, see
+ * [ModalWideNavigationRail].
*
* See [WideNavigationRailItem] for configuration specific to each item, and not the overall
- * [ModalExpandedNavigationRail] component.
+ * [DismissibleModalWideNavigationRail] component.
*
- * @param onDismissRequest Executes when the user rail closes, after it animates to
- * [ModalExpandedNavigationRailValue.Closed]
- * @param modifier the [Modifier] to be applied to this modal expanded navigation rail
- * @param railState state of the modal expanded navigation rail
- * @param shape defines the shape of this modal expanded navigation rail's container
+ * @param onDismissRequest executes when the user closes the rail, after it animates to
+ * [DismissibleModalWideNavigationRailValue.Closed]
+ * @param modifier the [Modifier] to be applied to this dismissible modal wide navigation rail
+ * @param railState state of the dismissible modal wide navigation rail
+ * @param shape defines the shape of this dismissible modal wide navigation rail's container
* @param colors [WideNavigationRailColors] that will be used to resolve the colors used for this
- * modal expanded navigation rail. See [WideNavigationRailDefaults.colors]
+ * dismissible modal wide navigation rail. See [WideNavigationRailDefaults.colors]
* @param header optional header that may hold a [FloatingActionButton] or a logo
- * @param windowInsets a window insets of this modal expanded navigation rail
- * @param arrangement the [WideNavigationRailArrangement] of this modal expanded navigation rail
- * @param gesturesEnabled whether the modal expanded navigation rail can be interacted by gestures
- * @param properties [ModalExpandedNavigationRailProperties] for further customization of this modal
+ * @param windowInsets a window insets of this dismissible modal wide navigation rail
+ * @param arrangement the [WideNavigationRailArrangement] of this dismissible modal wide navigation
+ * rail
+ * @param gesturesEnabled whether the dismissible modal wide navigation rail can be interacted by
+ * gestures
+ * @param properties [ModalWideNavigationRailProperties] for further customization of this modal
* expanded navigation rail's window behavior
- * @param content the content of this modal expanded navigation rail, typically
+ * @param content the content of this dismissible modal wide navigation rail, typically
* [WideNavigationRailItem]s with [NavigationItemIconPosition.Start] icon position
*/
@ExperimentalMaterial3ExpressiveApi
@Composable
-fun ModalExpandedNavigationRail(
+fun DismissibleModalWideNavigationRail(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
- railState: ModalExpandedNavigationRailState = rememberModalExpandedNavigationRailState(),
+ railState: DismissibleModalWideNavigationRailState =
+ rememberDismissibleModalWideNavigationRailState(),
shape: Shape = WideNavigationRailDefaults.modalContainerShape,
colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
header: @Composable (() -> Unit)? = null,
windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
gesturesEnabled: Boolean = true,
- properties: ModalExpandedNavigationRailProperties =
- ModalExpandedNavigationRailDefaults.Properties,
+ properties: ModalWideNavigationRailProperties =
+ DismissibleModalWideNavigationRailDefaults.Properties,
content: @Composable () -> Unit
) {
val animateToDismiss: suspend () -> Unit = {
if (
railState.anchoredDraggableState.confirmValueChange(
- ModalExpandedNavigationRailValue.Closed
+ DismissibleModalWideNavigationRailValue.Closed
)
) {
railState.close()
@@ -461,11 +620,13 @@
) {
Box(modifier = Modifier.fillMaxSize().imePadding()) {
Scrim(
- color = colors.expandedModalScrimColor,
+ color = colors.modalScrimColor,
onDismissRequest = animateToDismiss,
- visible = railState.targetValue != ModalExpandedNavigationRailValue.Closed
+ visible = railState.targetValue != DismissibleModalWideNavigationRailValue.Closed
)
ModalWideNavigationRailContent(
+ expanded = true,
+ isStandaloneModal = true,
predictiveBackProgress = predictiveBackProgress,
predictiveBackState = predictiveBackState,
settleToDismiss = settleToDismiss,
@@ -623,17 +784,17 @@
* @param contentColor the preferred color for content inside a wide navigation rail. Defaults to
* either the matching content color for [containerColor], or to the current [LocalContentColor]
* if [containerColor] is not a color from the theme
- * @param expandedModalContainerColor the color used for the background of a modal expanded
- * navigation rail. Use [Color.Transparent] to have no color
- * @param expandedModalScrimColor the color used for the scrim overlay for background content of a
- * modal expanded navigation rail
+ * @param modalContainerColor the color used for the background of a modal wide navigation rail. Use
+ * [Color.Transparent] to have no color
+ * @param modalScrimColor the color used for the scrim overlay for background content of a modal
+ * wide navigation rail
*/
@Immutable
class WideNavigationRailColors(
val containerColor: Color,
val contentColor: Color,
- val expandedModalContainerColor: Color,
- val expandedModalScrimColor: Color,
+ val modalContainerColor: Color,
+ val modalScrimColor: Color,
) {
/**
* Returns a copy of this NavigationRailColors, optionally overriding some of the values. This
@@ -642,15 +803,14 @@
fun copy(
containerColor: Color = this.containerColor,
contentColor: Color = this.contentColor,
- expandedModalContainerColor: Color = this.expandedModalContainerColor,
- modalScrimColor: Color = this.expandedModalScrimColor,
+ modalContainerColor: Color = this.modalContainerColor,
+ modalScrimColor: Color = this.modalScrimColor,
) =
WideNavigationRailColors(
containerColor = containerColor.takeOrElse { this.containerColor },
contentColor = contentColor.takeOrElse { this.contentColor },
- expandedModalContainerColor =
- expandedModalContainerColor.takeOrElse { this.expandedModalContainerColor },
- expandedModalScrimColor = modalScrimColor.takeOrElse { this.expandedModalScrimColor },
+ modalContainerColor = modalContainerColor.takeOrElse { this.modalContainerColor },
+ modalScrimColor = modalScrimColor.takeOrElse { this.modalScrimColor },
)
override fun equals(other: Any?): Boolean {
@@ -659,8 +819,8 @@
if (containerColor != other.containerColor) return false
if (contentColor != other.contentColor) return false
- if (expandedModalContainerColor != other.expandedModalContainerColor) return false
- if (expandedModalScrimColor != other.expandedModalScrimColor) return false
+ if (modalContainerColor != other.modalContainerColor) return false
+ if (modalScrimColor != other.modalScrimColor) return false
return true
}
@@ -668,8 +828,8 @@
override fun hashCode(): Int {
var result = containerColor.hashCode()
result = 31 * result + contentColor.hashCode()
- result = 31 * result + expandedModalContainerColor.hashCode()
- result = 31 * result + expandedModalScrimColor.hashCode()
+ result = 31 * result + modalContainerColor.hashCode()
+ result = 31 * result + modalScrimColor.hashCode()
return result
}
@@ -682,7 +842,7 @@
val containerShape: Shape
@Composable get() = NavigationRailCollapsedTokens.ContainerShape.value
- /** Default container shape of a modal expanded navigation rail. */
+ /** Default container shape of a modal wide navigation rail. */
val modalContainerShape: Shape
@Composable get() = NavigationRailExpandedTokens.ModalContainerShape.value
@@ -714,9 +874,9 @@
?: WideNavigationRailColors(
containerColor = containerColor,
contentColor = contentColorFor(containerColor),
- expandedModalContainerColor =
+ modalContainerColor =
fromToken(NavigationRailExpandedTokens.ModalContainerColor),
- expandedModalScrimColor =
+ modalScrimColor =
ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
)
.also { defaultWideWideNavigationRailColorsCached = it }
@@ -762,18 +922,21 @@
}
}
-/** Default values for [ModalExpandedNavigationRail] */
+/** Default values for [DismissibleModalWideNavigationRail] */
@Immutable
@ExperimentalMaterial3ExpressiveApi
-expect object ModalExpandedNavigationRailDefaults {
+expect object DismissibleModalWideNavigationRailDefaults {
- /** Properties used to customize the behavior of a [ModalExpandedNavigationRail]. */
- val Properties: ModalExpandedNavigationRailProperties
+ /**
+ * Properties used to customize the behavior of a [ModalWideNavigationRail] or of a
+ * [DismissibleModalWideNavigationRail].
+ */
+ val Properties: ModalWideNavigationRailProperties
}
@Immutable
@ExperimentalMaterial3ExpressiveApi
-expect class ModalExpandedNavigationRailProperties(
+expect class ModalWideNavigationRailProperties(
shouldDismissOnBackPress: Boolean = true,
) {
val shouldDismissOnBackPress: Boolean
@@ -783,7 +946,7 @@
@Composable
internal expect fun ModalWideNavigationRailDialog(
onDismissRequest: () -> Unit,
- properties: ModalExpandedNavigationRailProperties,
+ properties: ModalWideNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
predictiveBackState: RailPredictiveBackState,
@@ -793,11 +956,13 @@
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun ModalWideNavigationRailContent(
+ expanded: Boolean,
+ isStandaloneModal: Boolean,
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
predictiveBackState: RailPredictiveBackState,
settleToDismiss: suspend (velocity: Float) -> Unit,
modifier: Modifier,
- railState: ModalExpandedNavigationRailState,
+ railState: DismissibleModalWideNavigationRailState,
colors: WideNavigationRailColors,
shape: Shape,
openModalRailMaxWidth: Dp,
@@ -812,7 +977,7 @@
Surface(
shape = shape,
- color = colors.expandedModalContainerColor,
+ color = colors.modalContainerColor,
modifier =
modifier
.widthIn(max = openModalRailMaxWidth)
@@ -841,11 +1006,16 @@
railSize,
_ ->
val width = railSize.width.toFloat()
- val minValue = if (isRtl) width else -width
+ val minValue =
+ if (isStandaloneModal) {
+ if (isRtl) width else -width
+ } else {
+ 0f
+ }
val maxValue = 0f
return@draggableAnchors DraggableAnchors {
- ModalExpandedNavigationRailValue.Closed at minValue
- ModalExpandedNavigationRailValue.Open at maxValue
+ DismissibleModalWideNavigationRailValue.Closed at minValue
+ DismissibleModalWideNavigationRailValue.Open at maxValue
} to railState.targetValue
}
.draggable(
@@ -877,7 +1047,7 @@
transformOrigin =
TransformOrigin(if (isRtl) 0f else 1f, PredictiveBackPivotFractionY)
},
- expanded = true,
+ expanded = expanded,
shape = shape,
colors = colors,
header = header,
@@ -976,5 +1146,4 @@
private val PredictiveBackMaxScaleYDistance = 48.dp
private const val PredictiveBackPivotFractionY = 0.5f
-private const val PredictiveBackPivotFractionYScaleDown = 0f
private const val HeaderLayoutIdTag: String = "header"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
index aa83fb2..1b0234f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRailState.kt
@@ -34,17 +34,17 @@
import androidx.compose.ui.unit.dp
@ExperimentalMaterial3ExpressiveApi
-/** Possible values of [ModalExpandedNavigationRailState]. */
-enum class ModalExpandedNavigationRailValue {
- /** The state of the modal expanded navigation rail when it is closed. */
+/** Possible values of [DismissibleModalWideNavigationRailState]. */
+enum class DismissibleModalWideNavigationRailValue {
+ /** The state of the dismissible modal wide navigation rail when it is closed. */
Closed,
- /** The state of the modal expanded navigation rail when it is open. */
+ /** The state of the dismissible modal wide navigation rail when it is open. */
Open,
}
/**
- * State of a modal expanded navigation rail, such as [ModalExpandedNavigationRail].
+ * State of a dismissible modal wide navigation rail, such as [DismissibleModalWideNavigationRail].
*
* Contains states relating to its swipe position as well as animations between state values.
*
@@ -55,11 +55,11 @@
*/
@Suppress("NotCloseable")
@ExperimentalMaterial3ExpressiveApi
-class ModalExpandedNavigationRailState(
- var initialValue: ModalExpandedNavigationRailValue,
+class DismissibleModalWideNavigationRailState(
+ var initialValue: DismissibleModalWideNavigationRailValue,
density: Density,
val animationSpec: AnimationSpec<Float>,
- var confirmValueChange: (ModalExpandedNavigationRailValue) -> Boolean = { true },
+ var confirmValueChange: (DismissibleModalWideNavigationRailValue) -> Boolean = { true },
) {
internal val anchoredDraggableState =
AnchoredDraggableState(
@@ -73,55 +73,55 @@
/**
* The current value of the state.
*
- * If no swipe or animation is in progress, this corresponds to the value the modal expanded
- * navigation rail is currently in. If a swipe or an animation is in progress, this corresponds
- * to the value the rail was in before the swipe or animation started.
+ * If no swipe or animation is in progress, this corresponds to the value the dismissible modal
+ * wide navigation rail is currently in. If a swipe or an animation is in progress, this
+ * corresponds to the value the rail was in before the swipe or animation started.
*/
- val currentValue: ModalExpandedNavigationRailValue
+ val currentValue: DismissibleModalWideNavigationRailValue
get() = anchoredDraggableState.currentValue
/**
- * The target value of the modal expanded navigation rail state.
+ * The target value of the dismissible modal wide navigation rail state.
*
* If a swipe is in progress, this is the value that the modal rail will animate to if the swipe
* finishes. If an animation is running, this is the target value of that animation. Finally, if
* no swipe or animation is in progress, this is the same as the [currentValue].
*/
- val targetValue: ModalExpandedNavigationRailValue
+ val targetValue: DismissibleModalWideNavigationRailValue
get() = anchoredDraggableState.targetValue
- /** Whether the modal expanded navigation rail is open. */
+ /** Whether the dismissible modal wide navigation rail is open. */
val isOpen: Boolean
- get() = currentValue != ModalExpandedNavigationRailValue.Closed
+ get() = currentValue != DismissibleModalWideNavigationRailValue.Closed
/** Whether the state is currently animating. */
val isAnimationRunning: Boolean
get() = anchoredDraggableState.isAnimationRunning
/**
- * Open the modal expanded navigation rail with animation and suspend until it if fully open or
- * the animation has been cancelled. This method will throw CancellationException if the
+ * Open the dismissible modal wide navigation rail with animation and suspend until it if fully
+ * open or the animation has been cancelled. This method will throw CancellationException if the
* animation is interrupted.
*
* @return the reason the expand animation ended
*/
- suspend fun open() = animateTo(ModalExpandedNavigationRailValue.Open)
+ suspend fun open() = animateTo(DismissibleModalWideNavigationRailValue.Open)
/**
- * Close the modal expanded navigation rail with animation and suspend until it is fully closed
- * or the animation has been cancelled. This method will throw CancellationException if the
- * animation interrupted.
+ * Close the dismissible modal wide navigation rail with animation and suspend until it is fully
+ * closed or the animation has been cancelled. This method will throw CancellationException if
+ * the animation interrupted.
*
* @return the reason the collapse animation ended
*/
- suspend fun close() = animateTo(ModalExpandedNavigationRailValue.Closed)
+ suspend fun close() = animateTo(DismissibleModalWideNavigationRailValue.Closed)
/**
* Set the state without any animation and suspend until it's set.
*
* @param targetValue The new target value
*/
- suspend fun snapTo(targetValue: ModalExpandedNavigationRailValue) {
+ suspend fun snapTo(targetValue: DismissibleModalWideNavigationRailValue) {
anchoredDraggableState.snapTo(targetValue)
}
@@ -141,7 +141,7 @@
get() = anchoredDraggableState.offset
private suspend fun animateTo(
- targetValue: ModalExpandedNavigationRailValue,
+ targetValue: DismissibleModalWideNavigationRailValue,
animationSpec: AnimationSpec<Float> = this.animationSpec,
velocity: Float = anchoredDraggableState.lastVelocity
) {
@@ -162,39 +162,49 @@
}
companion object {
- /** The default [Saver] implementation for [ModalExpandedNavigationRailState]. */
+ /** The default [Saver] implementation for [DismissibleModalWideNavigationRailState]. */
fun Saver(
density: Density,
animationSpec: AnimationSpec<Float>,
- confirmStateChange: (ModalExpandedNavigationRailValue) -> Boolean
+ confirmStateChange: (DismissibleModalWideNavigationRailValue) -> Boolean
) =
- Saver<ModalExpandedNavigationRailState, ModalExpandedNavigationRailValue>(
+ Saver<DismissibleModalWideNavigationRailState, DismissibleModalWideNavigationRailValue>(
save = { it.currentValue },
restore = {
- ModalExpandedNavigationRailState(it, density, animationSpec, confirmStateChange)
+ DismissibleModalWideNavigationRailState(
+ it,
+ density,
+ animationSpec,
+ confirmStateChange
+ )
}
)
}
}
/**
- * Create and [remember] a [ModalExpandedNavigationRailState].
+ * Create and [remember] a [DismissibleModalWideNavigationRailState].
*
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun rememberModalExpandedNavigationRailState(
- confirmValueChange: (ModalExpandedNavigationRailValue) -> Boolean = { true }
-): ModalExpandedNavigationRailState {
+fun rememberDismissibleModalWideNavigationRailState(
+ confirmValueChange: (DismissibleModalWideNavigationRailValue) -> Boolean = { true }
+): DismissibleModalWideNavigationRailState {
val density = LocalDensity.current
// TODO: Load the motionScheme tokens from the component tokens file.
val animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value<Float>()
return rememberSaveable(
- saver = ModalExpandedNavigationRailState.Saver(density, animationSpec, confirmValueChange)
+ saver =
+ DismissibleModalWideNavigationRailState.Saver(
+ density,
+ animationSpec,
+ confirmValueChange
+ )
) {
- ModalExpandedNavigationRailState(
- initialValue = ModalExpandedNavigationRailValue.Closed,
+ DismissibleModalWideNavigationRailState(
+ initialValue = DismissibleModalWideNavigationRailValue.Closed,
density = density,
animationSpec = animationSpec,
confirmValueChange = confirmValueChange
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..1bf6f05 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
if (anchors.hasAnchorFor(targetValue)) {
try {
dragMutex.mutate(dragPriority) {
- dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+ dragTarget = targetValue
restartable(inputs = { anchors to [email protected] }) {
(latestAnchors, latestTarget) ->
anchoredDragScope.block(latestAnchors, latestTarget)
@@ -856,7 +856,14 @@
} else state.requireOffset()
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
- placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ }
}
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index 639db61..ba0cb07 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -65,10 +65,10 @@
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.rotate
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
@@ -390,7 +390,7 @@
*/
@ExperimentalMaterial3ExpressiveApi
val loadingIndicatorContainerColor: Color
- @Composable get() = LoadingIndicatorDefaults.ContainedContainerColor
+ @Composable get() = LoadingIndicatorDefaults.containedContainerColor
/** The default indicator color for [Indicator] */
@Deprecated("Use loadingIndicatorColor instead", ReplaceWith("loadingIndicatorColor"))
@@ -403,14 +403,17 @@
*/
@ExperimentalMaterial3ExpressiveApi
val loadingIndicatorColor: Color
- @Composable get() = LoadingIndicatorDefaults.ContainedIndicatorColor
+ @Composable get() = LoadingIndicatorDefaults.containedIndicatorColor
/** The default refresh threshold for [rememberPullToRefreshState] */
val PositionalThreshold = 80.dp
- /** The default elevation for [IndicatorBox] */
+ /** The default elevation for an [IndicatorBox] that is applied to an [Indicator] */
val Elevation = ElevationTokens.Level2
+ /** The default elevation for an [IndicatorBox] that is applied to a [LoadingIndicator] */
+ val LoadingIndicatorElevation = ElevationTokens.Level0
+
/**
* A Wrapper that handles the size, offset, clipping, shadow, and background drawing for a
* pull-to-refresh indicator, useful when implementing custom indicators.
@@ -452,12 +455,22 @@
[email protected]()
}
}
- .graphicsLayer {
- val showElevation = state.distanceFraction > 0f || isRefreshing
- translationY = state.distanceFraction * threshold.roundToPx() - size.height
- shadowElevation = if (showElevation) elevation.toPx() else 0f
- this.shape = shape
- clip = true
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.placeWithLayer(
+ 0,
+ 0,
+ layerBlock = {
+ val showElevation = state.distanceFraction > 0f || isRefreshing
+ translationY =
+ state.distanceFraction * threshold.roundToPx() - size.height
+ shadowElevation = if (showElevation) elevation.toPx() else 0f
+ this.shape = shape
+ clip = true
+ }
+ )
+ }
}
.background(color = containerColor, shape = shape),
contentAlignment = Alignment.Center,
@@ -515,7 +528,18 @@
}
}
- /** A [LoadingIndicator] indicator for [PullToRefreshBox]. */
+ /**
+ * A [LoadingIndicator] indicator for [PullToRefreshBox].
+ *
+ * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
+ * calculate the offset
+ * @param isRefreshing whether a refresh is occurring
+ * @param modifier the modifier applied to this layout
+ * @param containerColor the container color of this indicator
+ * @param color the color of this indicator
+ * @param elevation the elevation of this indicator
+ * @param threshold how much the indicator can be pulled down before a refresh is triggered on
+ */
@ExperimentalMaterial3ExpressiveApi
@Composable
fun LoadingIndicator(
@@ -524,7 +548,7 @@
modifier: Modifier = Modifier,
containerColor: Color = this.loadingIndicatorContainerColor,
color: Color = this.loadingIndicatorColor,
- elevation: Dp = ElevationTokens.Level0,
+ elevation: Dp = LoadingIndicatorElevation,
threshold: Dp = PositionalThreshold
) {
IndicatorBox(
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
index 7c960c0..e218493 100644
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
+++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/WideNavigationRail.commonStubs.kt
@@ -21,22 +21,22 @@
@Immutable
@ExperimentalMaterial3ExpressiveApi
-actual class ModalExpandedNavigationRailProperties
+actual class ModalWideNavigationRailProperties
actual constructor(
actual val shouldDismissOnBackPress: Boolean,
)
@Immutable
@ExperimentalMaterial3ExpressiveApi
-actual object ModalExpandedNavigationRailDefaults {
- actual val Properties: ModalExpandedNavigationRailProperties = implementedInJetBrainsFork()
+actual object DismissibleModalWideNavigationRailDefaults {
+ actual val Properties: ModalWideNavigationRailProperties = implementedInJetBrainsFork()
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal actual fun ModalWideNavigationRailDialog(
onDismissRequest: () -> Unit,
- properties: ModalExpandedNavigationRailProperties,
+ properties: ModalWideNavigationRailProperties,
onPredictiveBack: (Float) -> Unit,
onPredictiveBackCancelled: () -> Unit,
predictiveBackState: RailPredictiveBackState,
diff --git a/compose/runtime/runtime-lint/build.gradle b/compose/runtime/runtime-lint/build.gradle
index f01441a..d0426fb 100644
--- a/compose/runtime/runtime-lint/build.gradle
+++ b/compose/runtime/runtime-lint/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index d82bd53..d42b4b5 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -39,7 +39,7 @@
api("androidx.lifecycle:lifecycle-runtime:2.6.1")
api("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
androidTestImplementation(libs.testRunner)
@@ -53,7 +53,7 @@
inceptionYear = "2020"
description = "Compose integration with LiveData"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
+ samples(project(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
}
android {
diff --git a/compose/runtime/runtime-livedata/samples/build.gradle b/compose/runtime/runtime-livedata/samples/build.gradle
index 80ae202..12e5ab5 100644
--- a/compose/runtime/runtime-livedata/samples/build.gradle
+++ b/compose/runtime/runtime-livedata/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-livedata"))
+ implementation(project(":compose:runtime:runtime-livedata"))
}
androidx {
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index 5750ab4..1865745 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -38,7 +38,7 @@
api(project(":compose:runtime:runtime"))
api(libs.rxjava2)
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
@@ -51,7 +51,7 @@
inceptionYear = "2020"
description = "Compose integration with RxJava 2"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
+ samples(project(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
}
android {
diff --git a/compose/runtime/runtime-rxjava2/samples/build.gradle b/compose/runtime/runtime-rxjava2/samples/build.gradle
index 7360519..ba7d0b8 100644
--- a/compose/runtime/runtime-rxjava2/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava2/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-rxjava2"))
+ implementation(project(":compose:runtime:runtime-rxjava2"))
}
androidx {
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index 7aac897..af420bf 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -38,7 +38,7 @@
api(project(":compose:runtime:runtime"))
api(libs.rxjava3)
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
@@ -51,7 +51,7 @@
inceptionYear = "2020"
description = "Compose integration with RxJava 3"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
+ samples(project(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
}
android {
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index 61a0015..9906d22 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -36,7 +36,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.compose.material:material:1.2.1")
- implementation(projectOrArtifact(":compose:runtime:runtime-rxjava3"))
+ implementation(project(":compose:runtime:runtime-rxjava3"))
}
androidx {
diff --git a/compose/runtime/runtime-saveable-lint/build.gradle b/compose/runtime/runtime-saveable-lint/build.gradle
index 24676c6..e911913 100644
--- a/compose/runtime/runtime-saveable-lint/build.gradle
+++ b/compose/runtime/runtime-saveable-lint/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index 62b0876..f5ee280 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -34,7 +34,7 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime"))
implementation kotlin("test")
implementation(libs.kotlinCoroutinesTest)
implementation(libs.kotlinReflect)
diff --git a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 4b1b684..e92c777 100644
--- a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -20,6 +20,8 @@
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
class ViewApplier(root: View) : AbstractApplier<View>(root) {
+ var called = false
+
var onBeginChangesCalled = 0
private set
@@ -28,29 +30,53 @@
override fun insertTopDown(index: Int, instance: View) {
// Ignored as the tree is built bottom-up.
+ called = true
}
override fun insertBottomUp(index: Int, instance: View) {
current.addAt(index, instance)
+ called = true
}
override fun remove(index: Int, count: Int) {
current.removeAt(index, count)
+ called = true
}
override fun move(from: Int, to: Int, count: Int) {
current.moveAt(from, to, count)
+ called = true
}
override fun onClear() {
root.removeAllChildren()
+ called = true
}
override fun onBeginChanges() {
onBeginChangesCalled++
+ called = true
}
override fun onEndChanges() {
onEndChangesCalled++
+ called = true
+ }
+
+ override var current: View
+ get() = super.current.also { if (it != root) called = true }
+ set(value) {
+ super.current = value
+ called = true
+ }
+
+ override fun down(node: View) {
+ super.down(node)
+ called = true
+ }
+
+ override fun up() {
+ super.up()
+ called = true
}
}
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index fc9bb64..f02ef5a 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -1,19 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 110569b..9212ade 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -22,6 +22,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -31,6 +32,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -286,6 +288,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -459,6 +462,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -468,6 +480,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index 5826792..f02ef5a 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -1,23 +1,3 @@
// Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
- Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
- Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
- Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
-
-
-RemovedClass: androidx.compose.runtime.ExpectKt:
- Removed class androidx.compose.runtime.ExpectKt
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+ Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 186ecc4..9580ec3 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -26,6 +26,7 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+ method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
method public void clear();
method public void down(N node);
method public N getCurrent();
@@ -35,6 +36,7 @@
method public default void onBeginChanges();
method public default void onEndChanges();
method public void remove(int index, int count);
+ method public default void reuse();
method public void up();
property public abstract N current;
}
@@ -313,6 +315,7 @@
method public void recordModificationsOf(java.util.Set<?> values);
method public void recordReadOf(Object value);
method public void recordWriteOf(Object value);
+ method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
property public abstract boolean hasPendingChanges;
property public abstract boolean isComposing;
@@ -487,6 +490,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
}
+ public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+ method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
+ public final class PausableCompositionKt {
+ method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+ }
+
public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
method public boolean isPaused();
@@ -496,6 +508,14 @@
property public final boolean isPaused;
}
+ public interface PausedComposition {
+ method public void apply();
+ method public void cancel();
+ method public boolean isComplete();
+ method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+ property public abstract boolean isComplete;
+ }
+
public final class PrimitiveSnapshotStateKt {
method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index 65f7a91..e5794f5 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -28,15 +28,15 @@
dependencies {
- androidTestImplementation(projectOrArtifact(":compose:ui:ui"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
- androidTestImplementation(projectOrArtifact(":compose:foundation:foundation"))
- androidTestImplementation(projectOrArtifact(":compose:foundation:foundation-layout"))
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
- androidTestImplementation(projectOrArtifact(":compose:runtime:runtime"))
- androidTestImplementation(projectOrArtifact(":compose:runtime:runtime-saveable"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-text"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-util"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:runtime:runtime-saveable"))
+ androidTestImplementation(project(":compose:ui:ui-text"))
+ androidTestImplementation(project(":compose:ui:ui-util"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:benchmark-utils"))
@@ -48,9 +48,9 @@
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinReflect)
androidTestImplementation(libs.kotlinCoroutinesTest)
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation("androidx.activity:activity:1.2.0")
- androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
+ androidTestImplementation(project(":activity:activity-compose"))
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
package androidx.compose.runtime.benchmark
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
@@ -30,8 +31,13 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
measureComposeFocused { repeat(100) { Rect() } }
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+ measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+ }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+ layout(constraints.minWidth, constraints.minHeight) {}
}
class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
}
@Composable
+private fun SimpleText(text: String) {
+ val measurer = rememberTextMeasurer()
+ Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
private fun Rect(color: Color) {
val modifier = remember(color) { Modifier.background(color) }
Column(modifier) {}
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index f4785bb..233a8eb 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -37,7 +37,7 @@
dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(libs.kotlinCoroutinesCore)
- implementation(projectOrArtifact(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui"))
}
}
@@ -74,11 +74,11 @@
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui"))
- implementation(projectOrArtifact(":compose:material:material"))
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
- implementation(projectOrArtifact(":activity:activity-compose"))
+ implementation(project(":activity:activity-compose"))
implementation(libs.testExtJunit)
implementation(libs.testRules)
implementation(libs.testRunner)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index e1cd951..5b33661 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -174,6 +174,16 @@
* root to be used as the target of a new composition in the future.
*/
fun clear()
+
+ /** Apply a change to the current node. */
+ fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ current.block(value)
+ }
+
+ /** Notify [current] is is being reused in reusable content. */
+ fun reuse() {
+ (current as? ComposeNodeLifecycleCallback)?.onReuse()
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 28f2850..bdde575 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -101,6 +101,15 @@
priority: Int,
endRelativeAfter: Int
)
+
+ /** The restart scope is pausing */
+ fun rememberPausingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is resuming */
+ fun startResumingScope(scope: RecomposeScopeImpl)
+
+ /** The restart scope is finished resuming */
+ fun endResumingScope(scope: RecomposeScopeImpl)
}
/**
@@ -1356,6 +1365,9 @@
private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
private var insertFixups = FixupList()
+ private var pausable: Boolean = false
+ private var shouldPauseCallback: (() -> Boolean)? = null
+
override val applyCoroutineContext: CoroutineContext
@TestOnly get() = parentContext.effectCoroutineContext
@@ -2726,7 +2738,10 @@
providerCache = null
// Invoke the scope's composition function
+ val shouldRestartReusing = !reusing && firstInRange.scope.reusing
+ if (shouldRestartReusing) reusing = true
firstInRange.scope.compose(this)
+ if (shouldRestartReusing) reusing = false
// We could have moved out of a provider so the provider cache is invalid.
providerCache = null
@@ -3038,8 +3053,34 @@
}
@ComposeCompilerApi
- @Suppress("UNUSED")
override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
+ // We only want to pause when we are not resuming and only when inserting new content or
+ // when reusing content. This 0 bit of `flags` is only 1 if this function was restarted by
+ // the restart lambda. The other bits of this flags are currently all 0's and are reserved
+ // for future use.
+ if (((flags and 1) == 0) && (inserting || reusing)) {
+ val callback = shouldPauseCallback ?: return true
+ val scope = currentRecomposeScope ?: return true
+ val pausing = callback()
+ if (pausing) {
+ scope.used = true
+ // Force the composer back into the reusing state when this scope restarts.
+ scope.reusing = reusing
+ scope.paused = true
+ // Remember a place-holder object to ensure all remembers are sent in the correct
+ // order. The remember manager will record the remember callback for the resumed
+ // content into a place-holder to ensure that, when the remember callbacks are
+ // dispatched, the callbacks for the resumed content are dispatched in the same
+ // order they would have been had the content not paused.
+ changeListWriter.rememberPausingScope(scope)
+ parentContext.reportPausedScope(scope)
+ return false
+ }
+ return true
+ }
+
+ // Otherwise we should execute the function if the parameters have changed or when
+ // skipping is disabled.
return parametersChanged || !skipping
}
@@ -3118,6 +3159,11 @@
}
invalidateStack.push(scope)
scope.start(compositionToken)
+ if (scope.paused) {
+ scope.paused = false
+ scope.resuming = true
+ changeListWriter.startResumingScope(scope)
+ }
}
}
@@ -3133,8 +3179,16 @@
// exception stack unwinding that might have not called the doneJoin/endRestartGroup in the
// the correct order.
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop() else null
- scope?.requiresRecompose = false
- scope?.end(compositionToken)?.let { changeListWriter.endCompositionScope(it, composition) }
+ if (scope != null) {
+ scope.requiresRecompose = false
+ scope.end(compositionToken)?.let {
+ changeListWriter.endCompositionScope(it, composition)
+ }
+ if (scope.resuming) {
+ scope.resuming = false
+ changeListWriter.endResumingScope(scope)
+ }
+ }
val result =
if (scope != null && !scope.skipped && (scope.used || forceRecomposeScopes)) {
if (scope.anchor == null) {
@@ -3438,10 +3492,16 @@
*/
internal fun composeContent(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
+ shouldPause: (() -> Boolean)?
) {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
- doCompose(invalidationsRequested, content)
+ this.shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, content)
+ } finally {
+ this.shouldPauseCallback = null
+ }
}
internal fun prepareCompose(block: () -> Unit) {
@@ -3460,6 +3520,7 @@
*/
internal fun recompose(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
+ shouldPause: (() -> Boolean)?
): Boolean {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
// even if invalidationsRequested is empty we still need to recompose if the Composer has
@@ -3467,7 +3528,12 @@
// there were a change for a state which was used by the child composition. such changes
// will be tracked and added into `invalidations` list.
if (invalidationsRequested.size > 0 || invalidations.isNotEmpty() || forciblyRecompose) {
- doCompose(invalidationsRequested, null)
+ shouldPauseCallback = shouldPause
+ try {
+ doCompose(invalidationsRequested, null)
+ } finally {
+ shouldPauseCallback = null
+ }
return changes.isNotEmpty()
}
return false
@@ -3786,6 +3852,10 @@
parentContext.unregisterComposition(composition)
}
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ parentContext.reportPausedScope(scope)
+ }
+
override val effectCoroutineContext: CoroutineContext
get() = parentContext.effectCoroutineContext
@@ -3802,6 +3872,20 @@
parentContext.composeInitial(composition, content)
}
+ override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.composeInitialPaused(composition, shouldPause, content)
+
+ override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> =
+ parentContext.recomposePaused(composition, shouldPause, invalidScopes)
+
override fun invalidate(composition: ControlledComposition) {
// Invalidate ourselves with our parent before we invalidate a child composer.
// This ensures that when we are scheduling recompositions, parents always
@@ -4118,45 +4202,40 @@
// To ensure this order, we call `enters` as a pre-order traversal
// of the group tree, and then call `leaves` in the inverse order.
- val start = currentGroup
- val end = currentGroupEnd
- for (group in start until end) {
- val node = node(group)
- if (node is ComposeNodeLifecycleCallback) {
- val endRelativeOrder = slotsSize - slotsStartIndex(group)
- rememberManager.deactivating(node, endRelativeOrder, -1, -1)
- }
-
- forEachData(group) { slotIndex, data ->
- when (data) {
- is RememberObserverHolder -> {
- val wrapped = data.wrapped
- if (wrapped is ReusableRememberObserver) {
- // do nothing, the value should be preserved on reuse
- } else {
- removeData(group, slotIndex, data)
- val endRelativeOrder = slotsSize - slotIndex
- withAfterAnchorInfo(data.after) { priority, endRelativeAfter ->
- rememberManager.forgetting(
- wrapped,
- endRelativeOrder,
- priority,
- endRelativeAfter
- )
- }
+ forAllData(currentGroup) { slotIndex, data ->
+ when (data) {
+ is ComposeNodeLifecycleCallback -> {
+ val endRelativeOrder = slotsSize - slotIndex
+ rememberManager.deactivating(data, endRelativeOrder, -1, -1)
+ }
+ is RememberObserverHolder -> {
+ val wrapped = data.wrapped
+ if (wrapped is ReusableRememberObserver) {
+ // do nothing, the value should be preserved on reuse
+ } else {
+ removeData(slotIndex, data)
+ val endRelativeOrder = slotsSize - slotIndex
+ withAfterAnchorInfo(data.after) { priority, endRelativeAfter ->
+ rememberManager.forgetting(
+ wrapped,
+ endRelativeOrder,
+ priority,
+ endRelativeAfter
+ )
}
}
- is RecomposeScopeImpl -> {
- removeData(group, slotIndex, data)
- data.release()
- }
+ }
+ is RecomposeScopeImpl -> {
+ removeData(slotIndex, data)
+ data.release()
}
}
}
}
-private fun SlotWriter.removeData(group: Int, index: Int, data: Any?) {
- runtimeCheck(data === set(group, index, Composer.Empty)) { "Slot table is out of sync" }
+private fun SlotWriter.removeData(index: Int, data: Any?) {
+ val result = clear(index)
+ runtimeCheck(data === result) { "Slot table is out of sync (expected $data, got $result)" }
}
@JvmInline
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index e2efd82..f674dbe 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -18,13 +18,12 @@
package androidx.compose.runtime
-import androidx.collection.MutableIntList
import androidx.collection.MutableScatterSet
-import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.changelist.ChangeList
import androidx.compose.runtime.collection.ScopeMap
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.RememberEventDispatcher
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.ReaderKind
import androidx.compose.runtime.snapshots.StateObjectImpl
@@ -289,6 +288,29 @@
* used to compose as if the scopes have already been changed.
*/
fun <R> delegateInvalidations(to: ControlledComposition?, groupIndex: Int, block: () -> R): R
+
+ /**
+ * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`.
+ * Setting the callback to `null` disables pausing.
+ *
+ * @return the previous value of the callback which will be restored once the callback is no
+ * longer needed.
+ * @see PausableComposition
+ */
+ fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)?
+}
+
+/** Utility function to set and restore a should pause callback. */
+internal inline fun <R> ControlledComposition.pausable(
+ noinline shouldPause: () -> Boolean,
+ block: () -> R
+): R {
+ val previous = setShouldPauseCallback(shouldPause)
+ return try {
+ block()
+ } finally {
+ setShouldPauseCallback(previous)
+ }
}
/**
@@ -409,7 +431,12 @@
/** The applier to use to update the tree managed by the composition. */
private val applier: Applier<*>,
recomposeContext: CoroutineContext? = null
-) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices {
+) :
+ ControlledComposition,
+ ReusableComposition,
+ RecomposeScopeOwner,
+ CompositionServices,
+ PausableComposition {
/**
* `null` if a composition isn't pending to apply. `Set<Any>` or `Array<Set<Any>>` if there are
* modifications to record [PendingApplyNoModifications] if a composition is pending to apply,
@@ -520,6 +547,14 @@
@Suppress("MemberVisibilityCanBePrivate") // published as internal
internal var pendingInvalidScopes = false
+ /**
+ * If the [shouldPause] callback is set the composition is pausable and should pause whenever
+ * the [shouldPause] callback returns `true`.
+ */
+ private var shouldPause: (() -> Boolean)? = null
+
+ private var pendingPausedComposition: PausedCompositionImpl? = null
+
private var invalidationDelegate: CompositionImpl? = null
private var invalidationDelegateGroup: Int = 0
@@ -572,10 +607,16 @@
get() = synchronized(lock) { composer.hasPendingChanges }
override fun setContent(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composeInitial(content)
}
override fun setContentWithReuse(content: @Composable () -> Unit) {
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
composer.startReuseFromRoot()
composeInitial(content)
@@ -583,6 +624,50 @@
composer.endReuseFromRoot()
}
+ override fun setPausableContent(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = false,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition {
+ checkPrecondition(!disposed) { "The composition is disposed" }
+ checkPrecondition(pendingPausedComposition == null) {
+ "A pausable composition is in progress"
+ }
+ val pausedComposition =
+ PausedCompositionImpl(
+ composition = this,
+ context = parent,
+ composer = composer,
+ content = content,
+ reusable = true,
+ abandonSet = abandonSet,
+ applier = applier,
+ lock = lock,
+ )
+ pendingPausedComposition = pausedComposition
+ return pausedComposition
+ }
+
+ internal fun pausedCompositionFinished() {
+ pendingPausedComposition = null
+ }
+
private fun composeInitial(content: @Composable () -> Unit) {
checkPrecondition(!disposed) { "The composition is disposed" }
this.composable = content
@@ -701,7 +786,7 @@
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
}
- composer.composeContent(invalidations, content)
+ composer.composeContent(invalidations, content, shouldPause)
observer?.onEndComposition(this)
}
}
@@ -911,7 +996,7 @@
this,
invalidations.asMap() as Map<RecomposeScope, Set<Any>>
)
- composer.recompose(invalidations).also { shouldDrain ->
+ composer.recompose(invalidations, shouldPause).also { shouldDrain ->
// Apply would normally do this for us; do it now if apply shouldn't happen.
if (!shouldDrain) drainPendingModificationsLocked()
observer?.onEndComposition(this)
@@ -939,11 +1024,13 @@
try {
if (changes.isEmpty()) return
trace("Compose:applyChanges") {
+ val applier = pendingPausedComposition?.pausableApplier ?: applier
+ val rememberManager = pendingPausedComposition?.rememberManager ?: manager
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
- changes.executeAndFlushAllPendingChanges(applier, slots, manager)
+ changes.executeAndFlushAllPendingChanges(applier, slots, rememberManager)
}
applier.onEndChanges()
}
@@ -962,9 +1049,12 @@
}
}
} finally {
- // Only dispatch abandons if we do not have any late changes. The instances in the
- // abandon set can be remembered in the late changes.
- if (this.lateChanges.isEmpty()) manager.dispatchAbandons()
+ // Only dispatch abandons if we do not have any late changes or pending paused
+ // compositions. The instances in the abandon set can be remembered in the late changes
+ // or when the paused composition is applied.
+ if (this.lateChanges.isEmpty() && pendingPausedComposition == null) {
+ manager.dispatchAbandons()
+ }
}
}
@@ -1062,6 +1152,12 @@
} else block()
}
+ override fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)? {
+ val previous = this.shouldPause
+ this.shouldPause = shouldPause
+ return previous
+ }
+
override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
if (scope.defaultsInScope) {
scope.defaultsInvalid = true
@@ -1241,218 +1337,6 @@
// This is only used in tests to ensure the stacks do not silently leak.
internal fun composerStacksSizes(): Int = composer.stacksSize()
-
- /** Helper for collecting remember observers for later strictly ordered dispatch. */
- private class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
- RememberManager {
- private val remembering = mutableListOf<RememberObserver>()
- private val leaving = mutableListOf<Any>()
- private val sideEffects = mutableListOf<() -> Unit>()
- private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
- private val pending = mutableListOf<Any>()
- private val priorities = MutableIntList()
- private val afters = MutableIntList()
-
- override fun remembering(instance: RememberObserver) {
- remembering.add(instance)
- }
-
- override fun forgetting(
- instance: RememberObserver,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun sideEffect(effect: () -> Unit) {
- sideEffects += effect
- }
-
- override fun deactivating(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- override fun releasing(
- instance: ComposeNodeLifecycleCallback,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- val releasing =
- releasing
- ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
-
- releasing += instance
- recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
- }
-
- fun dispatchRememberObservers() {
- // Add any pending out-of-order forgotten objects
- processPendingLeaving(Int.MIN_VALUE)
-
- // Send forgets and node callbacks
- if (leaving.isNotEmpty()) {
- trace("Compose:onForgotten") {
- val releasing = releasing
- for (i in leaving.size - 1 downTo 0) {
- val instance = leaving[i]
- if (instance is RememberObserver) {
- abandoning.remove(instance)
- instance.onForgotten()
- }
- if (instance is ComposeNodeLifecycleCallback) {
- // node callbacks are in the same queue as forgets to ensure ordering
- if (releasing != null && instance in releasing) {
- instance.onRelease()
- } else {
- instance.onDeactivate()
- }
- }
- }
- }
- }
-
- // Send remembers
- if (remembering.isNotEmpty()) {
- trace("Compose:onRemembered") {
- remembering.fastForEach { instance ->
- abandoning.remove(instance)
- instance.onRemembered()
- }
- }
- }
- }
-
- fun dispatchSideEffects() {
- if (sideEffects.isNotEmpty()) {
- trace("Compose:sideeffects") {
- sideEffects.fastForEach { sideEffect -> sideEffect() }
- sideEffects.clear()
- }
- }
- }
-
- fun dispatchAbandons() {
- if (abandoning.isNotEmpty()) {
- trace("Compose:abandons") {
- val iterator = abandoning.iterator()
- // remove elements one by one to ensure that abandons will not be dispatched
- // second time in case [onAbandoned] throws.
- while (iterator.hasNext()) {
- val instance = iterator.next()
- iterator.remove()
- instance.onAbandoned()
- }
- }
- }
- }
-
- private fun recordLeaving(
- instance: Any,
- endRelativeOrder: Int,
- priority: Int,
- endRelativeAfter: Int
- ) {
- processPendingLeaving(endRelativeOrder)
- if (endRelativeAfter in 0 until endRelativeOrder) {
- pending.add(instance)
- priorities.add(priority)
- afters.add(endRelativeAfter)
- } else {
- leaving.add(instance)
- }
- }
-
- private fun processPendingLeaving(endRelativeOrder: Int) {
- if (pending.isNotEmpty()) {
- var index = 0
- var toAdd: MutableList<Any>? = null
- var toAddAfter: MutableIntList? = null
- var toAddPriority: MutableIntList? = null
- while (index < afters.size) {
- if (endRelativeOrder <= afters[index]) {
- val instance = pending.removeAt(index)
- val endRelativeAfter = afters.removeAt(index)
- val priority = priorities.removeAt(index)
-
- if (toAdd == null) {
- toAdd = mutableListOf(instance)
- toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
- toAddPriority = MutableIntList().also { it.add(priority) }
- } else {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
- toAdd.add(instance)
- toAddAfter.add(endRelativeAfter)
- toAddPriority.add(priority)
- }
- } else {
- index++
- }
- }
- if (toAdd != null) {
- toAddPriority as MutableIntList
- toAddAfter as MutableIntList
-
- // Sort the list into [after, -priority] order where it is ordered by after
- // in ascending order as the primary key and priority in descending order as
- // secondary key.
-
- // For example if remember occurs after a child group it must be added after
- // all the remembers of the child. This is reported with an after which is the
- // slot index of the child's last slot. As this slot might be at the same
- // location as where its parents ends this would be ambiguous which should
- // first if both the two groups request a slot to be after the same slot.
- // Priority is used to break the tie here which is the group index of the group
- // which is leaving. Groups that are lower must be added before the parent's
- // remember when they have the same after.
-
- // The sort must be stable as as consecutive remembers in the same group after
- // the same child will have the same after and priority.
-
- // A selection sort is used here because it is stable and the groups are
- // typically very short so this quickly exit list of one and not loop for
- // for sizes of 2. As the information is split between three lists, to
- // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
- // an option to supply a custom swap.
- for (i in 0 until toAdd.size - 1) {
- for (j in i + 1 until toAdd.size) {
- val iAfter = toAddAfter[i]
- val jAfter = toAddAfter[j]
- if (
- iAfter < jAfter ||
- (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
- ) {
- toAdd.swap(i, j)
- toAddPriority.swap(i, j)
- toAddAfter.swap(i, j)
- }
- }
- }
- leaving.addAll(toAdd)
- }
- }
- }
- }
-}
-
-private fun <T> MutableList<T>.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
-}
-
-private fun MutableIntList.swap(a: Int, b: Int) {
- val item = this[a]
- this[a] = this[b]
- this[b] = item
}
internal object ScopeInvalidated
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 561890c..e5b6d6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -16,6 +16,7 @@
package androidx.compose.runtime
+import androidx.collection.ScatterSet
import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.tooling.CompositionData
import kotlin.coroutines.CoroutineContext
@@ -52,6 +53,20 @@
content: @Composable () -> Unit
)
+ internal abstract fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl>
+
+ internal abstract fun reportPausedScope(scope: RecomposeScopeImpl)
+
internal abstract fun invalidate(composition: ControlledComposition)
internal abstract fun invalidateScope(scope: RecomposeScopeImpl)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
new file mode 100644
index 0000000..0eff8d6
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(InternalComposeApi::class)
+
+package androidx.compose.runtime
+
+import androidx.collection.emptyScatterSet
+import androidx.collection.mutableIntListOf
+import androidx.collection.mutableObjectListOf
+import androidx.compose.runtime.internal.RememberEventDispatcher
+
+/**
+ * A [PausableComposition] is a sub-composition that can be composed incrementally as it supports
+ * being paused and resumed.
+ *
+ * Pausable sub-composition can be used between frames to prepare a sub-composition before it is
+ * required by the main composition. For example, this is used in lazy lists to prepare list items
+ * in between frames to that are likely to be scrolled in. The composition is paused when the start
+ * of the next frame is near allowing composition to be spread across multiple frames without
+ * delaying the production of the next frame.
+ *
+ * The result of the composition should not be used (e.g. the nodes should not added to a layout
+ * tree or placed in layout) until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called. The composition is incomplete and will not
+ * automatically recompose until after [PausedComposition.apply] is called.
+ *
+ * A [PausableComposition] is a [ReusableComposition] but [setPausableContent] should be used
+ * instead of [ReusableComposition.setContentWithReuse] to create a paused composition.
+ *
+ * If [Composition.setContent] or [ReusableComposition.setContentWithReuse] are used then the
+ * composition behaves as if it wasn't pausable. If there is a [PausedComposition] that has not yet
+ * been applied, an exception is thrown.
+ *
+ * @see Composition
+ * @see ReusableComposition
+ */
+interface PausableComposition : ReusableComposition {
+ /**
+ * Set the content of the composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContent(content: @Composable () -> Unit): PausedComposition
+
+ /**
+ * Set the content of a resuable composition. A [PausedComposition] that is currently paused. No
+ * composition is performed until [PausedComposition.resume] is called.
+ * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+ * The composition should not be used until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called.
+ *
+ * @see Composition.setContent
+ * @see ReusableComposition.setContentWithReuse
+ */
+ fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
+}
+
+/**
+ * [PausedComposition] is the result of calling [PausableComposition.setContent] or
+ * [PausableComposition.setContentWithReuse]. It is used to drive the paused composition to
+ * completion. A [PausedComposition] should not be used until [isComplete] is `true` and [apply] has
+ * been called.
+ *
+ * A [PausedComposition] is created paused and will only compose the `content` parameter when
+ * [resume] is called the first time.
+ */
+interface PausedComposition {
+ /**
+ * Returns `true` when the [PausedComposition] is complete. [isComplete] matches the last value
+ * returned from [resume]. Once a [PausedComposition] is [isComplete] the [apply] method should
+ * be called.
+ */
+ val isComplete: Boolean
+
+ /**
+ * Resume the composition that has been paused. This method should be called until [resume]
+ * returns `true` or [isComplete] is `true` which has the same result as the last result of
+ * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
+ * composition should be paused. For example, in lazy lists this returns `false` until just
+ * prior to the next frame starting in which it returns `true`
+ *
+ * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
+ * exception.
+ *
+ * @param shouldPause A lambda that is used to determine if the composition should be paused.
+ * This lambda is called often so should be a very simple calculation. Returning `true` does
+ * not guarantee the composition will pause, it should only be considered a request to pause
+ * the composition. Not all composable functions are pausable and only pausable composition
+ * functions will pause.
+ * @return `true` if the composition is complete and `false` if one or more calls to `resume`
+ * are required to complete composition.
+ */
+ fun resume(shouldPause: () -> Boolean): Boolean
+
+ /**
+ * Apply the composition. This is the last step of a paused composition and is required to be
+ * called prior to the composition is usable.
+ */
+ fun apply()
+
+ /**
+ * Cancels the paused composition. This should only be used if the composition is going to be
+ * disposed and the entire composition is not going to be used.
+ */
+ fun cancel()
+}
+
+/**
+ * Create a [PausableComposition]. A [PausableComposition] can create a [PausedComposition] which
+ * allows pausing and resuming the composition.
+ *
+ * @param applier The [Applier] instance to be used in the composition.
+ * @param parent The parent [CompositionContext].
+ * @see Applier
+ * @see CompositionContext
+ * @see PausableComposition
+ */
+fun PausableComposition(applier: Applier<*>, parent: CompositionContext): PausableComposition =
+ CompositionImpl(parent, applier)
+
+internal enum class PausedCompositionState {
+ Invalid,
+ Cancelled,
+ InitialPending,
+ RecomposePending,
+ ApplyPending,
+ Applied,
+}
+
+internal class PausedCompositionImpl(
+ val composition: CompositionImpl,
+ val context: CompositionContext,
+ val composer: ComposerImpl,
+ abandonSet: MutableSet<RememberObserver>,
+ val content: @Composable () -> Unit,
+ val reusable: Boolean,
+ val applier: Applier<*>,
+ val lock: SynchronizedObject,
+) : PausedComposition {
+ private var state = PausedCompositionState.InitialPending
+ private var invalidScopes = emptyScatterSet<RecomposeScopeImpl>()
+ internal val rememberManager = RememberEventDispatcher(abandonSet)
+ internal val pausableApplier = RecordingApplier(applier.current)
+
+ override val isComplete: Boolean
+ get() = state >= PausedCompositionState.ApplyPending
+
+ override fun resume(shouldPause: () -> Boolean): Boolean {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending -> {
+ if (reusable) composer.startReuseFromRoot()
+ try {
+ invalidScopes =
+ context.composeInitialPaused(composition, shouldPause, content)
+ } finally {
+ if (reusable) composer.endReuseFromRoot()
+ }
+ state = PausedCompositionState.RecomposePending
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.RecomposePending -> {
+ invalidScopes = context.recomposePaused(composition, shouldPause, invalidScopes)
+ if (invalidScopes.isEmpty()) markComplete()
+ }
+ PausedCompositionState.ApplyPending ->
+ error("Pausable composition is complete and apply() should be applied")
+ PausedCompositionState.Applied -> error("The paused composition has been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ }
+ return isComplete
+ }
+
+ override fun apply() {
+ try {
+ when (state) {
+ PausedCompositionState.InitialPending,
+ PausedCompositionState.RecomposePending ->
+ error("The paused composition has not completed yet")
+ PausedCompositionState.ApplyPending -> {
+ applyChanges()
+ state = PausedCompositionState.Applied
+ }
+ PausedCompositionState.Applied ->
+ error("The paused composition has already been applied")
+ PausedCompositionState.Cancelled ->
+ error("The paused composition has been cancelled")
+ PausedCompositionState.Invalid ->
+ error("The paused composition is invalid because of a previous exception")
+ }
+ } catch (e: Exception) {
+ state = PausedCompositionState.Invalid
+ throw e
+ }
+ }
+
+ override fun cancel() {
+ state = PausedCompositionState.Cancelled
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+
+ private fun markComplete() {
+ state = PausedCompositionState.ApplyPending
+ }
+
+ private fun applyChanges() {
+ synchronized(lock) {
+ @Suppress("UNCHECKED_CAST")
+ try {
+ pausableApplier.playTo(applier as Applier<Any?>)
+ rememberManager.dispatchRememberObservers()
+ rememberManager.dispatchSideEffects()
+ } finally {
+ rememberManager.dispatchAbandons()
+ composition.pausedCompositionFinished()
+ }
+ }
+ }
+}
+
+internal class RecordingApplier<N>(root: N) : Applier<N> {
+ private val stack = mutableObjectListOf<N>()
+ private val operations = mutableIntListOf()
+ private val instances = mutableObjectListOf<Any?>()
+
+ override var current: N = root
+
+ override fun down(node: N) {
+ operations.add(DOWN)
+ instances.add(node)
+ stack.add(current)
+ current = node
+ }
+
+ override fun up() {
+ operations.add(UP)
+ current = stack.removeAt(stack.size - 1)
+ }
+
+ override fun remove(index: Int, count: Int) {
+ operations.add(REMOVE)
+ operations.add(index)
+ operations.add(count)
+ }
+
+ override fun move(from: Int, to: Int, count: Int) {
+ operations.add(MOVE)
+ operations.add(from)
+ operations.add(to)
+ operations.add(count)
+ }
+
+ override fun clear() {
+ operations.add(CLEAR)
+ }
+
+ override fun insertBottomUp(index: Int, instance: N) {
+ operations.add(INSERT_BOTTOM_UP)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun insertTopDown(index: Int, instance: N) {
+ operations.add(INSERT_TOP_DOWN)
+ operations.add(index)
+ instances.add(instance)
+ }
+
+ override fun apply(block: N.(Any?) -> Unit, value: Any?) {
+ operations.add(APPLY)
+ instances.add(block)
+ instances.add(value)
+ }
+
+ override fun reuse() {
+ operations.add(REUSE)
+ }
+
+ fun playTo(applier: Applier<N>) {
+ var currentOperation = 0
+ var currentInstance = 0
+ val operations = operations
+ val size = operations.size
+ val instances = instances
+ applier.onBeginChanges()
+ try {
+ while (currentOperation < size) {
+ val operation = operations[currentOperation++]
+ when (operation) {
+ UP -> {
+ applier.up()
+ }
+ DOWN -> {
+ @Suppress("UNCHECKED_CAST") val node = instances[currentInstance++] as N
+ applier.down(node)
+ }
+ REMOVE -> {
+ val index = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.remove(index, count)
+ }
+ MOVE -> {
+ val from = operations[currentOperation++]
+ val to = operations[currentOperation++]
+ val count = operations[currentOperation++]
+ applier.move(from, to, count)
+ }
+ CLEAR -> {
+ applier.clear()
+ }
+ INSERT_TOP_DOWN -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertTopDown(index, instance)
+ }
+ INSERT_BOTTOM_UP -> {
+ val index = operations[currentOperation++]
+
+ @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+ applier.insertBottomUp(index, instance)
+ }
+ APPLY -> {
+ @Suppress("UNCHECKED_CAST")
+ val block = instances[currentInstance++] as Any?.(Any?) -> Unit
+ val value = instances[currentInstance++]
+ applier.apply(block, value)
+ }
+ REUSE -> {
+ applier.reuse()
+ }
+ }
+ }
+ runtimeCheck(currentInstance == instances.size) { "Applier operation size mismatch" }
+ instances.clear()
+ operations.clear()
+ } finally {
+ applier.onEndChanges()
+ }
+ }
+
+ // These commands need to be an integer, not just a enum value, as they are stored along side
+ // the commands integer parameters, so the values are explicitly set.
+ companion object {
+ const val UP = 0
+ const val DOWN = UP + 1
+ const val REMOVE = DOWN + 1
+ const val MOVE = REMOVE + 1
+ const val CLEAR = MOVE + 1
+ const val INSERT_BOTTOM_UP = CLEAR + 1
+ const val INSERT_TOP_DOWN = INSERT_BOTTOM_UP + 1
+ const val APPLY = INSERT_TOP_DOWN + 1
+ const val REUSE = APPLY + 1
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 88245a7..48657d5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -55,13 +55,16 @@
((lowBits shl 1) and highBits))
}
-private const val UsedFlag = 0x01
-private const val DefaultsInScopeFlag = 0x02
-private const val DefaultsInvalidFlag = 0x04
-private const val RequiresRecomposeFlag = 0x08
-private const val SkippedFlag = 0x10
-private const val RereadingFlag = 0x20
-private const val ForcedRecomposeFlag = 0x40
+private const val UsedFlag = 0x001
+private const val DefaultsInScopeFlag = 0x002
+private const val DefaultsInvalidFlag = 0x004
+private const val RequiresRecomposeFlag = 0x008
+private const val SkippedFlag = 0x010
+private const val RereadingFlag = 0x020
+private const val ForcedRecomposeFlag = 0x040
+private const val ForceReusing = 0x080
+private const val Paused = 0x100
+private const val Resuming = 0x200
internal interface RecomposeScopeOwner {
fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
@@ -110,11 +113,51 @@
var used: Boolean
get() = flags and UsedFlag != 0
set(value) {
- if (value) {
- flags = flags or UsedFlag
- } else {
- flags = flags and UsedFlag.inv()
- }
+ flags =
+ if (value) {
+ flags or UsedFlag
+ } else {
+ flags and UsedFlag.inv()
+ }
+ }
+
+ /**
+ * Used to force a scope to the reusing state when a composition is paused while reusing
+ * content.
+ */
+ var reusing: Boolean
+ get() = flags and ForceReusing != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or ForceReusing
+ } else {
+ flags and ForceReusing.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var paused: Boolean
+ get() = flags and Paused != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Paused
+ } else {
+ flags and Paused.inv()
+ }
+ }
+
+ /** Used to flag a scope as paused for pausable compositions */
+ var resuming: Boolean
+ get() = flags and Resuming != 0
+ set(value) {
+ flags =
+ if (value) {
+ flags or Resuming
+ } else {
+ flags and Resuming.inv()
+ }
}
/**
@@ -299,7 +342,9 @@
}
fun scopeSkipped() {
- skipped = true
+ if (!reusing) {
+ skipped = true
+ }
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 67d0d8a..e347076 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,12 +17,15 @@
package androidx.compose.runtime
import androidx.collection.MutableScatterSet
+import androidx.collection.ScatterSet
+import androidx.collection.emptyScatterSet
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.collection.wrapIntoSet
import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.SnapshotThreadLocal
import androidx.compose.runtime.internal.logError
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.MutableSnapshot
@@ -232,6 +235,7 @@
// End properties guarded by stateLock
private val _state = MutableStateFlow(State.Inactive)
+ private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>()
/**
* A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its
@@ -1116,6 +1120,54 @@
}
}
+ internal override fun composeInitialPaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ content: @Composable () -> Unit
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ composition.pausable(shouldPause) {
+ composeInitial(composition, content)
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ internal override fun recomposePaused(
+ composition: ControlledComposition,
+ shouldPause: () -> Boolean,
+ invalidScopes: ScatterSet<RecomposeScopeImpl>
+ ): ScatterSet<RecomposeScopeImpl> {
+ return try {
+ recordComposerModifications()
+ composition.recordModificationsOf(invalidScopes.wrapIntoSet())
+ composition.pausable(shouldPause) {
+ val needsApply = performRecompose(composition, null)
+ if (needsApply != null) {
+ performInitialMovableContentInserts(composition)
+ needsApply.applyChanges()
+ needsApply.applyLateChanges()
+ }
+ pausedScopes.get() ?: emptyScatterSet()
+ }
+ } finally {
+ pausedScopes.set(null)
+ }
+ }
+
+ override fun reportPausedScope(scope: RecomposeScopeImpl) {
+ val scopes =
+ pausedScopes.get()
+ ?: run {
+ val newScopes = mutableScatterSetOf<RecomposeScopeImpl>()
+ pausedScopes.set(newScopes)
+ newScopes
+ }
+ scopes.add(scope)
+ }
+
private fun performInitialMovableContentInserts(composition: ControlledComposition) {
synchronized(stateLock) {
if (!compositionValuesAwaitingInsert.fastAny { it.composition == composition }) return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index cce9013..5c622d6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -18,6 +18,7 @@
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
+import androidx.collection.MutableObjectList
import androidx.compose.runtime.snapshots.fastAny
import androidx.compose.runtime.snapshots.fastFilterIndexed
import androidx.compose.runtime.snapshots.fastForEach
@@ -1290,6 +1291,12 @@
/** This a count of the [nodeCount] of the explicitly started groups. */
private val nodeCountStack = IntStack()
+ /**
+ * Deferred slot writes for open groups to avoid thrashing the slot table when slots are added
+ * to parent group which already has children.
+ */
+ private var deferredSlotWrites: MutableIntObjectMap<MutableObjectList<Any?>>? = null
+
/** The current group that will be started by [startGroup] or skipped by [skipGroup] */
var currentGroup = 0
private set
@@ -1439,6 +1446,19 @@
* being inserted.
*/
fun update(value: Any?): Any? {
+ if (insertCount > 0 && currentSlot != slotsGapStart) {
+ // Defer write as doing it now would thrash the slot table.
+ val deferred =
+ (deferredSlotWrites ?: MutableIntObjectMap())
+ .also { deferredSlotWrites = it }
+ .getOrPut(parent) { MutableObjectList() }
+ deferred.add(value)
+ return Composer.Empty
+ }
+ return rawUpdate(value)
+ }
+
+ private fun rawUpdate(value: Any?): Any? {
val result = skip()
set(value)
return result
@@ -1604,6 +1624,14 @@
return result
}
+ /** Set the slot by index to Composer.Empty, returning previous value */
+ fun clear(slotIndex: Int): Any? {
+ val address = dataIndexToDataAddress(slotIndex)
+ val result = slots[address]
+ slots[address] = Composer.Empty
+ return result
+ }
+
/**
* Skip the current slot without updating. If the slot table is inserting then and
* [Composer.Empty] slot is added and [skip] return [Composer.Empty].
@@ -1664,7 +1692,7 @@
groups.dataIndex(groupIndexToAddress(groupIndex + groupSize(groupIndex)))
private val currentGroupSlotIndex: Int
- get() = currentSlot - slotsStartIndex(parent)
+ get() = currentSlot - slotsStartIndex(parent) + (deferredSlotWrites?.get(parent)?.size ?: 0)
/**
* Advance [currentGroup] by [amount]. The [currentGroup] group cannot be advanced outside the
@@ -1850,6 +1878,14 @@
val newGroupSize = currentGroup - groupIndex
val isNode = groups.isNode(groupAddress)
if (inserting) {
+ // Check for deferred slot writes
+ val deferredSlotWrites = deferredSlotWrites
+ deferredSlotWrites?.get(groupIndex)?.let {
+ it.forEach { value -> rawUpdate(value) }
+ deferredSlotWrites.remove(groupIndex)
+ }
+
+ // Close the group
groups.updateGroupSize(groupAddress, newGroupSize)
groups.updateNodeCount(groupAddress, newNodes)
nodeCount = nodeCountStack.pop() + if (isNode) 1 else newNodes
@@ -1995,16 +2031,6 @@
}
}
- inline fun forEachData(group: Int, block: (index: Int, data: Any?) -> Unit) {
- val address = groupIndexToAddress(group)
- val slotsStart = groups.slotIndex(address)
- val slotsEnd = groups.dataIndex(groupIndexToAddress(group + 1))
-
- for (slot in slotsStart until slotsEnd) {
- block(slot - slotsStart, slots[dataIndexToDataAddress(slot)])
- }
- }
-
inline fun forAllData(group: Int, block: (index: Int, data: Any?) -> Unit) {
val address = groupIndexToAddress(group)
val start = groups.dataIndex(address)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
index 4780cdb..e2eaa76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberManager
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotTable
@@ -40,6 +41,7 @@
import androidx.compose.runtime.changelist.Operation.EndCompositionScope
import androidx.compose.runtime.changelist.Operation.EndCurrentGroup
import androidx.compose.runtime.changelist.Operation.EndMovableContentPlacement
+import androidx.compose.runtime.changelist.Operation.EndResumingScope
import androidx.compose.runtime.changelist.Operation.EnsureGroupStarted
import androidx.compose.runtime.changelist.Operation.EnsureRootGroupStarted
import androidx.compose.runtime.changelist.Operation.InsertSlots
@@ -48,11 +50,13 @@
import androidx.compose.runtime.changelist.Operation.MoveNode
import androidx.compose.runtime.changelist.Operation.ReleaseMovableGroupAtCurrent
import androidx.compose.runtime.changelist.Operation.Remember
+import androidx.compose.runtime.changelist.Operation.RememberPausingScope
import androidx.compose.runtime.changelist.Operation.RemoveCurrentGroup
import androidx.compose.runtime.changelist.Operation.RemoveNode
import androidx.compose.runtime.changelist.Operation.ResetSlots
import androidx.compose.runtime.changelist.Operation.SideEffect
import androidx.compose.runtime.changelist.Operation.SkipToEndOfCurrentGroup
+import androidx.compose.runtime.changelist.Operation.StartResumingScope
import androidx.compose.runtime.changelist.Operation.TrimParentValues
import androidx.compose.runtime.changelist.Operation.UpdateAnchoredValue
import androidx.compose.runtime.changelist.Operation.UpdateAuxData
@@ -87,6 +91,18 @@
operations.push(Remember) { setObject(Remember.Value, value) }
}
+ fun pushRememberPausingScope(scope: RecomposeScopeImpl) {
+ operations.push(RememberPausingScope) { setObject(RememberPausingScope.Scope, scope) }
+ }
+
+ fun pushStartResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(StartResumingScope) { setObject(StartResumingScope.Scope, scope) }
+ }
+
+ fun pushEndResumingScope(scope: RecomposeScopeImpl) {
+ operations.push(EndResumingScope) { setObject(EndResumingScope.Scope, scope) }
+ }
+
fun pushUpdateValue(value: Any?, groupSlotIndex: Int) {
operations.push(UpdateValue) {
setObject(UpdateValue.Value, value)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
index 74c7146..9b87d45 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.MovableContentState
import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SlotReader
import androidx.compose.runtime.SlotTable
@@ -192,6 +193,18 @@
changeList.pushRemember(value)
}
+ fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ changeList.pushRememberPausingScope(scope)
+ }
+
+ fun startResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushStartResumingScope(scope)
+ }
+
+ fun endResumingScope(scope: RecomposeScopeImpl) {
+ changeList.pushEndResumingScope(scope)
+ }
+
fun updateValue(value: Any?, groupSlotIndex: Int) {
pushSlotTableOperationPreamble(useParentSlot = true)
changeList.pushUpdateValue(value, groupSlotIndex)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
index 894f3d47..15aa234 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
@@ -18,7 +18,6 @@
import androidx.compose.runtime.Anchor
import androidx.compose.runtime.Applier
-import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.ControlledComposition
@@ -171,6 +170,66 @@
}
}
+ object RememberPausingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.rememberPausingScope(scope)
+ }
+ }
+
+ object StartResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.startResumingScope(scope)
+ }
+ }
+
+ object EndResumingScope : Operation(objects = 1) {
+ inline val Scope
+ get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+ override fun objectParamName(parameter: ObjectParameter<*>): String =
+ when (parameter) {
+ Scope -> "scope"
+ else -> super.objectParamName(parameter)
+ }
+
+ override fun OperationArgContainer.execute(
+ applier: Applier<*>,
+ slots: SlotWriter,
+ rememberManager: RememberManager
+ ) {
+ val scope = getObject(Scope)
+ rememberManager.endResumingScope(scope)
+ }
+ }
+
object AppendValue : Operation(objects = 2) {
inline val Anchor
get() = ObjectParameter<Anchor>(0)
@@ -467,7 +526,7 @@
slots: SlotWriter,
rememberManager: RememberManager
) {
- (applier.current as ComposeNodeLifecycleCallback).onReuse()
+ applier.reuse()
}
}
@@ -492,7 +551,7 @@
) {
val value = getObject(Value)
val block = getObject(Block)
- applier.current.block(value)
+ applier.apply(block, value)
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
new file mode 100644
index 0000000..d9d78ea
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.internal
+
+import androidx.collection.MutableIntList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.mutableScatterMapOf
+import androidx.collection.mutableScatterSetOf
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.RecomposeScopeImpl
+import androidx.compose.runtime.RememberManager
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.Stack
+import androidx.compose.runtime.snapshots.fastForEach
+
+/**
+ * Used as a placeholder for paused compositions to ensure the remembers are dispatch in the correct
+ * order. While the paused composition is resuming all remembered objects are placed into the this
+ * classes list instead of the main list. As remembers are dispatched, this will dispatch remembers
+ * to the object remembered in the paused composition's content in the order that they would have
+ * been dispatched had the composition not been paused.
+ */
+internal class PausedCompositionRemembers(private val abandoning: MutableSet<RememberObserver>) :
+ RememberObserver {
+ val pausedRemembers = mutableListOf<RememberObserver>()
+
+ override fun onRemembered() {
+ pausedRemembers.fastForEach {
+ abandoning.remove(it)
+ it.onRemembered()
+ }
+ }
+
+ // These are never called
+ override fun onForgotten() {}
+
+ override fun onAbandoned() {}
+}
+
+/** Helper for collecting remember observers for later strictly ordered dispatch. */
+internal class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
+ RememberManager {
+ private val remembering = mutableListOf<RememberObserver>()
+ private var currentRememberingList = remembering
+ private val leaving = mutableListOf<Any>()
+ private val sideEffects = mutableListOf<() -> Unit>()
+ private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
+ private var pausedPlaceholders:
+ MutableScatterMap<RecomposeScopeImpl, PausedCompositionRemembers>? =
+ null
+ private val pending = mutableListOf<Any>()
+ private val priorities = MutableIntList()
+ private val afters = MutableIntList()
+ private var nestedRemembersLists: Stack<MutableList<RememberObserver>>? = null
+
+ override fun remembering(instance: RememberObserver) {
+ currentRememberingList.add(instance)
+ }
+
+ override fun forgetting(
+ instance: RememberObserver,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun sideEffect(effect: () -> Unit) {
+ sideEffects += effect
+ }
+
+ override fun deactivating(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun releasing(
+ instance: ComposeNodeLifecycleCallback,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ val releasing =
+ releasing ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
+
+ releasing += instance
+ recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+ }
+
+ override fun rememberPausingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholder = PausedCompositionRemembers(abandoning)
+ (pausedPlaceholders
+ ?: mutableScatterMapOf<RecomposeScopeImpl, PausedCompositionRemembers>().also {
+ pausedPlaceholders = it
+ })[scope] = pausedPlaceholder
+ this.currentRememberingList.add(pausedPlaceholder)
+ }
+
+ override fun startResumingScope(scope: RecomposeScopeImpl) {
+ val placeholder = pausedPlaceholders?.get(scope)
+ if (placeholder != null) {
+ (nestedRemembersLists
+ ?: Stack<MutableList<RememberObserver>>().also { nestedRemembersLists = it })
+ .push(currentRememberingList)
+ currentRememberingList = placeholder.pausedRemembers
+ }
+ }
+
+ override fun endResumingScope(scope: RecomposeScopeImpl) {
+ val pausedPlaceholders = pausedPlaceholders
+ if (pausedPlaceholders != null) {
+ val placeholder = pausedPlaceholders[scope]
+ if (placeholder != null) {
+ nestedRemembersLists?.pop()?.let { currentRememberingList = it }
+ pausedPlaceholders.remove(scope)
+ }
+ }
+ }
+
+ fun dispatchRememberObservers() {
+ // Add any pending out-of-order forgotten objects
+ processPendingLeaving(Int.MIN_VALUE)
+
+ // Send forgets and node callbacks
+ if (leaving.isNotEmpty()) {
+ trace("Compose:onForgotten") {
+ val releasing = releasing
+ for (i in leaving.size - 1 downTo 0) {
+ val instance = leaving[i]
+ if (instance is RememberObserver) {
+ abandoning.remove(instance)
+ instance.onForgotten()
+ }
+ if (instance is ComposeNodeLifecycleCallback) {
+ // node callbacks are in the same queue as forgets to ensure ordering
+ if (releasing != null && instance in releasing) {
+ instance.onRelease()
+ } else {
+ instance.onDeactivate()
+ }
+ }
+ }
+ }
+ }
+
+ // Send remembers
+ if (remembering.isNotEmpty()) {
+ trace("Compose:onRemembered") { dispatchRememberList(remembering) }
+ }
+ }
+
+ private fun dispatchRememberList(list: List<RememberObserver>) {
+ list.fastForEach { instance ->
+ abandoning.remove(instance)
+ instance.onRemembered()
+ }
+ }
+
+ fun dispatchSideEffects() {
+ if (sideEffects.isNotEmpty()) {
+ trace("Compose:sideeffects") {
+ sideEffects.fastForEach { sideEffect -> sideEffect() }
+ sideEffects.clear()
+ }
+ }
+ }
+
+ fun dispatchAbandons() {
+ if (abandoning.isNotEmpty()) {
+ trace("Compose:abandons") {
+ val iterator = abandoning.iterator()
+ // remove elements one by one to ensure that abandons will not be dispatched
+ // second time in case [onAbandoned] throws.
+ while (iterator.hasNext()) {
+ val instance = iterator.next()
+ iterator.remove()
+ instance.onAbandoned()
+ }
+ }
+ }
+ }
+
+ private fun recordLeaving(
+ instance: Any,
+ endRelativeOrder: Int,
+ priority: Int,
+ endRelativeAfter: Int
+ ) {
+ processPendingLeaving(endRelativeOrder)
+ if (endRelativeAfter in 0 until endRelativeOrder) {
+ pending.add(instance)
+ priorities.add(priority)
+ afters.add(endRelativeAfter)
+ } else {
+ leaving.add(instance)
+ }
+ }
+
+ private fun processPendingLeaving(endRelativeOrder: Int) {
+ if (pending.isNotEmpty()) {
+ var index = 0
+ var toAdd: MutableList<Any>? = null
+ var toAddAfter: MutableIntList? = null
+ var toAddPriority: MutableIntList? = null
+ while (index < afters.size) {
+ if (endRelativeOrder <= afters[index]) {
+ val instance = pending.removeAt(index)
+ val endRelativeAfter = afters.removeAt(index)
+ val priority = priorities.removeAt(index)
+
+ if (toAdd == null) {
+ toAdd = mutableListOf(instance)
+ toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
+ toAddPriority = MutableIntList().also { it.add(priority) }
+ } else {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+ toAdd.add(instance)
+ toAddAfter.add(endRelativeAfter)
+ toAddPriority.add(priority)
+ }
+ } else {
+ index++
+ }
+ }
+ if (toAdd != null) {
+ toAddPriority as MutableIntList
+ toAddAfter as MutableIntList
+
+ // Sort the list into [after, -priority] order where it is ordered by after
+ // in ascending order as the primary key and priority in descending order as
+ // secondary key.
+
+ // For example if remember occurs after a child group it must be added after
+ // all the remembers of the child. This is reported with an after which is the
+ // slot index of the child's last slot. As this slot might be at the same
+ // location as where its parents ends this would be ambiguous which should
+ // first if both the two groups request a slot to be after the same slot.
+ // Priority is used to break the tie here which is the group index of the group
+ // which is leaving. Groups that are lower must be added before the parent's
+ // remember when they have the same after.
+
+ // The sort must be stable as as consecutive remembers in the same group after
+ // the same child will have the same after and priority.
+
+ // A selection sort is used here because it is stable and the groups are
+ // typically very short so this quickly exit list of one and not loop for
+ // for sizes of 2. As the information is split between three lists, to
+ // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
+ // an option to supply a custom swap.
+ for (i in 0 until toAdd.size - 1) {
+ for (j in i + 1 until toAdd.size) {
+ val iAfter = toAddAfter[i]
+ val jAfter = toAddAfter[j]
+ if (
+ iAfter < jAfter ||
+ (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
+ ) {
+ toAdd.swap(i, j)
+ toAddPriority.swap(i, j)
+ toAddAfter.swap(i, j)
+ }
+ }
+ }
+ leaving.addAll(toAdd)
+ }
+ }
+ }
+}
+
+private fun <T> MutableList<T>.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
+
+private fun MutableIntList.swap(a: Int, b: Int) {
+ val item = this[a]
+ this[a] = this[b]
+ this[b] = item
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index c2560dd..2d820aa 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4608,6 +4608,19 @@
revalidate()
}
+ @Test // regression test for b/362291064
+ fun avoidsThrashingTheSlotTable() = compositionTest {
+ val count = 100
+ var data by mutableIntStateOf(0)
+ compose { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ validate { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ data++
+ advance()
+ revalidate()
+ }
+
private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
val channel = Channel<Unit>(Channel.CONFLATED)
val job = launch { channel.consumeEach { Snapshot.sendApplyNotifications() } }
@@ -4800,16 +4813,16 @@
private val rob_reports_to_alice = Report("Rob", "Alice")
private val clark_reports_to_lois = Report("Clark", "Lois")
-private interface Counted {
+internal interface Counted {
val count: Int
}
-private interface Ordered {
+internal interface Ordered {
val rememberOrder: Int
val forgetOrder: Int
}
-private interface Named {
+internal interface Named {
val name: String
}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
new file mode 100644
index 0000000..36a9747
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
@@ -0,0 +1,606 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+import androidx.compose.runtime.mock.EmptyApplier
+import androidx.compose.runtime.mock.Linear
+import androidx.compose.runtime.mock.MockViewValidator
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.View
+import androidx.compose.runtime.mock.ViewApplier
+import androidx.compose.runtime.mock.compositionTest
+import androidx.compose.runtime.mock.validate
+import androidx.compose.runtime.mock.view
+import kotlin.coroutines.resume
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runTest
+
+@Stable
+class PausableCompositionTests {
+ @Test
+ fun canCreateARootPausableComposition() = runTest {
+ val recomposer = Recomposer(coroutineContext)
+ val pausableComposition = PausableComposition(EmptyApplier(), recomposer)
+ pausableComposition.dispose()
+ recomposer.cancel()
+ recomposer.close()
+ }
+
+ @Test
+ fun canCreateANestedPausableComposition() = compositionTest {
+ compose {
+ val parent = rememberCompositionContext()
+ DisposableEffect(Unit) {
+ val pausableComposition = PausableComposition(EmptyApplier(), parent)
+ onDispose { pausableComposition.dispose() }
+ }
+ }
+ }
+
+ @Test
+ fun canRecordAComposition() = compositionTest {
+ // This just tests the recording mechanism used in the tests below.
+ val recording = recordTest {
+ compose { A() }
+
+ validate { this.A() }
+ }
+
+ // Legend for the recording:
+ // +N: Enter N for functions A, B, C, D, (where A:1 is the first lambda in A())
+ // -N: Exit N
+ // *N: Calling N (e.g *B is recorded before B() is called).
+ // ^n: calling remember for some value
+
+ // Here we expect the normal, synchronous, execution as the recorded composition is not
+ // pausable. That is if we see a *B that should immediately followed by a B+ its content and
+ // a B-.
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, +B, *Linear, +A:1, *C, +C, ^x, *Text, -C, *D, +D, +D:1, *C, +C, " +
+ "^x, *Text, -C, *C, +C, ^x, *Text, -C, *C, +C, ^x, *Text, -C, -D:1, -D, -A:1, " +
+ "-B, -A"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ normalWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+
+ // Same Legend as canRecordAComposition
+ // Here we expect all functions to exit before the content of the function is executed
+ // because the above will pause at every pause point. If we see a B* we should not receive
+ // a B+ until after the caller finishes. (e.g. A-).
+ assertEquals(
+ recording,
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C"
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusableContent() = compositionTest {
+ val awaiter = Awaiter()
+ var receivedIteration = 0
+ val recording = recordTest {
+ compose {
+ PausableContent(
+ reuseWorkflow {
+ receivedIteration = iteration
+ awaiter.done()
+ }
+ ) {
+ A()
+ }
+ }
+ awaiter.await()
+ }
+ validate { this.PausableContent { this.A() } }
+ assertEquals(10, receivedIteration)
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertEquals(
+ "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C",
+ recording
+ )
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun canPauseReusingContent() = compositionTest {
+ val awaiter = Awaiter()
+ var recording = ""
+ val workflow: Workflow = {
+ // Create the content
+ setContentWithReuse()
+ resumeTillComplete { false }
+ apply()
+
+ // Reuse the content
+ recording = recordTest {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ }
+ awaiter.done()
+ }
+
+ compose { PausableContent(workflow) { A() } }
+ awaiter.await()
+ // Same Legend as canRecordAComposition
+ // Here we expect the result to be the same as if we were inserting new content as in
+ // canPauseContent
+ assertArrayEquals(
+ ("+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+ "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +C, ^x, *Text, -C")
+ .splitRecording(),
+ recording.splitRecording()
+ )
+ }
+
+ @Test
+ fun applierOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var applier: ViewApplier? = null
+
+ val workflow = workflow {
+ setContent()
+
+ assertFalse(applier?.called == true, "Applier was called during set content")
+
+ resumeTillComplete { false }
+
+ assertFalse(applier?.called == true, "Applier was called during resume")
+
+ apply()
+
+ assertTrue(applier?.called == true, "Applier wasn't called")
+
+ awaiter.done()
+ }
+
+ compose {
+ PausableContent(workflow, { view -> ViewApplier(view).also { applier = it } }) { A() }
+ }
+ awaiter.await()
+ }
+
+ @Test
+ @Ignore // Requires compiler support
+ fun rememberOnlyCalledInApply() = compositionTest {
+ val awaiter = Awaiter()
+ var onRememberCalled = false
+
+ val workflow = workflow {
+ setContent()
+ assertFalse(onRememberCalled, "onRemember called during set content")
+
+ resumeTillComplete {
+ assertFalse(onRememberCalled, "onRemember called during resume")
+ true
+ }
+ assertFalse(onRememberCalled, "onRemember called before resume returned")
+
+ apply()
+
+ assertTrue(onRememberCalled, "onRemember was not called in apply")
+
+ awaiter.done()
+ }
+
+ fun rememberedObject(name: String) =
+ object : RememberObserver {
+ val name = name
+
+ override fun onRemembered() {
+ onRememberCalled = true
+ report("+$name")
+ }
+
+ override fun onForgotten() {
+ report("-$name")
+ }
+
+ override fun onAbandoned() {
+ report("!$name")
+ }
+ }
+
+ val recording = recordTest {
+ compose {
+ PausableContent(workflow) {
+ val a = remember { rememberedObject("a") }
+ report("C(${a.name})")
+ B {
+ val b = remember { rememberedObject("b") }
+ report("C(${b.name})")
+ B {
+ val c = remember { rememberedObject("c") }
+ report("C(${c.name})")
+ C()
+ val d = remember { rememberedObject("d") }
+ report("C(${d.name})")
+ D()
+ }
+ }
+ }
+ }
+
+ awaiter.await()
+ }
+ // Same Legend as canRecordAComposition except the addition of the C(N) added above and
+ // +a, +b, etc. which records when the remembered object are sent the on-remember. This
+ // ensures that all onRemember calls are made after the composition has completed.
+ assertEquals(
+ "C(a), +B, *Linear, -B, C(b), +B, *Linear, -B, C(c), C(d), +C, ^x, *Text, -C, +D, " +
+ "-D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+ "-C, +a, +b, +c, +d",
+ recording
+ )
+ }
+
+ @Suppress("ListIterator")
+ @Test
+ fun pausable_testRemember_RememberForgetOrder() = compositionTest {
+ var order = 0
+ val objects = mutableListOf<Any>()
+ val newRememberObject = { name: String ->
+ object : RememberObserver, Counted, Ordered, Named {
+ override var name = name
+ override var count = 0
+ override var rememberOrder = -1
+ override var forgetOrder = -1
+
+ override fun onRemembered() {
+ assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
+ rememberOrder = order++
+ count++
+ }
+
+ override fun onForgotten() {
+ assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
+ forgetOrder = order++
+ count--
+ }
+
+ override fun onAbandoned() {
+ assertEquals(0, count, "onAbandoned called after onRemembered")
+ }
+ }
+ .also { objects.add(it) }
+ }
+
+ @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
+
+ @Composable
+ fun Tree() {
+ used(remember { newRememberObject("L0B") })
+ Linear {
+ used(remember { newRememberObject("L1B") })
+ Linear {
+ used(remember { newRememberObject("L2B") })
+ Linear {
+ used(remember { newRememberObject("L3B") })
+ Linear { used(remember { newRememberObject("Leaf") }) }
+ used(remember { newRememberObject("L3A") })
+ }
+ used(remember { newRememberObject("L2A") })
+ }
+ used(remember { newRememberObject("L1A") })
+ }
+ used(remember { newRememberObject("L0A") })
+ }
+
+ val awaiter = Awaiter()
+ val workFlow = normalWorkflow { awaiter.done() }
+
+ compose { PausableContent(workFlow) { Tree() } }
+ awaiter.await()
+
+ // Legend:
+ // L<N><B|A>: where N is the nesting level and B is before the children and
+ // A is after the children.
+ // Leaf: the object remembered in the middle.
+ // This is asserting that the remember order is the same as it would have been had the
+ // above composition was not paused.
+ assertEquals(
+ "L0B, L1B, L2B, L3B, Leaf, L3A, L2A, L1A, L0A",
+ objects
+ .mapNotNull { it as? Ordered }
+ .sortedBy { it.rememberOrder }
+ .joinToString { (it as Named).name },
+ "Expected enter order",
+ )
+ }
+}
+
+fun String.splitRecording() = split(", ")
+
+typealias Workflow = suspend PausableContentWorkflowScope.() -> Unit
+
+fun workflow(workflow: Workflow): Workflow = workflow
+
+fun reuseWorkflow(done: Workflow = {}) = workflow {
+ setContentWithReuse()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+fun normalWorkflow(done: Workflow = {}) = workflow {
+ setContent()
+ resumeTillComplete { true }
+ apply()
+ done()
+}
+
+private interface TestRecorder {
+ fun log(message: String)
+
+ fun logs(): String
+
+ fun clear()
+}
+
+private var recorder: TestRecorder =
+ object : TestRecorder {
+ override fun log(message: String) {}
+
+ override fun logs(): String = ""
+
+ override fun clear() {}
+ }
+
+private inline fun recordTest(block: () -> Unit): String {
+ val result = mutableListOf<String>()
+ val oldRecorder = recorder
+ recorder =
+ object : TestRecorder {
+ override fun log(message: String) {
+ result.add(message)
+ }
+
+ override fun logs() = result.joinToString()
+
+ override fun clear() {
+ result.clear()
+ }
+ }
+ block()
+ recorder = oldRecorder
+ return result.joinToString()
+}
+
+private fun report(message: String) {
+ synchronized(recorder) { recorder.log(message) }
+}
+
+private inline fun report(message: String, block: () -> Unit) {
+ report("+$message")
+ block()
+ report("-$message")
+}
+
+@Composable
+private fun A() {
+ report("A") {
+ report("^z")
+ val z = remember { 0 }
+ report("^Y")
+ val y = remember { 1 }
+ Text("A: $z $y")
+ report("*B")
+ B {
+ report("A:1") {
+ report("*C")
+ C()
+ report("*D")
+ D()
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.PausableContent(content: MockViewValidator.() -> Unit) {
+ this.view("PausableContentHost") { this.view("PausableContent", content) }
+}
+
+private fun MockViewValidator.A() {
+ Text("A: 0 1")
+ this.B {
+ this.C()
+ this.D()
+ }
+}
+
+@Composable
+private fun B(content: @Composable () -> Unit) {
+ report("B") {
+ report("*Linear")
+ Linear(content)
+ }
+}
+
+private fun MockViewValidator.B(content: MockViewValidator.() -> Unit) {
+ this.Linear(content)
+}
+
+@Composable
+private fun C() {
+ report("C") {
+ report("^x")
+ val x = remember { 3 }
+ report("*Text")
+ Text("C: $x")
+ }
+}
+
+private fun MockViewValidator.C() {
+ this.Text("C: 3")
+}
+
+@Composable
+private fun D() {
+ report("D") {
+ Linear {
+ report("D:1") {
+ repeat(3) {
+ report("*C")
+ C()
+ }
+ }
+ }
+ }
+}
+
+private fun MockViewValidator.D() {
+ this.Linear { repeat(3) { this.C() } }
+}
+
+interface PausableContentWorkflowScope {
+ val iteration: Int
+ val applied: Boolean
+
+ fun setContent(): PausedComposition
+
+ fun setContentWithReuse(): PausedComposition
+
+ fun resumeTillComplete(shouldPause: () -> Boolean)
+
+ fun apply()
+}
+
+fun PausableContentWorkflowScope.run(shouldPause: () -> Boolean = { true }) {
+ setContent()
+ resumeTillComplete(shouldPause)
+ apply()
+}
+
+class PausableContentWorkflowDriver(
+ private val composition: PausableComposition,
+ private val content: @Composable () -> Unit,
+ private var host: View?,
+ private var contentView: View?
+) : PausableContentWorkflowScope {
+ private var pausedComposition: PausedComposition? = null
+ override var iteration = 0
+ override val applied: Boolean
+ get() = host == null && pausedComposition == null
+
+ override fun setContent(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContent(content).also { pausedComposition = it }
+ }
+
+ override fun setContentWithReuse(): PausedComposition {
+ checkPrecondition(pausedComposition == null)
+ return composition.setPausableContentWithReuse(content).also { pausedComposition = it }
+ }
+
+ override fun resumeTillComplete(shouldPause: () -> Boolean) {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null)
+ while (!pausedComposition.isComplete) {
+ pausedComposition.resume(shouldPause)
+ iteration++
+ }
+ }
+
+ override fun apply() {
+ val pausedComposition = pausedComposition
+ checkPrecondition(pausedComposition != null && pausedComposition.isComplete)
+ pausedComposition.apply()
+ this.pausedComposition = null
+ val host = host
+ val contentView = contentView
+ if (host != null && contentView != null) {
+ host.children.add(contentView)
+ this.host = null
+ this.contentView = null
+ }
+ }
+}
+
+@Composable
+private fun PausableContent(
+ workflow: suspend PausableContentWorkflowScope.() -> Unit = { run() },
+ createApplier: (view: View) -> Applier<View> = { ViewApplier(it) },
+ content: @Composable () -> Unit
+) {
+ val host = View().also { it.name = "PausableContentHost" }
+ val pausableContent = View().also { it.name = "PausableContent" }
+ ComposeNode<View, ViewApplier>(factory = { host }, update = {})
+ val parent = rememberCompositionContext()
+ val composition =
+ remember(parent) { PausableComposition(createApplier(pausableContent), parent) }
+ LaunchedEffect(content as Any) {
+ val scope = PausableContentWorkflowDriver(composition, content, host, pausableContent)
+ scope.workflow()
+ }
+ DisposableEffect(Unit) { onDispose { composition.dispose() } }
+}
+
+private class Awaiter {
+ private var continuation: CancellableContinuation<Unit>? = null
+ private var done = false
+
+ suspend fun await() {
+ if (!done) {
+ suspendCancellableCoroutine { continuation = it }
+ }
+ }
+
+ fun resume() {
+ val current = continuation
+ continuation = null
+ current?.resume(Unit)
+ }
+
+ fun done() {
+ done = true
+ resume()
+ }
+}
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 1fe35c8..4e9e30d 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -38,10 +38,10 @@
commonMain {
dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui-unit"))
- implementation(projectOrArtifact(":compose:ui:ui-graphics"))
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-test-junit4"))
}
}
androidMain.dependencies {
@@ -49,11 +49,11 @@
// workaround for https://github.com/gradle/gradle/issues/8489
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
implementation "androidx.activity:activity-compose:1.3.1"
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ api(project(":compose:ui:ui-test-junit4"))
api(project(":test:screenshot:screenshot"))
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ compileOnly(project(":compose:ui:ui-android-stubs"))
implementation(libs.testCore)
implementation(libs.testRules)
}
@@ -86,7 +86,7 @@
dependsOn(commonTest)
dependencies {
implementation(libs.truth)
- implementation(projectOrArtifact(":compose:material:material"))
+ implementation(project(":compose:material:material"))
}
}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index a3306fe..6c2af78 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -495,6 +495,7 @@
public final class ImageBitmapKt {
method public static androidx.compose.ui.graphics.ImageBitmap ImageBitmap(int width, int height, optional int config, optional boolean hasAlpha, optional androidx.compose.ui.graphics.colorspace.ColorSpace colorSpace);
+ method public static androidx.compose.ui.graphics.ImageBitmap decodeToImageBitmap(byte[]);
method public static androidx.compose.ui.graphics.PixelMap toPixelMap(androidx.compose.ui.graphics.ImageBitmap, optional int startX, optional int startY, optional int width, optional int height, optional int[] buffer, optional int bufferOffset, optional int stride);
}
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index 423e117..bcf64b0 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -540,6 +540,7 @@
public final class ImageBitmapKt {
method public static androidx.compose.ui.graphics.ImageBitmap ImageBitmap(int width, int height, optional int config, optional boolean hasAlpha, optional androidx.compose.ui.graphics.colorspace.ColorSpace colorSpace);
+ method public static androidx.compose.ui.graphics.ImageBitmap decodeToImageBitmap(byte[]);
method public static androidx.compose.ui.graphics.PixelMap toPixelMap(androidx.compose.ui.graphics.ImageBitmap, optional int startX, optional int startY, optional int width, optional int height, optional int[] buffer, optional int bufferOffset, optional int stride);
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
index 93f415a..45d0f75 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.graphics
import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.os.Build
import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
@@ -29,6 +30,10 @@
*/
fun Bitmap.asImageBitmap(): ImageBitmap = AndroidImageBitmap(this)
+internal actual fun createImageBitmap(bytes: ByteArray): ImageBitmap {
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageBitmap()
+}
+
internal actual fun ActualImageBitmap(
width: Int,
height: Int,
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt
index 4a3d61b..e022cb3 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.kt
@@ -233,3 +233,12 @@
hasAlpha: Boolean = true,
colorSpace: ColorSpace = ColorSpaces.Srgb
): ImageBitmap = ActualImageBitmap(width, height, config, hasAlpha, colorSpace)
+
+/**
+ * Decodes a byte array of a Bitmap to an ImageBitmap.
+ *
+ * @return The converted ImageBitmap.
+ */
+fun ByteArray.decodeToImageBitmap(): ImageBitmap = createImageBitmap(this)
+
+internal expect fun createImageBitmap(bytes: ByteArray): ImageBitmap
diff --git a/compose/ui/ui-graphics/src/commonStubsMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.commonStubs.kt b/compose/ui/ui-graphics/src/commonStubsMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.commonStubs.kt
index 49b11c7..3d641f4 100644
--- a/compose/ui/ui-graphics/src/commonStubsMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.commonStubs.kt
+++ b/compose/ui/ui-graphics/src/commonStubsMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.commonStubs.kt
@@ -25,3 +25,5 @@
hasAlpha: Boolean,
colorSpace: ColorSpace
): ImageBitmap = implementedInJetBrainsFork()
+
+internal actual fun createImageBitmap(bytes: ByteArray): ImageBitmap = implementedInJetBrainsFork()
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
@Composable
private fun BoxWithMissingContentDescription() {
Box(
- Modifier.size(20.dp).semantics {
+ Modifier.size(48.dp).semantics {
// The SemanticsModifier will make this node importantForAccessibility
// Having no content description is now a violation
this.contentDescription = ""
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
index fac5641..5d97f9c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
@@ -29,6 +29,8 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -265,6 +267,42 @@
rule.runOnIdle { assertEquals(calculateExpectedIntOffset(alignment), targetOffset.value) }
}
+ @Test
+ fun onPlacedCalledOnReuseInsideLazyColumn() {
+ lateinit var density: Density
+ val items = 200
+ val visibleItems = 2
+ val itemSize = 50.dp
+ val invocations = arrayOf(0, 0)
+
+ // It's important to share lambda across all iterations
+ val placedCallback0: (LayoutCoordinates) -> Unit = { invocations[0] = invocations[0] + 1 }
+ val placedCallback1: (LayoutCoordinates) -> Unit = { invocations[1] = invocations[1] + 1 }
+ val scrollState = LazyListState()
+ rule.setContent {
+ density = LocalDensity.current
+ LazyColumn(Modifier.size(itemSize, itemSize * visibleItems), scrollState) {
+ items(items) {
+ Box(Modifier.size(itemSize).onPlaced(placedCallback0)) {
+ Box(Modifier.size(itemSize).onPlaced(placedCallback1))
+ }
+ }
+ }
+ }
+
+ var expectedInvocations = visibleItems
+ val delta = with(density) { (itemSize * visibleItems).toPx() }
+ repeat(items / visibleItems) {
+ rule.runOnIdle {
+ assertThat(invocations[0]).isAtLeast(expectedInvocations)
+ assertThat(invocations[1]).isAtLeast(expectedInvocations)
+
+ scrollState.dispatchRawDelta(delta)
+ expectedInvocations += visibleItems
+ }
+ }
+ }
+
private fun Modifier.animatePlacement(
targetOffset: MutableState<Offset>,
alignment: () -> Alignment
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 23ba0f4..72a1457 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -238,6 +238,13 @@
abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity>(
private val effectContext: CoroutineContext = EmptyCoroutineContext
) {
+ /**
+ * Returns the current host activity of type [A]. If no such activity is available, for example
+ * if you've navigated to a different activity and the original host has now been destroyed,
+ * this will return `null`.
+ */
+ protected abstract val activity: A?
+
private val idlingResourceRegistry = IdlingResourceRegistry()
internal val composeRootRegistry = ComposeRootRegistry()
@@ -337,13 +344,6 @@
private val testContext = TestContext(testOwner)
/**
- * Returns the current host activity of type [A]. If no such activity is available, for example
- * if you've navigated to a different activity and the original host has now been destroyed,
- * this will return `null`.
- */
- protected abstract val activity: A?
-
- /**
* The receiver scope of the test passed to [runTest]. Note that some of the properties and
* methods will only work during the call to [runTest], as they require that the environment has
* been set up.
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index dbd6e6b..4870663 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -1,33 +1,9 @@
// Baseline format: 1.0
-AddedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.TextLinkStyles, androidx.compose.ui.text.LinkInteractionListener):
- Added method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.TextLinkStyles,androidx.compose.ui.text.LinkInteractionListener)
-
-
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.merge has changed deprecation state true --> false
-
-
-RemovedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.LinkInteractionListener):
- Removed method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.LinkInteractionListener)
-RemovedMethod: androidx.compose.ui.text.TextLinkStyles#merge(androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextLinkStyles.merge(androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#getLinkStyles():
- Removed method androidx.compose.ui.text.TextStyle.getLinkStyles()
-RemovedMethod: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.merge(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #7:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(String, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #8:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(androidx.compose.ui.text.ParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 7ccf854..46a0d57 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -167,9 +167,11 @@
public final class MultiParagraph {
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, optional int maxLines, optional boolean ellipsis, float width);
- ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional int overflow);
method public float[] fillBoundingBoxes(long range, float[] array, @IntRange(from=0L) int arrayStart);
method public androidx.compose.ui.text.style.ResolvedTextDirection getBidiRunDirection(int offset);
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
@@ -295,10 +297,12 @@
public final class ParagraphKt {
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, optional int maxLines, optional boolean ellipsis, float width);
- method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional int overflow);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
}
@androidx.compose.runtime.Immutable public final class ParagraphStyle implements androidx.compose.ui.text.AnnotatedString.Annotation {
@@ -1693,9 +1697,13 @@
public static final class TextOverflow.Companion {
method public int getClip();
method public int getEllipsis();
+ method public int getMiddleEllipsis();
+ method public int getStartEllipsis();
method public int getVisible();
property public final int Clip;
property public final int Ellipsis;
+ property public final int MiddleEllipsis;
+ property public final int StartEllipsis;
property public final int Visible;
}
diff --git a/compose/ui/ui-text/api/restricted_current.ignore b/compose/ui/ui-text/api/restricted_current.ignore
index dbd6e6b..4870663 100644
--- a/compose/ui/ui-text/api/restricted_current.ignore
+++ b/compose/ui/ui-text/api/restricted_current.ignore
@@ -1,33 +1,9 @@
// Baseline format: 1.0
-AddedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.TextLinkStyles, androidx.compose.ui.text.LinkInteractionListener):
- Added method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.TextLinkStyles,androidx.compose.ui.text.LinkInteractionListener)
-
-
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Constructor androidx.compose.ui.text.TextStyle has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.copy has changed deprecation state true --> false
-ChangedDeprecated: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion):
- Method androidx.compose.ui.text.TextStyle.merge has changed deprecation state true --> false
-
-
-RemovedMethod: androidx.compose.ui.text.Html_androidKt#fromHtml(androidx.compose.ui.text.AnnotatedString.Companion, String, androidx.compose.ui.text.LinkInteractionListener):
- Removed method androidx.compose.ui.text.Html_androidKt.fromHtml(androidx.compose.ui.text.AnnotatedString.Companion,String,androidx.compose.ui.text.LinkInteractionListener)
-RemovedMethod: androidx.compose.ui.text.TextLinkStyles#merge(androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextLinkStyles.merge(androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#TextStyle(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed constructor androidx.compose.ui.text.TextStyle(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(androidx.compose.ui.graphics.Brush, float, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(androidx.compose.ui.graphics.Brush,float,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#copy(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.copy(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
-RemovedMethod: androidx.compose.ui.text.TextStyle#getLinkStyles():
- Removed method androidx.compose.ui.text.TextStyle.getLinkStyles()
-RemovedMethod: androidx.compose.ui.text.TextStyle#merge(long, long, androidx.compose.ui.text.font.FontWeight, androidx.compose.ui.text.font.FontStyle, androidx.compose.ui.text.font.FontSynthesis, androidx.compose.ui.text.font.FontFamily, String, long, androidx.compose.ui.text.style.BaselineShift, androidx.compose.ui.text.style.TextGeometricTransform, androidx.compose.ui.text.intl.LocaleList, long, androidx.compose.ui.text.style.TextDecoration, androidx.compose.ui.graphics.Shadow, androidx.compose.ui.graphics.drawscope.DrawStyle, int, int, long, androidx.compose.ui.text.style.TextIndent, androidx.compose.ui.text.style.LineHeightStyle, int, int, androidx.compose.ui.text.PlatformTextStyle, androidx.compose.ui.text.style.TextMotion, androidx.compose.ui.text.TextLinkStyles):
- Removed method androidx.compose.ui.text.TextStyle.merge(long,long,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontSynthesis,androidx.compose.ui.text.font.FontFamily,String,long,androidx.compose.ui.text.style.BaselineShift,androidx.compose.ui.text.style.TextGeometricTransform,androidx.compose.ui.text.intl.LocaleList,long,androidx.compose.ui.text.style.TextDecoration,androidx.compose.ui.graphics.Shadow,androidx.compose.ui.graphics.drawscope.DrawStyle,int,int,long,androidx.compose.ui.text.style.TextIndent,androidx.compose.ui.text.style.LineHeightStyle,int,int,androidx.compose.ui.text.PlatformTextStyle,androidx.compose.ui.text.style.TextMotion,androidx.compose.ui.text.TextLinkStyles)
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.AnnotatedString, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #7:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.MultiParagraph#MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.MultiParagraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(String, androidx.compose.ui.text.TextStyle, long, androidx.compose.ui.unit.Density, androidx.compose.ui.text.font.FontFamily.Resolver, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>>, java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>>, int, boolean) parameter #8:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
+DefaultValueChange: androidx.compose.ui.text.ParagraphKt#Paragraph(androidx.compose.ui.text.ParagraphIntrinsics, long, int, boolean) parameter #3:
+ Attempted to remove default value from parameter ellipsis in androidx.compose.ui.text.ParagraphKt.Paragraph
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 89caf66..ea1b9e1 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -167,9 +167,11 @@
public final class MultiParagraph {
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.AnnotatedString annotatedString, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, optional int maxLines, optional boolean ellipsis, float width);
- ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ ctor @Deprecated public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ ctor public MultiParagraph(androidx.compose.ui.text.MultiParagraphIntrinsics intrinsics, long constraints, optional int maxLines, optional int overflow);
method public float[] fillBoundingBoxes(long range, float[] array, @IntRange(from=0L) int arrayStart);
method public androidx.compose.ui.text.style.ResolvedTextDirection getBidiRunDirection(int offset);
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
@@ -295,10 +297,12 @@
public final class ParagraphKt {
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, optional int maxLines, optional boolean ellipsis, float width);
- method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(androidx.compose.ui.text.ParagraphIntrinsics paragraphIntrinsics, long constraints, optional int maxLines, optional int overflow);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis, float width, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.Font.ResourceLoader resourceLoader);
- method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional boolean ellipsis);
+ method @Deprecated public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, boolean ellipsis);
+ method public static androidx.compose.ui.text.Paragraph Paragraph(String text, androidx.compose.ui.text.TextStyle style, long constraints, androidx.compose.ui.unit.Density density, androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.SpanStyle>> spanStyles, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional int maxLines, optional int overflow);
}
@androidx.compose.runtime.Immutable public final class ParagraphStyle implements androidx.compose.ui.text.AnnotatedString.Annotation {
@@ -1704,9 +1708,13 @@
public static final class TextOverflow.Companion {
method public int getClip();
method public int getEllipsis();
+ method public int getMiddleEllipsis();
+ method public int getStartEllipsis();
method public int getVisible();
property public final int Clip;
property public final int Ellipsis;
+ property public final int MiddleEllipsis;
+ property public final int StartEllipsis;
property public final int Visible;
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
index 967b8cc..1a54763 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/NonLinearFontScalingBenchmark.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -93,7 +94,8 @@
private fun paragraph(text: String, width: Float, density: Density): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text, density),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
index b0fbaea..8251b68 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -97,7 +98,8 @@
): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text, spanStyles),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
index e06b9f6..7834ca4 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphMethodBenchmark.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -87,7 +88,8 @@
Constraints(
maxWidth =
ceil(paragraphIntrinsics.maxIntrinsicWidth / preferredLineCount).toInt()
- )
+ ),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
index d480613..a5fd8ab 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -86,7 +87,8 @@
private fun paragraph(text: String, width: Float): Paragraph {
return Paragraph(
paragraphIntrinsics = paragraphIntrinsics(text),
- constraints = Constraints(maxWidth = ceil(width).toInt())
+ constraints = Constraints(maxWidth = ceil(width).toInt()),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 83bbc1b..074a435 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -18,6 +18,7 @@
import android.graphics.Paint
import android.graphics.Typeface
+import android.os.Build
import android.text.TextPaint
import android.text.style.AbsoluteSizeSpan
import android.text.style.BackgroundColorSpan
@@ -68,6 +69,7 @@
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.style.TextMotion
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -1119,7 +1121,7 @@
simpleParagraph(
text = text,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = paragraphWidth
)
@@ -1139,7 +1141,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = paragraphWidth
@@ -1159,7 +1161,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = paragraphWidth
@@ -1179,7 +1181,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 6 * fontSize.toPx(),
@@ -1199,7 +1201,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1211,14 +1213,14 @@
}
@Test
- fun testEllipsis_withLimitedHeight_ellipsisFalse_doesNotEllipsis() {
+ fun testEllipsis_withLimitedHeight_overflowNotEllipsis_doesNotEllipsis() {
with(defaultDensity) {
val text = "This is a text"
val fontSize = 30.sp
val paragraph =
simpleParagraph(
text = text,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1238,7 +1240,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx(), // fits 2 lines
@@ -1258,7 +1260,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 4 * fontSize.toPx(),
@@ -1278,7 +1280,7 @@
val paragraph =
simpleParagraph(
text = text,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = fontSize.toPx() / 4
@@ -1300,7 +1302,7 @@
text = text,
spanStyles =
listOf(AnnotatedString.Range(SpanStyle(fontSize = fontSize * 2), 0, 2)),
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
width = 4 * fontSize.toPx(),
height = 2.2f * fontSize.toPx() // fits 2 lines
@@ -1311,6 +1313,52 @@
}
}
+ // Experimentally verified that middle and start ellipsis don't work correctly on API 21
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Test
+ fun testEllipsis_withMaxLinesOne_doesStartEllipsis() {
+ with(defaultDensity) {
+ val text = "abcde"
+ val fontSize = 100.sp
+ val paragraphWidth = (text.length - 2f) * fontSize.toPx()
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ overflow = TextOverflow.StartEllipsis,
+ maxLines = 1,
+ style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineEllipsisOffset(0)).isEqualTo(0)
+ assertThat(paragraph.getLineEllipsisCount(0)).isEqualTo(3)
+ }
+ }
+
+ // Experimentally verified that middle and start ellipsis don't work correctly on API 21
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
+ @Test
+ fun testEllipsis_withMaxLinesOne_doesMiddleEllipsis() {
+ with(defaultDensity) {
+ val text = "abcde"
+ val fontSize = 100.sp
+ val paragraphWidth = (text.length - 2f) * fontSize.toPx()
+ val paragraph =
+ simpleParagraph(
+ text = text,
+ overflow = TextOverflow.MiddleEllipsis,
+ maxLines = 1,
+ style = TextStyle(fontFamily = basicFontFamily, fontSize = fontSize),
+ width = paragraphWidth
+ )
+
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
+ assertThat(paragraph.getLineEllipsisOffset(0)).isEqualTo(1)
+ assertThat(paragraph.getLineEllipsisCount(0)).isEqualTo(3)
+ }
+ }
+
@Test
fun testSpanStyle_fontSize_appliedOnTextPaint() {
with(defaultDensity) {
@@ -1909,7 +1957,7 @@
spanStyles = listOf(),
placeholders = listOf(),
maxLines = Int.MAX_VALUE,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = minWidthConstraints,
fontFamilyResolver = UncachedFontFamilyResolver(context),
density = defaultDensity,
@@ -1925,7 +1973,7 @@
spanStyles = listOf(),
placeholders = listOf(),
maxLines = Int.MAX_VALUE,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = minHeightConstraints,
fontFamilyResolver = UncachedFontFamilyResolver(context),
density = defaultDensity,
@@ -2035,7 +2083,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
textIndent: TextIndent? = null,
textAlign: TextAlign = TextAlign.Unspecified,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
width: Float,
height: Float = Float.POSITIVE_INFINITY,
@@ -2048,7 +2096,7 @@
placeholders = listOf(),
style = TextStyle(textAlign = textAlign, textIndent = textIndent).merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = Density(density = 1f),
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
index 39311f4..6e206d0 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphFillBoundingBoxesTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -187,7 +188,8 @@
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = fontSize),
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = fontFamilyResolver
+ fontFamilyResolver = fontFamilyResolver,
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
index 6b6835d..9881518 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphGetRangeForRectTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.matchers.assertThat
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -220,7 +221,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 1490cd4..792816d 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -1416,7 +1417,8 @@
style = TextStyle(textDirection = TextDirection.Rtl),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// the first character uses TextDirection.Content, text is Ltr
@@ -1441,7 +1443,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// Rendered as below:
@@ -1465,7 +1468,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
// Rendered as below:
@@ -1490,7 +1494,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.placeholderRects).hasSize(1)
@@ -1524,7 +1529,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.placeholderRects).hasSize(2)
@@ -1563,7 +1569,8 @@
placeholders = placeholders,
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1575,7 +1582,8 @@
style = TextStyle(),
constraints = minWidthConstraints,
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1587,7 +1595,8 @@
style = TextStyle(),
constraints = minHeightConstraints,
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1610,7 +1619,8 @@
style = TextStyle(fontSize = fontSize, fontFamily = fontFamilyMeasureFont),
constraints = constraints,
density = this,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
@@ -1821,7 +1831,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
@@ -1845,7 +1856,8 @@
maxLines = maxLines,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
index f7dc800..e8064cb 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -286,7 +287,8 @@
),
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
- fontFamilyResolver = UncachedFontFamilyResolver(context)
+ fontFamilyResolver = UncachedFontFamilyResolver(context),
+ overflow = TextOverflow.Clip
)
}
}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
index 341c972..ca8432a 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphFillBoundingBoxesTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -694,7 +695,7 @@
spanStyles = text.spanStyles,
placeholders = placeholders,
maxLines = Int.MAX_VALUE,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
index 7c2fa58..76b79a7 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationBoundingBoxTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -358,7 +359,7 @@
text = TEST_CONTENT_MAP[textDirection]!![lineBreakFrom]!!,
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints =
Constraints(
maxWidth = (widthInFontSize * fontSizeInPx),
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
index e01f03d..a851fb5 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -345,7 +346,7 @@
textIndent = textIndent
),
maxLines = lastLine + 1,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
constraints = Constraints(maxWidth = width),
density = Density(density = 1f),
fontFamilyResolver =
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
index 07e2092..3d869ea 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.LineHeightStyle.Alignment
import androidx.compose.ui.text.style.LineHeightStyle.Trim
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -922,7 +923,6 @@
text: String = "",
style: TextStyle? = null,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
width: Float = Float.MAX_VALUE
): Paragraph {
@@ -939,7 +939,7 @@
)
.merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 1d8aa21..e12b98e 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.em
@@ -759,7 +760,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@@ -783,7 +784,7 @@
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
height = fontSizeInPx,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val box = paragraph.getBoundingBox(5)
@@ -806,7 +807,7 @@
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
height = fontSizeInPx,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val box = paragraph.getBoundingBox(4)
@@ -828,7 +829,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1
)
@@ -851,7 +852,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
height = fontSizeInPx
)
@@ -874,7 +875,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
width = 3 * fontSizeInPx,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
height = fontSizeInPx
)
@@ -2008,7 +2009,8 @@
density = defaultDensity,
fontFamilyResolver = resourceLoader,
// just have 10x font size to have a bitmap
- constraints = Constraints(maxWidth = (fontSizeInPx * 10).ceilToInt())
+ constraints = Constraints(maxWidth = (fontSizeInPx * 10).ceilToInt()),
+ overflow = TextOverflow.Clip
)
paragraph.bitmap()
@@ -2297,7 +2299,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesSmallerThanTextLines_returnsTrue() {
val text = "aaa\naa"
val maxLines = text.lines().size - 1
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isTrue()
}
@@ -2306,7 +2309,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesEqualToTextLines_returnsFalse() {
val text = "aaa\naa"
val maxLines = text.lines().size
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2315,7 +2319,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesGreaterThanTextLines_returnsFalse() {
val text = "aaa\naa"
val maxLines = text.lines().size + 1
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2332,7 +2337,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
// One line can only contain 1 character
width = fontSizeInPx
)
@@ -2345,7 +2350,8 @@
fun didExceedMaxLines_ellipsis_withMaxLinesEqualToTextLines_withLineWrap_returnsFalse() {
val text = "a"
val maxLines = text.lines().size
- val paragraph = simpleParagraph(text = text, maxLines = maxLines, ellipsis = true)
+ val paragraph =
+ simpleParagraph(text = text, maxLines = maxLines, overflow = TextOverflow.Ellipsis)
assertThat(paragraph.didExceedMaxLines).isFalse()
}
@@ -2362,7 +2368,7 @@
text = text,
style = TextStyle(fontSize = fontSize),
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
// One line can only contain 1 character
width = fontSizeInPx
)
@@ -2772,7 +2778,7 @@
simpleParagraph(
text = text,
maxLines = 1,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
style = TextStyle(),
width = Float.MAX_VALUE
)
@@ -2883,7 +2889,12 @@
val text = "aaa\nbbb\nccc"
val paragraph =
- simpleParagraph(text = text, maxLines = 2, ellipsis = true, width = Float.MAX_VALUE)
+ simpleParagraph(
+ text = text,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ width = Float.MAX_VALUE
+ )
assertThat(paragraph.lineCount).isEqualTo(2)
assertThat(paragraph.getLineEnd(0)).isEqualTo(4)
@@ -2903,7 +2914,7 @@
text = text,
style = TextStyle(fontFamily = fontFamilyMeasureFont, fontSize = 10.sp),
maxLines = 2,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = 50f
)
@@ -3028,7 +3039,7 @@
text = text,
style = textStyle,
maxLines = maxLines,
- ellipsis = true,
+ overflow = TextOverflow.Ellipsis,
width = 480f // px
)
as AndroidParagraph
@@ -4550,7 +4561,8 @@
val paragraph =
Paragraph(
paragraphIntrinsics = paragraphIntrinsics,
- constraints = Constraints(maxWidth = (fontSizeInPx * text.length).ceilToInt())
+ constraints = Constraints(maxWidth = (fontSizeInPx * text.length).ceilToInt()),
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.maxIntrinsicWidth).isEqualTo(paragraphIntrinsics.maxIntrinsicWidth)
@@ -4807,7 +4819,7 @@
text: String = "",
style: TextStyle? = null,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
density: Density? = null,
width: Float = Float.MAX_VALUE,
@@ -4818,7 +4830,7 @@
spanStyles = spanStyles,
style = TextStyle(fontFamily = fontFamilyMeasureFont).merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = density ?: defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
index db2bd03..df5f5e6 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTextDirectionTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -64,7 +65,8 @@
style = TextStyle(textDirection = TextDirection.Unspecified),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Ltr)
@@ -80,7 +82,8 @@
style = TextStyle(textDirection = TextDirection.Unspecified),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Rtl)
@@ -98,7 +101,8 @@
),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Ltr)
@@ -116,7 +120,8 @@
),
constraints = Constraints(),
density = defaultDensity,
- fontFamilyResolver = resourceLoader
+ fontFamilyResolver = resourceLoader,
+ overflow = TextOverflow.Clip
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(ResolvedTextDirection.Rtl)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
index 6fad0af..e475f7f 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphPlaceholderIntegrationTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.text.android.style.ceilToInt
import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -504,7 +505,7 @@
fontSize = fontSize.sp,
width = 2 * fontSize,
maxLines = 1,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -531,7 +532,7 @@
fontSize = fontSize.sp,
width = 2 * fontSize,
height = 1.3f * fontSize,
- ellipsis = true
+ overflow = TextOverflow.Ellipsis
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -559,7 +560,6 @@
width = 2 * fontSize,
height = fontSize,
maxLines = 1,
- ellipsis = false
)
val placeholderRects = paragraph.placeholderRects
assertThat(placeholderRects.size).isEqualTo(placeholders.size)
@@ -576,7 +576,7 @@
width: Float = Float.MAX_VALUE,
height: Float = Float.MAX_VALUE,
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false
+ overflow: TextOverflow = TextOverflow.Clip
): Paragraph {
return Paragraph(
text = text,
@@ -584,7 +584,7 @@
spanStyles = spanStyles,
placeholders = placeholders,
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = defaultDensity,
fontFamilyResolver = UncachedFontFamilyResolver(context)
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index 180831d..17a6a87 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -96,6 +96,36 @@
}
@Test
+ fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsStartEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = longText,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis,
+ constraints = Constraints(maxWidth = 200)
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.width).isEqualTo(200)
+ }
+
+ @Test
+ fun width_shouldMatter_ifSoftwrapIsDisabled_butOverflowIsMiddleEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = longText,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis,
+ constraints = Constraints(maxWidth = 200)
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.width).isEqualTo(200)
+ }
+
+ @Test
fun width_shouldBeMaxIntrinsicWidth_ifSoftwrapIsDisabled_andOverflowIsClip() {
val textLayoutResult =
layoutText(
@@ -144,7 +174,35 @@
}
@Test
- fun dontOverwriteMaxLines_ifSoftwrapIsEnabled() {
+ fun overwriteMaxLines_ifSoftwrapIsDisabled_andTextOverflowIsStartEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = false,
+ overflow = TextOverflow.StartEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(1)
+ }
+
+ @Test
+ fun overwriteMaxLines_ifSoftwrapIsDisabled_andTextOverflowIsMiddleEllipsis() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = false,
+ overflow = TextOverflow.MiddleEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(1)
+ }
+
+ @Test
+ fun dontOverwriteMaxLines_endEllipsis_ifSoftwrapIsEnabled() {
val textLayoutResult =
layoutText(
textLayoutInput(
@@ -158,6 +216,34 @@
}
@Test
+ fun dontOverwriteMaxLines_middleEllipsis_ifSoftwrapIsEnabled() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = true,
+ overflow = TextOverflow.MiddleEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(5)
+ }
+
+ @Test
+ fun dontOverwriteMaxLines_startEllipsis_ifSoftwrapIsEnabled() {
+ val textLayoutResult =
+ layoutText(
+ textLayoutInput(
+ text = multiLineText,
+ softWrap = true,
+ overflow = TextOverflow.StartEllipsis
+ )
+ )
+
+ assertThat(textLayoutResult.multiParagraph.lineCount).isEqualTo(5)
+ }
+
+ @Test
fun disabledSoftwrap_andOverflowClip_shouldConstrainLayoutSize() {
val textLayoutResult =
layoutText(
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
index 64d9fdb..c6b7358 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidParagraphGetRangeForRectTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.rangeOf
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
@@ -500,7 +501,6 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
textIndent: TextIndent? = null,
textAlign: TextAlign = TextAlign.Unspecified,
- ellipsis: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
width: Float,
height: Float = Float.POSITIVE_INFINITY,
@@ -519,7 +519,7 @@
)
.merge(style),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt(), maxHeight = height.ceilToInt()),
density = Density(density = 1f),
fontFamilyResolver = fontFamilyResolver
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
index 2187744..55b2fac 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/style/TextLineBreaker.kt
@@ -43,7 +43,7 @@
placeholders = listOf(),
style = textStyle,
maxLines = Int.MAX_VALUE,
- ellipsis = false,
+ overflow = TextOverflow.Clip,
constraints =
Constraints(maxWidth = maxWidth, maxHeight = Float.POSITIVE_INFINITY.ceilToInt()),
density = density,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index dfe0975..7425e87 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -82,6 +82,10 @@
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
+import androidx.compose.ui.text.style.TextOverflow.Companion.MiddleEllipsis
+import androidx.compose.ui.text.style.TextOverflow.Companion.StartEllipsis
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
@@ -97,7 +101,7 @@
internal class AndroidParagraph(
val paragraphIntrinsics: AndroidParagraphIntrinsics,
val maxLines: Int,
- val ellipsis: Boolean,
+ val overflow: TextOverflow,
val constraints: Constraints
) : Paragraph {
constructor(
@@ -106,7 +110,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
fontFamilyResolver: FontFamily.Resolver,
density: Density
@@ -121,7 +125,7 @@
density = density
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = overflow,
constraints = constraints
)
@@ -139,7 +143,7 @@
val style = paragraphIntrinsics.style
charSequence =
- if (shouldAttachIndentationFixSpan(style, ellipsis)) {
+ if (shouldAttachIndentationFixSpan(style, overflow == Ellipsis)) {
// When letter spacing, align and ellipsize applied to text, the ellipsized line is
// indented wrong. This function adds the IndentationFixSpan in order to fix the
// issue
@@ -164,13 +168,14 @@
val lineBreakWordStyle = toLayoutLineBreakWordStyle(style.lineBreak.wordBreak)
val ellipsize =
- if (ellipsis) {
- TextUtils.TruncateAt.END
- } else {
- null
+ when (overflow) {
+ Ellipsis -> TextUtils.TruncateAt.END
+ MiddleEllipsis -> TextUtils.TruncateAt.MIDDLE
+ StartEllipsis -> TextUtils.TruncateAt.START
+ else -> null
}
- val firstLayout =
+ var firstLayout =
constructTextLayout(
alignment = alignment,
justificationMode = justificationMode,
@@ -182,8 +187,42 @@
lineBreakWordStyle = lineBreakWordStyle
)
- // Ellipsize if there's not enough vertical space to fit all lines
- if (ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
+ // In case of start/middle ellipsis when the letter spacing is enabled and some of the
+ // characters are ellipsized away, we need to remeasure. This is because though
+ // internally ellipsized character are replaced with zero-width U+FEFF character, the
+ // letter spacing is still applied to each such character. It's been fixed on API 35
+ // where letter spacing won't be applied to some special characters including U+FEFF.
+ if (
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM &&
+ textPaint.letterSpacing != 0f &&
+ (overflow == StartEllipsis || overflow == MiddleEllipsis) &&
+ firstLayout.getLineEllipsisCount(0) > 0
+ ) {
+ val beforeEllipsis = firstLayout.getLineEllipsisOffset(0)
+ val afterEllipsis = beforeEllipsis + firstLayout.getLineEllipsisCount(0)
+ val newSpannable =
+ TextUtils.concat(
+ charSequence.subSequence(0, beforeEllipsis),
+ Typography.ellipsis.toString(),
+ charSequence.subSequence(afterEllipsis, charSequence.length)
+ )
+ firstLayout =
+ constructTextLayout(
+ alignment = alignment,
+ justificationMode = justificationMode,
+ ellipsize = ellipsize,
+ maxLines = maxLines,
+ hyphens = hyphens,
+ breakStrategy = breakStrategy,
+ lineBreakStyle = lineBreakStyle,
+ lineBreakWordStyle = lineBreakWordStyle,
+ charSequence = newSpannable
+ )
+ }
+
+ // Ellipsize if there's not enough vertical space to fit all lines. Because this only makes
+ // sense for end ellipsis because start/middle only works for a single line.
+ if (overflow == Ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
val calculatedMaxLines =
firstLayout.numberOfLinesThatFitMaxHeight(constraints.maxHeight)
layout =
@@ -194,10 +233,8 @@
ellipsize = ellipsize,
// When we can't fully fit even a single line, measure with one line anyway.
// This will allow to have an ellipsis on that single line. If we measured
- // with
- // 0 maxLines, it would measure all lines with no ellipsis even though the
- // first
- // line might be partially visible
+ // with 0 maxLines, it would measure all lines with no ellipsis even though
+ // the first line might be partially visible
maxLines = calculatedMaxLines.coerceAtLeast(1),
hyphens = hyphens,
breakStrategy = breakStrategy,
@@ -443,6 +480,11 @@
override fun isLineEllipsized(lineIndex: Int): Boolean = layout.isLineEllipsized(lineIndex)
+ internal fun getLineEllipsisOffset(lineIndex: Int): Int =
+ layout.getLineEllipsisOffset(lineIndex)
+
+ internal fun getLineEllipsisCount(lineIndex: Int): Int = layout.getLineEllipsisCount(lineIndex)
+
override fun getLineForOffset(offset: Int): Int = layout.getLineForOffset(offset)
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
@@ -554,7 +596,8 @@
hyphens: Int,
breakStrategy: Int,
lineBreakStyle: Int,
- lineBreakWordStyle: Int
+ lineBreakWordStyle: Int,
+ charSequence: CharSequence = this.charSequence,
) =
TextLayout(
charSequence = charSequence,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
index 06e3aef..87c2aed 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
@@ -313,7 +313,9 @@
false
} else {
/* When maxLines exceeds
- 1. if ellipsis is applied, ellipsisCount of lastLine is greater than 0.
+ 1. if ellipsis is applied, ellipsisCount of lastLine is greater than 0. It works
+ for all ellipsis position because start/middle ellipsis only supported for a single
+ line text.
2. if ellipsis is not applies, lineEnd of the last line is unequals to
charSequence.length.
On certain cases, even though ellipsize is set, text overflow might still be
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
index 0cd4880..2cf1aa8 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/ActualParagraph.android.kt
@@ -29,6 +29,7 @@
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -62,7 +63,7 @@
density = density
),
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt())
)
@@ -72,7 +73,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -87,19 +88,19 @@
density = density
),
maxLines,
- ellipsis,
+ overflow,
constraints
)
internal actual fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph =
AndroidParagraph(
paragraphIntrinsics as AndroidParagraphIntrinsics,
maxLines,
- ellipsis,
+ overflow,
constraints
)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index b73a903..80672e0 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -345,7 +345,7 @@
// Sort all span start and end points.
// S1--S2--E1--S3--E3--E2
val spanCount = spanStyles.size
- val transitionOffsets = Array(spanCount * 2) { 0 }
+ val transitionOffsets = IntArray(spanCount * 2)
spanStyles.fastForEachIndexed { idx, spanStyle ->
transitionOffsets[idx] = spanStyle.start
transitionOffsets[idx + spanCount] = spanStyle.end
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index 6d265c5..fff373a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -33,6 +33,7 @@
import androidx.compose.ui.text.platform.drawMultiParagraph
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastFlatMap
@@ -48,13 +49,14 @@
* define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number of
* lines that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
* @param maxLines the maximum number of lines that the text can have
- * @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
+ * @param overflow configures how visual overflow is handled. Ellipsis is applied only when
+ * [maxLines] is set
*/
class MultiParagraph(
val intrinsics: MultiParagraphIntrinsics,
constraints: Constraints,
val maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false,
+ overflow: TextOverflow = TextOverflow.Clip,
) {
/**
@@ -62,6 +64,31 @@
* [ParagraphStyle]s in a given text.
*
* @param intrinsics previously calculated text intrinsics
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number
+ * of lines that fit with ellipsis is true. Minimum components of the [Constraints] object are
+ * no-op.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
+ */
+ @Deprecated("Constructor with `ellipsis: Boolean` is deprecated, pass TextOverflow instead ")
+ constructor(
+ intrinsics: MultiParagraphIntrinsics,
+ constraints: Constraints,
+ maxLines: Int = DefaultMaxLines,
+ ellipsis: Boolean,
+ ) : this(
+ intrinsics = intrinsics,
+ constraints = constraints,
+ maxLines = maxLines,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip
+ )
+
+ /**
+ * Lays out and renders multiple paragraphs at once. Unlike [Paragraph], supports multiple
+ * [ParagraphStyle]s in a given text.
+ *
+ * @param intrinsics previously calculated text intrinsics
* @param maxLines the maximum number of lines that the text can have
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
* @param width how wide the text is allowed to be
@@ -80,7 +107,12 @@
maxLines: Int = DefaultMaxLines,
ellipsis: Boolean = false,
width: Float
- ) : this(intrinsics, Constraints(maxWidth = width.ceilToInt()), maxLines, ellipsis)
+ ) : this(
+ intrinsics,
+ Constraints(maxWidth = width.ceilToInt()),
+ maxLines,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip
+ )
/**
* Lays out a given [annotatedString] with the given constraints. Unlike a [Paragraph],
@@ -180,7 +212,7 @@
fontFamilyResolver = fontFamilyResolver
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
constraints = Constraints(maxWidth = width.ceilToInt())
)
@@ -206,6 +238,7 @@
* [placeholders] crosses paragraph boundary.
* @see Placeholder
*/
+ @Deprecated("Constructor with `ellipsis: Boolean` is deprecated, pass TextOverflow instead")
constructor(
annotatedString: AnnotatedString,
style: TextStyle,
@@ -214,7 +247,7 @@
fontFamilyResolver: FontFamily.Resolver,
placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
maxLines: Int = Int.MAX_VALUE,
- ellipsis: Boolean = false
+ ellipsis: Boolean
) : this(
intrinsics =
MultiParagraphIntrinsics(
@@ -225,7 +258,53 @@
fontFamilyResolver = fontFamilyResolver
),
maxLines = maxLines,
- ellipsis = ellipsis,
+ overflow = if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints = constraints
+ )
+
+ /**
+ * Lays out a given [annotatedString] with the given constraints. Unlike a [Paragraph],
+ * [MultiParagraph] can handle a text what has multiple paragraph styles.
+ *
+ * @param annotatedString the text to be laid out
+ * @param style the [TextStyle] to be applied to the whole text
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the number
+ * of lines that fit with ellipsis is true. Minimum components of the [Constraints] object are
+ * no-op.
+ * @param density density of the device
+ * @param fontFamilyResolver to be used to load the font given in [SpanStyle]s
+ * @param placeholders a list of [Placeholder]s that specify ranges of text which will be
+ * skipped during layout and replaced with [Placeholder]. It's required that the range of each
+ * [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is
+ * thrown.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow configures how visual overflow is handled. Ellipsis is applied only when
+ * [maxLines] is set
+ * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set, or any of the
+ * [placeholders] crosses paragraph boundary.
+ * @see Placeholder
+ */
+ constructor(
+ annotatedString: AnnotatedString,
+ style: TextStyle,
+ constraints: Constraints,
+ density: Density,
+ fontFamilyResolver: FontFamily.Resolver,
+ placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
+ maxLines: Int = Int.MAX_VALUE,
+ overflow: TextOverflow = TextOverflow.Clip
+ ) : this(
+ intrinsics =
+ MultiParagraphIntrinsics(
+ annotatedString = annotatedString,
+ style = style,
+ placeholders = placeholders,
+ density = density,
+ fontFamilyResolver = fontFamilyResolver
+ ),
+ maxLines = maxLines,
+ overflow = overflow,
constraints = constraints
)
@@ -253,7 +332,7 @@
/**
* The amount of vertical space this paragraph occupies.
*
- * Valid only after [layout] has been called.
+ * Valid only after layout has been called.
*/
val height: Float
@@ -326,7 +405,7 @@
}
),
maxLines - currentLineCount,
- ellipsis,
+ overflow,
)
val paragraphTop = currentHeight
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index 81cec32a..36ba20c 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.text.platform.ActualParagraph
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.ceil
@@ -472,7 +473,7 @@
spanStyles,
placeholders,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt()),
density,
fontFamilyResolver
@@ -499,6 +500,7 @@
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
* @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set
*/
+@Deprecated("Paragraph that takes `ellipsis: Boolean` is deprecated, pass TextOverflow instead.")
fun Paragraph(
text: String,
style: TextStyle,
@@ -508,7 +510,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false
+ ellipsis: Boolean
): Paragraph =
ActualParagraph(
text,
@@ -516,7 +518,51 @@
spanStyles,
placeholders,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints,
+ density,
+ fontFamilyResolver
+ )
+
+/**
+ * Lays out a given [text] with the given constraints. A paragraph is a text that has a single
+ * [ParagraphStyle].
+ *
+ * If the [style] does not contain any [androidx.compose.ui.text.style.TextDirection],
+ * [androidx.compose.ui.text.style.TextDirection.Content] is used as the default value.
+ *
+ * @param text the text to be laid out
+ * @param style the [TextStyle] to be applied to the whole text
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the Paragraph. [Constraints.maxHeight] helps defining the number of lines
+ * that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
+ * @param density density of the device
+ * @param fontFamilyResolver [FontFamily.Resolver] to be used to load the font given in [SpanStyle]s
+ * @param spanStyles [SpanStyle]s to be applied to parts of text
+ * @param placeholders a list of placeholder metrics which tells [Paragraph] where should be left
+ * blank to leave space for inline elements.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow specifies how visual overflow should be handled
+ * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set
+ */
+fun Paragraph(
+ text: String,
+ style: TextStyle,
+ constraints: Constraints,
+ density: Density,
+ fontFamilyResolver: FontFamily.Resolver,
+ spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
+ placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
+ maxLines: Int = DefaultMaxLines,
+ overflow: TextOverflow = TextOverflow.Clip
+): Paragraph =
+ ActualParagraph(
+ text,
+ style,
+ spanStyles,
+ placeholders,
+ maxLines,
+ overflow,
constraints,
density,
fontFamilyResolver
@@ -549,7 +595,7 @@
ActualParagraph(
paragraphIntrinsics,
maxLines,
- ellipsis,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
Constraints(maxWidth = width.ceilToInt())
)
@@ -564,11 +610,42 @@
* @param maxLines the maximum number of lines that the text can have
* @param ellipsis whether to ellipsize text, applied only when [maxLines] is set
*/
+@Deprecated(
+ "Paragraph that takes ellipsis: Boolean is deprecated, pass TextOverflow instead.",
+ ReplaceWith(
+ "Paragraph(paragraphIntrinsics, constraints, maxLines, " +
+ "if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip"
+ )
+)
fun Paragraph(
paragraphIntrinsics: ParagraphIntrinsics,
constraints: Constraints,
maxLines: Int = DefaultMaxLines,
- ellipsis: Boolean = false
-): Paragraph = ActualParagraph(paragraphIntrinsics, maxLines, ellipsis, constraints)
+ ellipsis: Boolean
+): Paragraph =
+ ActualParagraph(
+ paragraphIntrinsics,
+ maxLines,
+ if (ellipsis) TextOverflow.Ellipsis else TextOverflow.Clip,
+ constraints
+ )
+
+/**
+ * Lays out the text in [ParagraphIntrinsics] with the given constraints. A paragraph is a text that
+ * has a single [ParagraphStyle].
+ *
+ * @param paragraphIntrinsics [ParagraphIntrinsics] instance
+ * @param constraints how wide and tall the text is allowed to be. [Constraints.maxWidth] will
+ * define the width of the Paragraph. [Constraints.maxHeight] helps defining the number of lines
+ * that fit with ellipsis is true. Minimum components of the [Constraints] object are no-op.
+ * @param maxLines the maximum number of lines that the text can have
+ * @param overflow specifies how visual overflow should be handled
+ */
+fun Paragraph(
+ paragraphIntrinsics: ParagraphIntrinsics,
+ constraints: Constraints,
+ maxLines: Int = DefaultMaxLines,
+ overflow: TextOverflow = TextOverflow.Clip
+): Paragraph = ActualParagraph(paragraphIntrinsics, maxLines, overflow, constraints)
internal fun Float.ceilToInt(): Int = ceil(this).toInt()
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index 5cbc366..cd65c98 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -277,7 +277,7 @@
)
val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val widthMatters = softWrap || overflow.isEllipsis
val maxWidth =
if (widthMatters && constraints.hasBoundedWidth) {
constraints.maxWidth
@@ -300,7 +300,7 @@
// AA…
// Here we assume there won't be any '\n' character when softWrap is false. And make
// maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ val overwriteMaxLines = !softWrap && overflow.isEllipsis
val finalMaxLines = if (overwriteMaxLines) 1 else maxLines
// if minWidth == maxWidth the width is fixed.
@@ -332,7 +332,7 @@
),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
- ellipsis = overflow == TextOverflow.Ellipsis
+ overflow = overflow
)
return TextLayoutResult(
@@ -447,3 +447,10 @@
return true
}
}
+
+private val TextOverflow.isEllipsis: Boolean
+ get() {
+ return this == TextOverflow.Ellipsis ||
+ this == TextOverflow.StartEllipsis ||
+ this == TextOverflow.MiddleEllipsis
+ }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
index 3651805..c590354 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/PlatformParagraph.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -54,7 +55,7 @@
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -64,7 +65,7 @@
internal expect fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
index 97dc5fa..8290cc6 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextOverflow.kt
@@ -26,7 +26,9 @@
return when (this) {
Clip -> "Clip"
Ellipsis -> "Ellipsis"
+ MiddleEllipsis -> "MiddleEllipsis"
Visible -> "Visible"
+ StartEllipsis -> "StartEllipsis"
else -> "Invalid"
}
}
@@ -40,7 +42,9 @@
@Stable val Clip = TextOverflow(1)
/**
- * Use an ellipsis to indicate that the text has overflowed.
+ * Use an ellipsis at the end of the string to indicate that the text has overflowed.
+ *
+ * For example, [This is a ...].
*
* @sample androidx.compose.ui.text.samples.TextOverflowEllipsisSample
*/
@@ -66,5 +70,29 @@
* such as `Modifier.clipToBounds`.
*/
@Stable val Visible = TextOverflow(3)
+
+ /**
+ * Use an ellipsis at the start of the string to indicate that the text has overflowed.
+ *
+ * For example, [... is a text].
+ *
+ * Note that not all platforms support the ellipsis at the start. For example, on Android
+ * the start ellipsis is only available for a single line text (i.e. when either a soft wrap
+ * is disabled or a maximum number of lines maxLines set to 1). In case of multiline text it
+ * will fallback to [Clip].
+ */
+ @Stable val StartEllipsis = TextOverflow(4)
+
+ /**
+ * Use an ellipsis in the middle of the string to indicate that the text has overflowed.
+ *
+ * For example, [This ... text].
+ *
+ * Note that not all platforms support the ellipsis in the middle. For example, on Android
+ * the middle ellipsis is only available for a single line text (i.e. when either a soft
+ * wrap is disabled or a maximum number of lines maxLines set to 1). In case of multiline
+ * text it will fallback to [Clip].
+ */
+ @Stable val MiddleEllipsis = TextOverflow(5)
}
}
diff --git a/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt b/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
index fb9de57..a624af7 100644
--- a/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
+++ b/compose/ui/ui-text/src/commonStubsMain/kotlin/androidx/compose/ui/text/platform/SkiaParagraph.commonStubs.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.implementedInJetBrainsFork
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -45,7 +46,7 @@
spanStyles: List<Range<SpanStyle>>,
placeholders: List<Range<Placeholder>>,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints,
density: Density,
fontFamilyResolver: FontFamily.Resolver
@@ -54,6 +55,6 @@
internal actual fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
- ellipsis: Boolean,
+ overflow: TextOverflow,
constraints: Constraints
): Paragraph = implementedInJetBrainsFork()
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index c372803..b0debd1 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -226,7 +226,9 @@
}
public static final class IntOffset.Companion {
+ method public long getMax();
method public long getZero();
+ property public final long Max;
property public final long Zero;
}
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index 6f13e9f..4fc0dc1 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -226,7 +226,9 @@
}
public static final class IntOffset.Companion {
+ method public long getMax();
method public long getZero();
+ property public final long Max;
property public final long Zero;
}
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 5dc4eb6..9b8d345 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -14,11 +14,15 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
package androidx.compose.ui.unit
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.Constraints.Companion.Infinity
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceIn
import kotlin.jvm.JvmInline
import kotlin.math.min
@@ -416,13 +420,13 @@
private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
// Wrap those throws in functions to avoid inlining the string building at the call sites
-private fun invalidConstraint(widthVal: Int, heightVal: Int) {
+private fun throwInvalidConstraintException(widthVal: Int, heightVal: Int) {
throw IllegalArgumentException(
"Can't represent a width of $widthVal and height of $heightVal in Constraints"
)
}
-private fun invalidSize(size: Int): Nothing {
+private fun throwInvalidSizeException(size: Int): Nothing {
throw IllegalArgumentException("Can't represent a size of $size in Constraints")
}
@@ -440,7 +444,7 @@
val widthBits = bitsNeedForSizeUnchecked(widthVal)
if (widthBits + heightBits > 31) {
- invalidConstraint(widthVal, heightVal)
+ throwInvalidConstraintException(widthVal, heightVal)
}
// Same as if (maxWidth == Infinity) 0 else maxWidth + 1 but branchless
@@ -489,7 +493,7 @@
size < MinNonFocusMask -> MaxAllowedForMinNonFocusBits
size < MinFocusMask -> MaxAllowedForMinFocusBits
size < MaxFocusMask -> MaxAllowedForMaxFocusBits
- else -> invalidSize(size)
+ else -> throwInvalidSizeException(size)
}
}
@@ -501,9 +505,9 @@
@Stable
fun Constraints(
minWidth: Int = 0,
- maxWidth: Int = Constraints.Infinity,
+ maxWidth: Int = Infinity,
minHeight: Int = 0,
- maxHeight: Int = Constraints.Infinity
+ maxHeight: Int = Infinity
): Constraints {
requirePrecondition(maxWidth >= minWidth) {
"maxWidth($maxWidth) must be >= than minWidth($minWidth)"
@@ -528,25 +532,25 @@
*/
fun Constraints.constrain(otherConstraints: Constraints) =
Constraints(
- minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth),
- maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth),
- minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight),
- maxHeight = otherConstraints.maxHeight.coerceIn(minHeight, maxHeight)
+ minWidth = otherConstraints.minWidth.fastCoerceIn(minWidth, maxWidth),
+ maxWidth = otherConstraints.maxWidth.fastCoerceIn(minWidth, maxWidth),
+ minHeight = otherConstraints.minHeight.fastCoerceIn(minHeight, maxHeight),
+ maxHeight = otherConstraints.maxHeight.fastCoerceIn(minHeight, maxHeight)
)
/** Takes a size and returns the closest size to it that satisfies the constraints. */
@Stable
fun Constraints.constrain(size: IntSize) =
IntSize(
- width = size.width.coerceIn(minWidth, maxWidth),
- height = size.height.coerceIn(minHeight, maxHeight)
+ width = size.width.fastCoerceIn(minWidth, maxWidth),
+ height = size.height.fastCoerceIn(minHeight, maxHeight)
)
/** Takes a width and returns the closest size to it that satisfies the constraints. */
-@Stable fun Constraints.constrainWidth(width: Int) = width.coerceIn(minWidth, maxWidth)
+@Stable fun Constraints.constrainWidth(width: Int) = width.fastCoerceIn(minWidth, maxWidth)
/** Takes a height and returns the closest size to it that satisfies the constraints. */
-@Stable fun Constraints.constrainHeight(height: Int) = height.coerceIn(minHeight, maxHeight)
+@Stable fun Constraints.constrainHeight(height: Int) = height.fastCoerceIn(minHeight, maxHeight)
/** Takes a size and returns whether it satisfies the current constraints. */
@Stable
@@ -558,17 +562,17 @@
@Stable
fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) =
Constraints(
- (minWidth + horizontal).coerceAtLeast(0),
+ (minWidth + horizontal).fastCoerceAtLeast(0),
addMaxWithMinimum(maxWidth, horizontal),
- (minHeight + vertical).coerceAtLeast(0),
+ (minHeight + vertical).fastCoerceAtLeast(0),
addMaxWithMinimum(maxHeight, vertical)
)
private fun addMaxWithMinimum(max: Int, value: Int): Int {
- return if (max == Constraints.Infinity) {
+ return if (max == Infinity) {
max
} else {
- (max + value).coerceAtLeast(0)
+ (max + value).fastCoerceAtLeast(0)
}
}
@@ -630,7 +634,6 @@
* compute other values without the need of lookup tables. For instance, [minHeightOffsets] returns
* `2 + 13 + bitOffset`.
*/
-@Suppress("NOTHING_TO_INLINE")
private inline fun indexToBitOffset(index: Int) =
(index and 0x1 shl 1) + ((index and 0x2 shr 1) * 3)
@@ -638,12 +641,10 @@
* Minimum Height shift offsets into Long value, indexed by FocusMask Max offsets are these + 31
* Width offsets are always either 2 (min) or 33 (max)
*/
-@Suppress("NOTHING_TO_INLINE") private inline fun minHeightOffsets(bitOffset: Int) = 15 + bitOffset
+private inline fun minHeightOffsets(bitOffset: Int) = 15 + bitOffset
/** The mask to use for both minimum and maximum width. */
-@Suppress("NOTHING_TO_INLINE")
private inline fun widthMask(bitOffset: Int) = (1 shl (13 + bitOffset)) - 1
/** The mask to use for both minimum and maximum height. */
-@Suppress("NOTHING_TO_INLINE")
private inline fun heightMask(bitOffset: Int) = (1 shl (18 - bitOffset)) - 1
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
index ca4caa3..89bf3f1 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntOffset.kt
@@ -137,6 +137,7 @@
companion object {
val Zero = IntOffset(0x0L)
+ val Max = IntOffset(0x7FFF_FFFF_7FFF_FFFF)
}
}
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..bc950b7 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -131,6 +131,11 @@
method public <R> R foldOut(R initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposeUiFlags {
+ field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
+ field public static boolean isRectTrackingEnabled;
+ }
+
public final class ComposedModifierKt {
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, Object? key3, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
@@ -293,14 +298,26 @@
ctor public DragAndDropEvent(android.view.DragEvent dragEvent);
}
- public interface DragAndDropModifierNode extends androidx.compose.ui.node.DelegatableNode androidx.compose.ui.draganddrop.DragAndDropTarget {
- method public boolean acceptDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropEvent startEvent);
- method public void drag(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
+ @Deprecated public interface DragAndDropModifierNode extends androidx.compose.ui.node.DelegatableNode androidx.compose.ui.draganddrop.DragAndDropTarget {
+ method @Deprecated public boolean acceptDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropEvent startEvent);
+ method @Deprecated public void drag(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
}
public final class DragAndDropNodeKt {
- method public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode();
- method public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ method @Deprecated public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode();
+ method @Deprecated public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ method public static androidx.compose.ui.draganddrop.DragAndDropSourceModifierNode DragAndDropSourceModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.draganddrop.DragAndDropStartTransferScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onStartTransfer);
+ method public static androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode DragAndDropTargetModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ }
+
+ public sealed interface DragAndDropSourceModifierNode extends androidx.compose.ui.node.LayoutAwareModifierNode {
+ method public boolean isRequestDragAndDropTransferRequired();
+ method public void requestDragAndDropTransfer(long offset);
+ property public abstract boolean isRequestDragAndDropTransferRequired;
+ }
+
+ public interface DragAndDropStartTransferScope {
+ method public boolean startDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
}
public interface DragAndDropTarget {
@@ -313,6 +330,9 @@
method public default void onStarted(androidx.compose.ui.draganddrop.DragAndDropEvent event);
}
+ public sealed interface DragAndDropTargetModifierNode extends androidx.compose.ui.node.LayoutAwareModifierNode {
+ }
+
public final class DragAndDropTransferData {
ctor public DragAndDropTransferData(android.content.ClipData clipData, optional Object? localState, optional int flags);
method public android.content.ClipData getClipData();
@@ -2338,20 +2358,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..9d1d078 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -131,6 +131,11 @@
method public <R> R foldOut(R initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final class ComposeUiFlags {
+ field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
+ field public static boolean isRectTrackingEnabled;
+ }
+
public final class ComposedModifierKt {
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, Object? key3, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
method public static androidx.compose.ui.Modifier composed(androidx.compose.ui.Modifier, String fullyQualifiedName, Object? key1, Object? key2, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo, kotlin.jvm.functions.Function1<? super androidx.compose.ui.Modifier,? extends androidx.compose.ui.Modifier> factory);
@@ -293,14 +298,26 @@
ctor public DragAndDropEvent(android.view.DragEvent dragEvent);
}
- public interface DragAndDropModifierNode extends androidx.compose.ui.node.DelegatableNode androidx.compose.ui.draganddrop.DragAndDropTarget {
- method public boolean acceptDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropEvent startEvent);
- method public void drag(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
+ @Deprecated public interface DragAndDropModifierNode extends androidx.compose.ui.node.DelegatableNode androidx.compose.ui.draganddrop.DragAndDropTarget {
+ method @Deprecated public boolean acceptDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropEvent startEvent);
+ method @Deprecated public void drag(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
}
public final class DragAndDropNodeKt {
- method public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode();
- method public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ method @Deprecated public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode();
+ method @Deprecated public static androidx.compose.ui.draganddrop.DragAndDropModifierNode DragAndDropModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ method public static androidx.compose.ui.draganddrop.DragAndDropSourceModifierNode DragAndDropSourceModifierNode(kotlin.jvm.functions.Function2<? super androidx.compose.ui.draganddrop.DragAndDropStartTransferScope,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onStartTransfer);
+ method public static androidx.compose.ui.draganddrop.DragAndDropTargetModifierNode DragAndDropTargetModifierNode(kotlin.jvm.functions.Function1<? super androidx.compose.ui.draganddrop.DragAndDropEvent,java.lang.Boolean> shouldStartDragAndDrop, androidx.compose.ui.draganddrop.DragAndDropTarget target);
+ }
+
+ public sealed interface DragAndDropSourceModifierNode extends androidx.compose.ui.node.LayoutAwareModifierNode {
+ method public boolean isRequestDragAndDropTransferRequired();
+ method public void requestDragAndDropTransfer(long offset);
+ property public abstract boolean isRequestDragAndDropTransferRequired;
+ }
+
+ public interface DragAndDropStartTransferScope {
+ method public boolean startDragAndDropTransfer(androidx.compose.ui.draganddrop.DragAndDropTransferData transferData, long decorationSize, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> drawDragDecoration);
}
public interface DragAndDropTarget {
@@ -313,6 +330,9 @@
method public default void onStarted(androidx.compose.ui.draganddrop.DragAndDropEvent event);
}
+ public sealed interface DragAndDropTargetModifierNode extends androidx.compose.ui.node.LayoutAwareModifierNode {
+ }
+
public final class DragAndDropTransferData {
ctor public DragAndDropTransferData(android.content.ClipData clipData, optional Object? localState, optional int flags);
method public android.content.ClipData getClipData();
@@ -2341,20 +2361,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
index dfd58d7..fb9f91c 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
@@ -34,9 +34,7 @@
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,7 +46,6 @@
private val nestedScrollingCaseFactory = { NestedScrollingTestCase() }
- @Ignore("b/362302352")
@Test
fun nested_scroll_propagation() {
benchmarkRule.runBenchmarkFor(nestedScrollingCaseFactory) {
@@ -101,7 +98,6 @@
private val noOpConnection = object : NestedScrollConnection {}
private val delta = Offset(200f, 200f)
private val velocity = Velocity(2000f, 200f)
- private var scrollResult = Offset.Zero
private var velocityResult = Velocity.Zero
private val IntermediateConnection = object : NestedScrollConnection {}
@@ -129,9 +125,8 @@
}
override fun toggleState() {
- scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.UserInput)
- scrollResult =
- dispatcher.dispatchPostScroll(delta, scrollResult, NestedScrollSource.UserInput)
+ val scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.UserInput)
+ dispatcher.dispatchPostScroll(delta, scrollResult, NestedScrollSource.UserInput)
runBlocking {
velocityResult = dispatcher.dispatchPreFling(velocity)
@@ -144,8 +139,5 @@
assertNotEquals(collectedDeltasMiddle, Offset.Zero)
assertNotEquals(collectedVelocityOuter, Velocity.Zero)
assertNotEquals(collectedVelocityMiddle, Velocity.Zero)
-
- assertEquals(scrollResult, collectedDeltasOuter)
- assertEquals(velocityResult, collectedVelocityOuter)
}
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/focus/FocusBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/focus/FocusBenchmark.kt
index b228824..30501e3 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/focus/FocusBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/focus/FocusBenchmark.kt
@@ -16,11 +16,20 @@
package androidx.compose.ui.benchmark.focus
+import android.view.KeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
+import android.view.KeyEvent.KEYCODE_TAB
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.LayeredComposeTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.testutils.doFramesUntilNoChangesPending
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusTarget
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -46,4 +55,29 @@
}
}
}
+
+ @Test
+ fun focusTraversal() {
+ composeBenchmarkRule.runBenchmarkFor({
+ object : ComposeTestCase {
+ @Composable
+ override fun Content() {
+ Column(Modifier.fillMaxSize()) {
+ repeat(10) {
+ Row(Modifier.focusTarget()) {
+ repeat(10) { Box(Modifier.focusTarget()) }
+ }
+ }
+ }
+ }
+ }
+ }) {
+ composeBenchmarkRule.runOnUiThread { doFramesUntilNoChangesPending() }
+
+ composeBenchmarkRule.measureRepeatedOnUiThread {
+ getHostView().dispatchKeyEvent(KeyEvent(ACTION_DOWN, KEYCODE_TAB))
+ getHostView().dispatchKeyEvent(KeyEvent(ACTION_UP, KEYCODE_TAB))
+ }
+ }
+ }
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeMultiFingerInputUIOnlyBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeMultiFingerInputUIOnlyBenchmark.kt
index d621768..65e7e71 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeMultiFingerInputUIOnlyBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeMultiFingerInputUIOnlyBenchmark.kt
@@ -90,6 +90,13 @@
clickOnItem(item = 0, expectedLabel = "0", numberOfMoves = 6, numberOfFingers = 3)
}
+ @Test
+ fun clickWith100MovesOnLateItem() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at 0 will be hit tested late.
+ clickOnItem(item = 0, expectedLabel = "0", numberOfMoves = 100, numberOfFingers = 3)
+ }
+
// This test requires less hit testing so changes to dispatch will be tracked more by this test.
@Test
fun clickWithMoveOnEarlyItemFyi() {
@@ -105,6 +112,19 @@
}
@Test
+ fun clickWith100MovesOnEarlyItemFyi() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at NumItems - 1 will be hit tested early.
+ val lastItem = NumItems - 1
+ clickOnItem(
+ item = lastItem,
+ expectedLabel = "$lastItem",
+ numberOfMoves = 100,
+ numberOfFingers = 3
+ )
+ }
+
+ @Test
fun clickWithMoveAndFlingHistoryOnLateItem() {
// As items that are laid out last are hit tested first (so z order is respected), item
// at 0 will be hit tested late.
@@ -117,6 +137,19 @@
)
}
+ @Test
+ fun clickWith100MovesAndFlingHistoryOnLateItem() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at 0 will be hit tested late.
+ clickOnItem(
+ item = 0,
+ expectedLabel = "0",
+ numberOfMoves = 100,
+ numberOfFingers = 3,
+ enableHistory = true
+ )
+ }
+
// This test requires less hit testing so changes to dispatch will be tracked more by this test.
@Test
fun clickWithMoveAndFlingHistoryOnEarlyItemFyi() {
@@ -132,6 +165,20 @@
)
}
+ @Test
+ fun clickWith100MovesAndFlingHistoryOnEarlyItemFyi() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at NumItems - 1 will be hit tested early.
+ val lastItem = NumItems - 1
+ clickOnItem(
+ item = lastItem,
+ expectedLabel = "$lastItem",
+ numberOfMoves = 100,
+ numberOfFingers = 3,
+ enableHistory = true
+ )
+ }
+
private fun clickOnItem(
item: Int,
expectedLabel: String,
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeOneFingerInputUIOnlyBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeOneFingerInputUIOnlyBenchmark.kt
index 2bc768a..223ac2d 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeOneFingerInputUIOnlyBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeOneFingerInputUIOnlyBenchmark.kt
@@ -85,6 +85,13 @@
clickOnItem(0, "0", 6)
}
+ @Test
+ fun clickWith100MovesOnLateItem() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at 0 will be hit tested late.
+ clickOnItem(0, "0", 100)
+ }
+
// This test requires less hit testing so changes to dispatch will be tracked more by this test.
@Test
fun clickWithMoveOnEarlyItemFyi() {
@@ -95,12 +102,27 @@
}
@Test
+ fun clickWith100MovesOnEarlyItemFyi() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at NumItems - 1 will be hit tested early.
+ val lastItem = NumItems - 1
+ clickOnItem(lastItem, "$lastItem", 100)
+ }
+
+ @Test
fun clickWithMoveAndFlingHistoryOnLateItem() {
// As items that are laid out last are hit tested first (so z order is respected), item
// at 0 will be hit tested late.
clickOnItem(0, "0", 6, true)
}
+ @Test
+ fun clickWith100MovesAndFlingHistoryOnLateItem() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at 0 will be hit tested late.
+ clickOnItem(0, "0", 100, true)
+ }
+
// This test requires less hit testing so changes to dispatch will be tracked more by this test.
@Test
fun clickWithMoveAndFlingHistoryOnEarlyItemFyi() {
@@ -110,6 +132,14 @@
clickOnItem(lastItem, "$lastItem", 6, true)
}
+ @Test
+ fun clickWith100MovesAndFlingHistoryOnEarlyItemFyi() {
+ // As items that are laid out last are hit tested first (so z order is respected), item
+ // at NumItems - 1 will be hit tested early.
+ val lastItem = NumItems - 1
+ clickOnItem(lastItem, "$lastItem", 100, true)
+ }
+
private fun clickOnItem(
item: Int,
expectedLabel: String,
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt
new file mode 100644
index 0000000..957df79
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/RectListBenchmark.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+
+package androidx.compose.ui.benchmark.spatial
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.collection.mutableIntListOf
+import androidx.compose.ui.spatial.RectList
+import androidx.compose.ui.util.fastForEach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.math.max
+import kotlin.random.Random
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class RectListBenchmark {
+
+ @get:Rule val rule = BenchmarkRule()
+
+ private fun construct() = RectList()
+
+ @Test
+ fun b01_insertExampleDataLinear() {
+ val testData = exampleLayoutRects
+ rule.measureRepeated {
+ val qt = construct()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ qt.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ -1,
+ false,
+ )
+ }
+ }
+ }
+
+ private fun insertRecursive(qt: RectList, item: Item, scrollableId: Int) {
+ val bounds = item.bounds
+
+ qt.insert(
+ item.id,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ scrollableId,
+ item.scrollable,
+ )
+ item.children.fastForEach {
+ insertRecursive(qt, it, if (item.scrollable) item.id else scrollableId)
+ }
+ }
+
+ @Test
+ fun b01_insertExampleData() {
+ val item = rootItem
+ rule.measureRepeated {
+ val qt = construct()
+ insertRecursive(qt, item, -1)
+ }
+ }
+
+ @Test
+ fun b02_removeExampleData() {
+ val testData = exampleLayoutRects
+ rule.measureRepeated {
+ val grid = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in testData.indices) {
+ grid.remove(i)
+ }
+ }
+ }
+
+ @Test
+ fun b03_updateExampleItems() {
+ val testData = exampleLayoutRects
+ val r = Random(1234)
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in testData.indices) {
+ val rect = testData[i]
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ qt.update(
+ i,
+ max(rect[0] + x, 0),
+ max(rect[1] + y, 0),
+ max(rect[2] + x, 0),
+ max(rect[3] + y, 0),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun b04_updateScrollableContainer() {
+ val scrollableItems = scrollableItems
+ val r = Random(1234)
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ scrollableItems.fastForEach {
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ qt.updateSubhierarchy(it.id, x, y)
+ }
+ }
+ }
+
+ @Test
+ fun b05_findOccludingRectsExampleItems() {
+ val queries = occludingRectQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.forEachIntersection(
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) {
+ runWithTimingDisabled { list.add(it) }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b06_findKNearestNeighborsInDirection() {
+ val queries = nearestNeighborQueries
+ val numberOfResults = 4
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.findKNearestNeighbors(
+ direction,
+ numberOfResults,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) { _, id, _, _, _, _ ->
+ runWithTimingDisabled { list.add(id) }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b06_findNearestNeighborInDirection() {
+ val queries = nearestNeighborQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ val result =
+ qt.findNearestNeighbor(
+ direction,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ )
+ runWithTimingDisabled { list.add(result) }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun b07_findEligiblePointerInputs() {
+ val queries = pointerInputQueries
+ rule.measureRepeated {
+ val qt = runWithTimingDisabled {
+ val qt = construct()
+ insertRecursive(qt, rootItem, -1)
+ qt
+ }
+ for (i in queries.indices) {
+ val list = runWithTimingDisabled { mutableIntListOf() }
+ val bounds = queries[i]
+ qt.forEachIntersection(
+ bounds[0],
+ bounds[1],
+ ) {
+ runWithTimingDisabled { list.add(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt
new file mode 100644
index 0000000..ece8059
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/spatial/SpatialTestData.kt
@@ -0,0 +1,2309 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.benchmark.spatial
+
+val occludingRectQueries =
+ arrayOf(
+ intArrayOf(490, 2073, 945, 2693),
+ intArrayOf(84, 2777, 1356, 2993),
+ intArrayOf(966, 2074, 1379, 2703),
+ intArrayOf(84, 1548, 1356, 1821),
+ intArrayOf(35, 2073, 490, 2693),
+ )
+
+val nearestNeighborQueries =
+ arrayOf(
+ intArrayOf(56, 2074, 469, 2703), // left side app image
+ intArrayOf(288, 2812, 576, 3036), // bottom nav bar middle button
+ intArrayOf(1048, 478, 1187, 548), // install button
+ intArrayOf(983, 1580, 1300, 1790), // show button
+ intArrayOf(1258, 1933, 1342, 2017), // more icon
+ )
+
+val pointerInputQueries =
+ arrayOf(
+ intArrayOf(1120, 1654), // Show button
+ intArrayOf(1263, 1943), // three dots button
+ intArrayOf(615, 2312), // app image
+ intArrayOf(1100, 496), // install button
+ intArrayOf(710, 215), // search bar
+ )
+
+class Item(
+ val id: Int,
+ val bounds: IntArray,
+ val scrollable: Boolean,
+ val focusable: Boolean,
+ val pointerInput: Boolean,
+) {
+ val children: MutableList<Item> = mutableListOf()
+
+ operator fun Item.unaryPlus() {
+ @Suppress("LABEL_RESOLVE_WILL_CHANGE") [email protected](this)
+ }
+}
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+): Item = Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput)
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+ scope: Item.() -> Unit
+): Item {
+ return Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput).apply { scope() }
+}
+
+val rootItem =
+ Item(0, 0, 0, 1440, 3120, false, false, false) {
+ +Item(1, 0, 0, 1440, 3120, false, false, false) {
+ +Item(2, 0, 0, 1, 1, false, false, false)
+ +Item(3, 0, 0, 1440, 3120, false, false, false) {
+ +Item(4, 0, 0, 1440, 3120, false, false, false) {
+ +Item(5, 0, 0, 1440, 3120, false, false, false) {
+ +Item(6, 0, 0, 1440, 3120, false, false, false) {
+ +Item(7, 0, 0, 1440, 3120, false, false, false) {
+ +Item(8, 0, 0, 1440, 3120, false, false, false) {
+ +Item(9, 0, 0, 1440, 3120, false, false, false) {
+ +Item(10, 0, 0, 1440, 3036, false, false, false) {
+ +Item(11, 0, 0, 1440, 2812, false, false, false) {
+ +Item(12, 0, 0, 1440, 2812, false, false, false) {
+ +Item(
+ 13,
+ 0,
+ 0,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 14,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 15,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 16,
+ 0,
+ 145,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 17,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 18,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 19,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 20,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 21,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 22,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 23,
+ 28,
+ 187,
+ 168,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 24,
+ 56,
+ 215,
+ 140,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 25,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 26,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 27,
+ 196,
+ 216,
+ 337,
+ 300,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 28,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 29,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 30,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 31,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 32,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 33,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 34,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 35,
+ 1104,
+ 187,
+ 1244,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 36,
+ 1132,
+ 215,
+ 1216,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 37,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 38,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 39,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 40,
+ 1272,
+ 187,
+ 1412,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 41,
+ 1300,
+ 215,
+ 1384,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 42,
+ 0,
+ 369,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 43,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 44,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 45,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 46,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 47,
+ 0,
+ 1877,
+ 1440,
+ 2735,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 48,
+ 0,
+ 1877,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 49,
+ 84,
+ 1877,
+ 1356,
+ 2073,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 50,
+ 84,
+ 1947,
+ 354,
+ 2003,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 51,
+ 84,
+ 1947,
+ 302,
+ 2003,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 52,
+ 354,
+ 1933,
+ 1188,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 53,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 54,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 55,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 56,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 57,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 58,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 59,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 60,
+ 945,
+ 2073,
+ 1400,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 61,
+ 966,
+ 2074,
+ 1379,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 62,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 63,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 64,
+ 966,
+ 2515,
+ 1379,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 65,
+ 490,
+ 2073,
+ 945,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 66,
+ 511,
+ 2074,
+ 924,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 67,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 68,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 69,
+ 511,
+ 2515,
+ 924,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 70,
+ 1400,
+ 2073,
+ 1855,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 71,
+ 1421,
+ 2074,
+ 1834,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 72,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 73,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 74,
+ 1421,
+ 2515,
+ 1834,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 75,
+ 35,
+ 2073,
+ 490,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 76,
+ 56,
+ 2074,
+ 469,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 77,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 78,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 79,
+ 56,
+ 2515,
+ 469,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 80,
+ 0,
+ 2777,
+ 1440,
+ 2993,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 81,
+ 84,
+ 2777,
+ 1356,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 82,
+ 84,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 83,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 84,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 85,
+ 336,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 86,
+ 1188,
+ 2777,
+ 1356,
+ 2945,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 87,
+ 1272,
+ 2826,
+ 1356,
+ 2910,
+ false,
+ true,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 88,
+ 0,
+ 373,
+ 1440,
+ 1849,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 89,
+ 84,
+ 429,
+ 1356,
+ 645,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 90,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 91,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 92,
+ 336,
+ 436,
+ 950,
+ 652,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 93,
+ 992,
+ 429,
+ 1356,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 94,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 95,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 96,
+ 992,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 97,
+ 1048,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 98,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 99,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 100,
+ 1229,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 101,
+ 1229,
+ 443,
+ 1233,
+ 583,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 102,
+ 1233,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 103,
+ 1256,
+ 482,
+ 1319,
+ 545,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 104,
+ 1056,
+ 429,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 105,
+ 1056,
+ 583,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 106,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 107,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 108,
+ 0,
+ 687,
+ 1440,
+ 911,
+ true,
+ false,
+ false
+ ) {
+ +Item(
+ 109,
+ 0,
+ 799,
+ 84,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 110,
+ 84,
+ 729,
+ 377,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 111,
+ 171,
+ 733,
+ 291,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 112,
+ 171,
+ 733,
+ 242,
+ 803,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 113,
+ 249,
+ 747,
+ 291,
+ 789,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 114,
+ 84,
+ 810,
+ 377,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 115,
+ 338,
+ 818,
+ 377,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 116,
+ 377,
+ 799,
+ 433,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 117,
+ 433,
+ 762,
+ 437,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 118,
+ 437,
+ 799,
+ 493,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 119,
+ 493,
+ 729,
+ 839,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 120,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 121,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 122,
+ 493,
+ 810,
+ 839,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 123,
+ 799,
+ 818,
+ 838,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 124,
+ 839,
+ 799,
+ 895,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 125,
+ 895,
+ 762,
+ 899,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 126,
+ 899,
+ 799,
+ 955,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 127,
+ 955,
+ 729,
+ 1235,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 128,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 129,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 130,
+ 1022,
+ 810,
+ 1168,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 131,
+ 1129,
+ 818,
+ 1168,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 132,
+ 1235,
+ 799,
+ 1291,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 133,
+ 1291,
+ 762,
+ 1295,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 134,
+ 1295,
+ 799,
+ 1351,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 135,
+ 1351,
+ 729,
+ 1631,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 136,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 137,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 138,
+ 1383,
+ 810,
+ 1599,
+ 866,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 139,
+ 1631,
+ 799,
+ 1687,
+ 799,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 140,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 141,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 142,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 143,
+ 1348,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 144,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 145,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 146,
+ 1380,
+ 911,
+ 1975,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 147,
+ 2031,
+ 911,
+ 2588,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 148,
+ 2031,
+ 967,
+ 2588,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 149,
+ 2031,
+ 1051,
+ 2588,
+ 1121,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 150,
+ 2031,
+ 1149,
+ 2588,
+ 1289,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 151,
+ 2031,
+ 1345,
+ 2031,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 152,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 153,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 154,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 155,
+ 84,
+ 911,
+ 679,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 156,
+ 735,
+ 911,
+ 1292,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 157,
+ 735,
+ 967,
+ 1292,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 158,
+ 735,
+ 1051,
+ 1292,
+ 1191,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 159,
+ 735,
+ 1219,
+ 1292,
+ 1359,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 160,
+ 735,
+ 1415,
+ 735,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 161,
+ 0,
+ 1506,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 162,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 163,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 164,
+ 140,
+ 1548,
+ 1300,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 165,
+ 140,
+ 1604,
+ 927,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 166,
+ 140,
+ 1604,
+ 865,
+ 1688,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 167,
+ 140,
+ 1702,
+ 469,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 168,
+ 140,
+ 1702,
+ 203,
+ 1765,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 169,
+ 217,
+ 1706,
+ 469,
+ 1762,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 170,
+ 983,
+ 1580,
+ 1300,
+ 1790,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 171,
+ 1040,
+ 1615,
+ 1243,
+ 1755,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 172,
+ 1040,
+ 1654,
+ 1103,
+ 1717,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 173,
+ 1103,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 174,
+ 1117,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 175,
+ 42,
+ 2770,
+ 1398,
+ 2770,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(176, 0, 2812, 1440, 3036, false, false, false) {
+ +Item(
+ 177,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 178,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 179,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 180,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 181,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 182,
+ 46,
+ 2847,
+ 242,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 183,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 184,
+ 14,
+ 2931,
+ 274,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 185,
+ 74,
+ 2945,
+ 214,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 186,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 187,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 188,
+ 334,
+ 2847,
+ 530,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 189,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 190,
+ 302,
+ 2931,
+ 562,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 191,
+ 381,
+ 2945,
+ 484,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 192,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 193,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 194,
+ 622,
+ 2847,
+ 818,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 195,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 196,
+ 590,
+ 2931,
+ 850,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 197,
+ 649,
+ 2945,
+ 792,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 198,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 199,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 200,
+ 910,
+ 2847,
+ 1106,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 201,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 202,
+ 878,
+ 2931,
+ 1138,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 203,
+ 964,
+ 2945,
+ 1052,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 204,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 205,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 206,
+ 1198,
+ 2847,
+ 1394,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 207,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 208,
+ 1166,
+ 2931,
+ 1426,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 209,
+ 1235,
+ 2945,
+ 1358,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(210, 0, 0, 1440, 3120, false, false, false) {
+ +Item(211, 102, 2847, 186, 2931, false, false, false) {
+ +Item(
+ 212,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(213, 390, 2847, 474, 2931, false, false, false) {
+ +Item(
+ 214,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(215, 678, 2847, 762, 2931, false, false, false) {
+ +Item(
+ 216,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(217, 966, 2847, 1050, 2931, false, false, false) {
+ +Item(
+ 218,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 219,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 220,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(221, 0, 0, 1, 1, false, false, false) {
+ +Item(222, 0, 0, 1440, 196, false, false, false)
+ }
+ }
+ }
+ +Item(223, 0, 0, 1440, 3120, false, false, false) {
+ +Item(224, 636, 1476, 804, 1644, false, false, false)
+ }
+ +Item(225, 0, 0, 1, 1, false, false, false)
+ }
+ +Item(226, 0, 0, 1440, 145, false, false, false) {
+ +Item(227, 0, 145, 1440, 145, false, false, false)
+ }
+ }
+ }
+ +Item(228, 0, 0, 1, 1, false, false, false)
+ }
+ }
+ }
+ +Item(229, 0, 3036, 1440, 3120, false, false, false)
+ +Item(230, 0, 0, 1440, 145, false, false, false)
+ }
+
+val exampleLayoutRects: Array<IntArray> = run {
+ val emptyIntArray = IntArray(0)
+ val results = Array(231) { emptyIntArray }
+
+ fun push(item: Item) {
+ results[item.id] = item.bounds
+ item.children.forEach { child -> push(child) }
+ }
+ push(rootItem)
+ for (bounds in results) {
+ assert(bounds !== emptyIntArray)
+ }
+ results
+}
+
+val scrollableItems: List<Item> = run {
+ val results = mutableListOf<Item>()
+
+ fun traverse(item: Item) {
+ if (item.scrollable) {
+ results.add(item)
+ }
+ item.children.forEach { child -> traverse(child) }
+ }
+ traverse(rootItem)
+ results
+}
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 134d810..acc6d86 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -93,7 +93,7 @@
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
implementation("androidx.emoji2:emoji2:1.2.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
// `compose-ui` has a transitive dependency on `lifecycle-livedata-core`, and
// converting `lifecycle-runtime-compose` to KMP triggered a Gradle bug. Adding
@@ -125,6 +125,7 @@
dependencies {
implementation("androidx.fragment:fragment:1.3.0")
implementation("androidx.appcompat:appcompat:1.3.0")
+ implementation("androidx.activity:activity:1.9.1")
implementation(libs.testUiautomator)
implementation(libs.testRules)
implementation(libs.testRunner)
@@ -189,8 +190,11 @@
// version of foundation will be at least this version. This will prevent the bug in
// foundation from occurring. This does _NOT_ require that the app have foundation as
// a dependency.
- commonMainImplementation("androidx.compose.foundation:foundation:1.4.0") {
- because 'prevents a critical bug in Text'
+ // A separate issue exists when foundation 1.6 is used with ui 1.7: overscroll stops
+ // showing due to a behavior change in NestedScrollSource. This constraint ensures that
+ // a version of foundation compatible with the new NestedScrollSource is used.
+ commonMainImplementation("androidx.compose.foundation:foundation:1.7.0") {
+ because 'prevents a regression in Overscroll'
}
}
}
diff --git a/compose/ui/ui/integration-tests/ui-demos/build.gradle b/compose/ui/ui/integration-tests/ui-demos/build.gradle
index ff5f22c..e50bdba2 100644
--- a/compose/ui/ui/integration-tests/ui-demos/build.gradle
+++ b/compose/ui/ui/integration-tests/ui-demos/build.gradle
@@ -40,7 +40,7 @@
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
- debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation(project(":compose:ui:ui-tooling"))
}
android {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 7ade151..760a348 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -57,6 +57,7 @@
import androidx.compose.ui.demos.gestures.DragSlopExceededGestureFilterDemo
import androidx.compose.ui.demos.gestures.EventTypesDemo
import androidx.compose.ui.demos.gestures.HorizontalScrollersInVerticalScrollersDemo
+import androidx.compose.ui.demos.gestures.LongPressChangesHierarchyDemo
import androidx.compose.ui.demos.gestures.LongPressDragGestureFilterDemo
import androidx.compose.ui.demos.gestures.LongPressGestureDetectorDemo
import androidx.compose.ui.demos.gestures.MultiButtonsWithLoggingUsingOnClick
@@ -109,6 +110,9 @@
ComposableDemo("Pressure Tap") { DetectTapPressureGesturesDemo() },
ComposableDemo("Double Tap") { DoubleTapGestureFilterDemo() },
ComposableDemo("Long Press") { LongPressGestureDetectorDemo() },
+ ComposableDemo("Long Press (changes hierarchy)") {
+ LongPressChangesHierarchyDemo()
+ },
ComposableDemo("Scroll") { ScrollGestureFilterDemo() },
ComposableDemo("Drag") { DragGestureFilterDemo() },
ComposableDemo("Long Press Drag") { LongPressDragGestureFilterDemo() },
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/LongPressChangesHierarchyDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/LongPressChangesHierarchyDemo.kt
new file mode 100644
index 0000000..5d84a88
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/LongPressChangesHierarchyDemo.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.gestures
+
+import android.view.View
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Popup
+
+/*
+ * Moves UI to a Popup on long press which changes the top-level Compose container.
+ */
+@Composable
+fun ContainerMovesContentToPopupOnDrag(
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ val movableContent = remember { movableContentOf(content) }
+ var showPopup by remember { mutableStateOf(false) }
+ var offset by remember { mutableStateOf(Offset.Zero) }
+ Box(
+ modifier =
+ modifier.pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = {
+ showPopup = true
+ offset = Offset.Zero
+ },
+ onDrag = { _, deltaOffset -> offset += deltaOffset },
+ onDragCancel = {
+ showPopup = false
+ offset = Offset.Zero
+ },
+ onDragEnd = {
+ showPopup = false
+ offset = Offset.Zero
+ }
+ )
+ }
+ ) {
+ if (showPopup) {
+ Popup { Box(Modifier.offset { offset.round() }) { movableContent() } }
+ } else {
+ movableContent()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun LongPressChangesHierarchyDemo() {
+ MaterialTheme {
+ ContainerMovesContentToPopupOnDrag {
+ AndroidView(factory = ::View, modifier = Modifier.fillMaxWidth().aspectRatio(1f)) {
+ it.setBackgroundColor(Color.Red.toArgb())
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/lint-baseline.xml b/compose/ui/ui/lint-baseline.xml
index 5ce4ebe..d19007e 100644
--- a/compose/ui/ui/lint-baseline.xml
+++ b/compose/ui/ui/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="BanThreadSleep"
@@ -48,15 +48,6 @@
<issue
id="PrimitiveInCollection"
- message="return type Map<LayoutNode, Integer> of getMapOfOriginalDepth: replace with ObjectIntMap"
- errorLine1=" private val mapOfOriginalDepth by"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
message="return type List<HapticFeedbackType> of values: replace with IntList"
errorLine1=" fun values(): List<HapticFeedbackType> = listOf(LongPress, TextHandleMove)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -75,27 +66,27 @@
<issue
id="PrimitiveInCollection"
- message="return type Map<AlignmentLine, Integer> of getLastCalculation: replace with ObjectIntMap"
- errorLine1=" fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="return type Map<? extends AlignmentLine, Integer> of getLastCalculation: replace with ObjectIntMap"
+ errorLine1=" fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt"/>
</issue>
<issue
id="PrimitiveInCollection"
- message="return type Map<AlignmentLine, Integer> of getAlignmentLinesMap: replace with ObjectIntMap"
- errorLine1=" protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="return type Map<? extends AlignmentLine, Integer> of getAlignmentLinesMap: replace with ObjectIntMap"
+ errorLine1=" protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt"/>
</issue>
<issue
id="PrimitiveInCollection"
- message="return type Map<AlignmentLine, Integer> of calculateAlignmentLines: replace with ObjectIntMap"
- errorLine1=" fun calculateAlignmentLines(): Map<AlignmentLine, Int>"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="return type Map<? extends AlignmentLine, Integer> of calculateAlignmentLines: replace with ObjectIntMap"
+ errorLine1=" fun calculateAlignmentLines(): Map<out AlignmentLine, Int>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt"/>
</issue>
@@ -129,34 +120,25 @@
<issue
id="PrimitiveInCollection"
- message="return type Map<AlignmentLine, Integer> of getAlignmentLines: replace with ObjectIntMap"
- errorLine1=" val alignmentLines: Map<AlignmentLine, Int>"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="return type Map<? extends AlignmentLine, Integer> of getAlignmentLines: replace with ObjectIntMap"
+ errorLine1=" val alignmentLines: Map<out AlignmentLine, Int>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt"/>
</issue>
<issue
id="PrimitiveInCollection"
- message="method layout has parameter alignmentLines with type Map<AlignmentLine, Integer>: replace with ObjectIntMap"
- errorLine1=" alignmentLines: Map<AlignmentLine, Int> = emptyMap(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ message="method layout has parameter alignmentLines with type Map<? extends AlignmentLine, Integer>: replace with ObjectIntMap"
+ errorLine1=" alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt"/>
</issue>
<issue
id="PrimitiveInCollection"
- message="field oldAlignmentLines with type Map<AlignmentLine, Integer>: replace with ObjectIntMap"
- errorLine1=" private var oldAlignmentLines: MutableMap<AlignmentLine, Int>? = null"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable alignmentLines with type Map<AlignmentLine, ? extends Integer>: replace with ObjectIntMap"
+ message="variable alignmentLines with type Map<? extends AlignmentLine, ? extends Integer>: replace with ObjectIntMap"
errorLine1=" val alignmentLines = coordinator._measureResult?.alignmentLines"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropTest.kt
index 84c80b10..d0cc359 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropTest.kt
@@ -75,16 +75,24 @@
private lateinit var density: Density
- /**
+ /*
* Sets up a grid of drop targets resembling the following for testing:
+ * accepts accepts
+ * ┌───────────┐ ┌───────────┐
+ * │ │ │ │
+ * │ │ │ │
+ * │ │ │ │
+ * └───────────┘ └───────────┘
*
- * accepts accepts ┌───────────┐ ┌───────────┐ │ │ │ │ │ │ │ │ │ │ │ │ └───────────┘
- * └───────────┘
+ * accepts rejects
+ * ┌───────────┐ accepts ┌───────────┐
+ * │ accepts │ ┌─────┐ │ accepts │
+ * │─────┐ │ │ │ │ ┌─────│
+ * │ │ │ └─────┘ │ │ │
+ * └─────┘─────┘ └─────└─────┘
*
- * accepts rejects ┌───────────┐ accepts ┌───────────┐ │ accepts │ ┌─────┐ │ accepts │ │─────┐ │
- * │ │ │ ┌─────│ │ │ │ └─────┘ │ │ │ └─────┘─────┘ └─────└─────┘
- *
- * parent <------> child offset
+ * parent <------> child
+ * offset
*/
@Before
fun setup() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index ed3de3a..2e76b62 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -61,6 +61,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -95,6 +96,58 @@
hitPathTracker = HitPathTracker(layoutNode.outerCoordinator)
}
+ // Adds unattached nodes and verifies they aren't added to hit path tracker.
+ @Test
+ fun addPointerInputFilters_allInDetachedState_notAdded() {
+ val log = mutableListOf<LogEntry>()
+
+ val root = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+
+ val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+
+ val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+
+ val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+
+ // Detached pointer input nodes will NOT be added
+ hitPathTracker.addHitPath(PointerId(3), listOf(root, middle1, leaf1))
+ hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
+ hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
+
+ val expectedRoot = NodeParent()
+
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+
+ val log1 =
+ log.getOnCancelLog().filter {
+ it.pointerInputNode == leaf1 ||
+ it.pointerInputNode == middle1 ||
+ it.pointerInputNode == root
+ }
+
+ val log2 =
+ log.getOnCancelLog().filter {
+ it.pointerInputNode == leaf2 ||
+ it.pointerInputNode == middle2 ||
+ it.pointerInputNode == root
+ }
+
+ val log3 =
+ log.getOnCancelLog().filter {
+ it.pointerInputNode == leaf3 ||
+ it.pointerInputNode == middle3 ||
+ it.pointerInputNode == root
+ }
+
+ assertThat(log).hasSize(0)
+ assertThat(log1).hasSize(0)
+ assertThat(log2).hasSize(0)
+ assertThat(log3).hasSize(0)
+ }
+
@Test
fun addHitPath_emptyHitResult_resultIsCorrect() {
val pif1 = PointerInputNodeMock()
@@ -247,14 +300,9 @@
val pointerId1 = PointerId(1)
// Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- // Clear any old hits from previous calls (does not really apply here since it's the first
- // call)
- hitPathTracker.removeDetachedPointerInputNodes()
// Modifier.Node(s) hit by the second pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
- // Clear any old hits from previous calls
- hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot =
NodeParent().apply {
@@ -306,15 +354,10 @@
// Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
- // Clear any old hits from previous calls (does not really apply here since it's the first
- // call)
- hitPathTracker.removeDetachedPointerInputNodes()
// Modifier.Node(s) hit by the second pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
- // Clear any old hits from previous calls
- hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot =
NodeParent().apply {
@@ -972,14 +1015,6 @@
}
@Test
- fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() {
- val throwable = catchThrowable { hitPathTracker.removeDetachedPointerInputNodes() }
-
- assertThat(throwable).isNull()
- assertThat(areEqual(hitPathTracker.root, NodeParent()))
- }
-
- @Test
fun removeDetachedPointerInputFilters_complexNothingDetached_nothingRemovedNoCancelsCalled() {
// Arrange.
@@ -1010,8 +1045,6 @@
// Act.
- hitPathTracker.removeDetachedPointerInputNodes()
-
// Assert.
val expectedRoot =
@@ -1054,13 +1087,16 @@
@Test
fun removeDetachedPointerInputFilters_1PathRootDetached_allRemovedAndCorrectCancels() {
val log = mutableListOf<LogEntry>()
- val root = PointerInputNodeMock(log = log, coordinator = LayoutCoordinatesStub(false))
- val middle = PointerInputNodeMock(log = log, coordinator = LayoutCoordinatesStub(false))
- val leaf = PointerInputNodeMock(log = log, coordinator = LayoutCoordinatesStub(false))
+ val root = PointerInputNodeMock(log = log)
+ val middle = PointerInputNodeMock(log = log)
+ val leaf = PointerInputNodeMock(log = log)
hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detaches nodes
+ leaf.remove()
+ middle.remove()
+ root.remove()
assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue()
@@ -1075,13 +1111,14 @@
fun removeDetachedPointerInputFilters_1PathMiddleDetached_removesAndCancelsCorrect() {
val log = mutableListOf<LogEntry>()
val root = PointerInputNodeMock(log)
- val middle = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val child = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle = PointerInputNodeMock(log)
+ val child = PointerInputNodeMock(log)
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
- hitPathTracker.removeDetachedPointerInputNodes()
+ middle.remove()
+ child.remove()
val expectedRoot =
NodeParent().apply { children.add(Node(root).apply { pointerIds.add(pointerId) }) }
@@ -1101,12 +1138,13 @@
val log = mutableListOf<LogEntry>()
val root = PointerInputNodeMock(log)
val middle = PointerInputNodeMock(log)
- val leaf = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf = PointerInputNodeMock(log)
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach node
+ leaf.remove()
val expectedRoot =
NodeParent().apply {
@@ -1141,9 +1179,9 @@
val middle2 = PointerInputNodeMock(log)
val leaf2 = PointerInputNodeMock(log)
- val root3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root3 = PointerInputNodeMock(log)
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1153,7 +1191,10 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf3.remove()
+ middle3.remove()
+ root3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1199,8 +1240,8 @@
val log = mutableListOf<LogEntry>()
val root1 = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
val root2 = PointerInputNodeMock()
val middle2 = PointerInputNodeMock()
@@ -1218,7 +1259,9 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detaches Nodes
+ leaf1.remove()
+ middle1.remove()
val expectedRoot =
NodeParent().apply {
@@ -1269,7 +1312,7 @@
val root2 = PointerInputNodeMock(log)
val middle2 = PointerInputNodeMock(log)
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock(log)
val middle3 = PointerInputNodeMock(log)
@@ -1283,7 +1326,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ leaf2.remove()
val expectedRoot =
NodeParent().apply {
@@ -1332,17 +1375,17 @@
fun removeDetachedPointerInputFilters_3Roots2Detached_removesAndCancelsCorrect() {
val log = mutableListOf<LogEntry>()
- val root1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root1 = PointerInputNodeMock(log)
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
- val root2 = PointerInputNodeMock()
- val middle2 = PointerInputNodeMock()
- val leaf2 = PointerInputNodeMock()
+ val root2 = PointerInputNodeMock(log)
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
- val root3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root3 = PointerInputNodeMock(log)
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1352,7 +1395,14 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach all PointerInputNodeMocks associated with pointerId1 and pointerId3.
+ leaf1.remove()
+ middle1.remove()
+ root1.remove()
+
+ leaf3.remove()
+ middle3.remove()
+ root3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1390,12 +1440,12 @@
val log = mutableListOf<LogEntry>()
val root1 = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
val root2 = PointerInputNodeMock()
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock()
val middle3 = PointerInputNodeMock()
@@ -1409,7 +1459,11 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detaches nodes
+ leaf1.remove()
+ middle1.remove()
+ leaf2.remove()
+ middle2.remove()
val expectedRoot =
NodeParent().apply {
@@ -1452,11 +1506,11 @@
val root2 = PointerInputNodeMock(log)
val middle2 = PointerInputNodeMock(log)
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock()
val middle3 = PointerInputNodeMock()
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1466,7 +1520,9 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf2.remove()
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1511,23 +1567,34 @@
fun removeDetachedPointerInputFilters_3Roots3Detached_allRemovedAndCancelsCorrect() {
val log = mutableListOf<LogEntry>()
- val root1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root1 = PointerInputNodeMock(log)
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
- val root2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root2 = PointerInputNodeMock(log)
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
- val root3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root3 = PointerInputNodeMock(log)
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
hitPathTracker.addHitPath(PointerId(3), listOf(root1, middle1, leaf1))
hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detaches nodes
+ leaf1.remove()
+ middle1.remove()
+ root1.remove()
+
+ leaf2.remove()
+ middle2.remove()
+ root2.remove()
+
+ leaf3.remove()
+ middle3.remove()
+ root3.remove()
val expectedRoot = NodeParent()
@@ -1555,16 +1622,16 @@
val log = mutableListOf<LogEntry>()
val root1 = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
val root2 = PointerInputNodeMock(log)
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock(log)
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1574,7 +1641,13 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf1.remove()
+ middle1.remove()
+ leaf2.remove()
+ middle2.remove()
+ leaf3.remove()
+ middle3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1605,15 +1678,15 @@
val root1 = PointerInputNodeMock(log)
val middle1 = PointerInputNodeMock(log)
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf1 = PointerInputNodeMock(log)
val root2 = PointerInputNodeMock(log)
val middle2 = PointerInputNodeMock(log)
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock(log)
val middle3 = PointerInputNodeMock(log)
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1623,7 +1696,10 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach all PointerInputNodeMocks associated with pointerId1 and pointerId3.
+ leaf1.remove()
+ leaf2.remove()
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1664,17 +1740,17 @@
fun removeDetachedPointerInputFilters_3RootsStaggeredDetached_removesAndCancelsCorrect() {
val log = mutableListOf<LogEntry>()
- val root1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root1 = PointerInputNodeMock(log)
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
val root2 = PointerInputNodeMock(log)
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
val root3 = PointerInputNodeMock(log)
val middle3 = PointerInputNodeMock(log)
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1684,7 +1760,15 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf1.remove()
+ middle1.remove()
+ root1.remove()
+
+ leaf2.remove()
+ middle2.remove()
+
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1718,22 +1802,33 @@
fun removeDetachedPointerInputFilters_rootWith3MiddlesDetached_allRemovedAndCorrectCancels() {
val log = mutableListOf<LogEntry>()
- val root = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val root = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
hitPathTracker.addHitPath(PointerId(3), listOf(root, middle1, leaf1))
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach all PointerInputNodeMocks associated with pointerId1 and pointerId3.
+ leaf1.remove()
+ middle1.remove()
+
+ leaf2.remove()
+ middle2.remove()
+
+ leaf3.remove()
+ middle3.remove()
+
+ // Remove root last
+ root.remove()
val expectedRoot = NodeParent()
@@ -1784,16 +1879,20 @@
fun removeDetachedPointerInputFilters_rootWith3Middles1Detached_removesAndCancelsCorrect() {
val log = mutableListOf<LogEntry>()
- val root = PointerInputNodeMock(log)
+ val parentLayoutCoordinates = LayoutCoordinatesStub(true)
- val middle1 = PointerInputNodeMock(log)
- val leaf1 = PointerInputNodeMock(log)
+ val root = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
- val middle2 = PointerInputNodeMock(log)
- val leaf2 = PointerInputNodeMock(log)
+ val middle1 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
+ val leaf1 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
+ val leaf2 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
+
+ // Detached later
+ val middle3 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
+ // Detached later
+ val leaf3 = PointerInputNodeMock(log = log, coordinator = parentLayoutCoordinates)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1803,7 +1902,9 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach both PointerInputNodeMocks
+ middle3.remove()
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1847,11 +1948,11 @@
val root = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
val middle3 = PointerInputNodeMock(log)
val leaf3 = PointerInputNodeMock(log)
@@ -1864,7 +1965,11 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf1.remove()
+ middle1.remove()
+ leaf2.remove()
+ middle2.remove()
val expectedRoot =
NodeParent().apply {
@@ -1904,14 +2009,14 @@
val root = PointerInputNodeMock(log)
- val middle1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle1 = PointerInputNodeMock(log)
+ val leaf1 = PointerInputNodeMock(log)
- val middle2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle2 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
- val middle3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val middle3 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -1921,7 +2026,15 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detaches nodes
+ leaf1.remove()
+ middle1.remove()
+
+ leaf2.remove()
+ middle2.remove()
+
+ leaf3.remove()
+ middle3.remove()
val expectedRoot =
NodeParent().apply {
@@ -1960,7 +2073,7 @@
val middle = PointerInputNodeMock(log)
val leaf1 = PointerInputNodeMock(log)
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf2 = PointerInputNodeMock(log)
val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
@@ -1971,7 +2084,8 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Remove nodes
+ leaf2.remove()
val expectedRoot =
NodeParent().apply {
@@ -2013,9 +2127,9 @@
val middle = PointerInputNodeMock(log)
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf1 = PointerInputNodeMock(log)
val leaf2 = PointerInputNodeMock(log)
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -2025,7 +2139,9 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf1.remove()
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -2067,9 +2183,9 @@
val middle = PointerInputNodeMock(log)
- val leaf1 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf2 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
- val leaf3 = PointerInputNodeMock(log, coordinator = LayoutCoordinatesStub(false))
+ val leaf1 = PointerInputNodeMock(log)
+ val leaf2 = PointerInputNodeMock(log)
+ val leaf3 = PointerInputNodeMock(log)
val pointerId1 = PointerId(3)
val pointerId2 = PointerId(5)
@@ -2079,7 +2195,10 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputNodes()
+ // Detach nodes
+ leaf1.remove()
+ leaf2.remove()
+ leaf3.remove()
val expectedRoot =
NodeParent().apply {
@@ -3299,6 +3418,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -3415,6 +3536,9 @@
override fun transform(matrix: Matrix) {}
+ override val underlyingMatrix: Matrix
+ get() = Matrix()
+
override fun inverseTransform(matrix: Matrix) {}
override fun mapOffset(point: Offset, inverse: Boolean) = point
@@ -3427,6 +3551,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 461d659..0c03bf2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -56,6 +56,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -2907,6 +2908,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -2989,6 +2992,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) {}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index be78319..8370dfd 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -149,6 +150,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) {}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) {}
@OptIn(InternalCoreApi::class) override var showLayoutBounds: Boolean = false
@@ -216,6 +219,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -620,6 +625,9 @@
override fun transform(matrix: Matrix) {}
+ override val underlyingMatrix: Matrix
+ get() = Matrix()
+
override fun inverseTransform(matrix: Matrix) {}
override fun mapOffset(point: Offset, inverse: Boolean) = point
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
new file mode 100644
index 0000000..6f6e935
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlacementScopeMotionFrameOfReferenceTest {
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ @Test
+ fun testLazyList() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyListState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyColumn(state = state, modifier = Modifier.requiredHeight(100.dp)) {
+ items(30) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0, it * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp),
+ state = state
+ ) {
+ items(60) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5 * 2
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ rule.waitForIdle()
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0 + it % 2 * 20, it / 2 * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyStaggeredGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyStaggeredGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalStaggeredGrid(
+ state = state,
+ columns = StaggeredGridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp)
+ ) {
+ items(60) { index ->
+ Box(
+ Modifier.size(20.dp, ((index % 2) * 5).dp + 15.dp)
+ .onGloballyPositioned { coords[index] = it }
+ )
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 10
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testPager() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = PagerState { 30 }
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ HorizontalPager(
+ state,
+ pageSize = PageSize.Fixed(20.dp),
+ modifier = Modifier.requiredHeight(20.dp).requiredWidth(100.dp)
+ ) { index ->
+ Box(Modifier.size(20.dp, 20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToPage(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ requireNotNull(coords[itemId + it]) { "item $itemId, it = $it" }
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round(),
+ )
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
new file mode 100644
index 0000000..2a326fe
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RectListIntegrationTest.kt
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.scale
+import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
+import junit.framework.TestCase.assertFalse
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.roundToInt
+import kotlin.test.assertTrue
+import org.junit.ComparisonFailure
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class RectListIntegrationTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ @SmallTest
+ fun testSingleBox() {
+ rule.setContent { Box(Modifier.testTag("foo").size(10.dp)) }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testNestedBox() {
+ rule.setContent {
+ Box(Modifier.padding(10.dp)) { Box(Modifier.testTag("foo").size(10.dp)) }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdatePosition() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.padding(if (toggle) 50.dp else 10.dp)) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(50.dp, 50.dp, 60.dp, 60.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateTextPadding() {
+ var padding by mutableIntStateOf(0)
+
+ rule.setContent {
+ Box(Modifier.background(Color.Yellow).size(200.dp)) {
+ Text("Row", Modifier.testTag("text").padding(padding.dp).size(40.dp))
+ }
+ }
+
+ rule.onNodeWithTag("text").assertRectDp(0.dp, 0.dp, 40.dp, 40.dp)
+
+ rule.runOnIdle { padding += 10 }
+
+ rule.onNodeWithTag("text").assertRectDp(0.dp, 0.dp, 60.dp, 60.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddings() {
+ rule.setContent {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsTwo() {
+ rule.setContent {
+ Box(Modifier.padding(4.dp)) { Box(Modifier.testTag("test").padding(4.dp).size(4.dp)) }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(4.dp, 4.dp, 16.dp, 16.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsThree() {
+ var padding by mutableIntStateOf(20)
+ rule.setContent {
+ Box(Modifier.padding(padding.dp)) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ padding += 10
+ rule.onNodeWithTag("test").assertRectDp(110.dp, 110.dp, 210.dp, 210.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testPaddingsFour() {
+ var padding by mutableIntStateOf(20)
+ rule.setContent {
+ Box(Modifier.offset { IntOffset(padding.dp.roundToPx(), padding.dp.roundToPx()) }) {
+ Box(Modifier.padding(20.dp)) {
+ Box(Modifier.padding(60.dp)) { Box(Modifier.testTag("test").size(100.dp)) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("test").assertRectDp(100.dp, 100.dp, 200.dp, 200.dp)
+ padding += 10
+ rule.onNodeWithTag("test").assertRectDp(110.dp, 110.dp, 210.dp, 210.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateSize() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent { Box { Box(Modifier.testTag("foo").size(if (toggle) 50.dp else 10.dp)) } }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 50.dp, 50.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testUpdateTranslation() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ Box(
+ Modifier.offset {
+ if (toggle) IntOffset(10.dp.roundToPx(), 10.dp.roundToPx()) else IntOffset.Zero
+ }
+ ) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ rule.runOnIdle { toggle = !toggle }
+
+ rule.onNodeWithTag("foo").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testRemovingNodeRemovesRect() {
+ var toggle by mutableStateOf(false)
+
+ rule.setContent {
+ if (!toggle) {
+ Box(Modifier.testTag("foo").size(10.dp))
+ }
+ }
+
+ val node = rule.onNodeWithTag("foo")
+
+ node.assertRectDp(0.dp, 0.dp, 10.dp, 10.dp)
+
+ val semanticsNode = node.fetchSemanticsNode()
+ val owner = semanticsNode.layoutNode.owner as? AndroidComposeView
+ val rectList = owner?.rectManager?.rects ?: error("Could not find rect list")
+
+ val nodeId = semanticsNode.id
+
+ rule.runOnIdle {
+ assertTrue(nodeId in rectList)
+ toggle = !toggle
+ }
+
+ rule.runOnIdle { assertFalse(nodeId in rectList) }
+ }
+
+ @Test
+ @SmallTest
+ fun testScrolling() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.testTag("scroller").verticalScroll(scrollState)) {
+ for (i in 0 until 4) {
+ Box(Modifier.testTag("foo$i").width(200.dp).height(400.dp)) {
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ Box(Modifier.size(10.dp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, 0.dp, 200.dp, 400.dp)
+
+ rule.onNodeWithTag("foo3").assertRectDp(0.dp, 1200.dp, 200.dp, 1600.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("scroller").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 100.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, -100.dp, 200.dp, 300.dp)
+
+ rule.onNodeWithTag("foo3").assertRectDp(0.dp, 1100.dp, 200.dp, 1500.dp)
+ }
+
+ @Composable
+ fun ColorStripe(red: Int, green: Int, blue: Int) {
+ Canvas(Modifier.size(45.dp, 500.dp)) {
+ drawRect(Color(red = red, green = green, blue = blue))
+ }
+ }
+
+ @Test
+ @SmallTest
+ fun testScrollingWeirdness() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.verticalScroll(scrollState)) {
+ Box(Modifier.testTag("foo").size(100.dp))
+ Box(Modifier.testTag("bar").size(100.dp))
+ }
+ }
+
+ rule.onNodeWithTag("foo").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+
+ rule.onNodeWithTag("bar").assertRectDp(0.dp, 100.dp, 100.dp, 200.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testRotatedBox() {
+ rule.setContent {
+ Box(
+ Modifier.testTag("outer").graphicsLayer {
+ translationX = 100.dp.toPx()
+ rotationZ = 45f
+ }
+ ) {
+ Box(Modifier.testTag("inner").size(100.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+ rule.onNodeWithTag("inner").assertRectDp(79.dp, -21.dp, 220.dp, 121.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScaledBox() {
+ var toggle by mutableStateOf(true)
+ rule.setContent {
+ Box(
+ Modifier.testTag("outer").padding(10.dp).graphicsLayer {
+ translationX = if (toggle) 0f else 100.dp.toPx()
+ rotationZ = if (toggle) 0f else 45f
+ }
+ ) {
+ Box(Modifier.testTag("inner").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 30.dp, 30.dp)
+ rule.onNodeWithTag("inner").assertRectDp(10.dp, 10.dp, 20.dp, 20.dp)
+ toggle = !toggle
+ rule.onNodeWithTag("inner").assertRectDp(108.dp, 8.dp, 122.dp, 22.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScaledBoxUpdate() {
+ rule.setContent {
+ Box(Modifier.testTag("outer").padding(10.dp).scale(2f)) {
+ Box(Modifier.testTag("inner").size(10.dp))
+ }
+ }
+
+ rule.onNodeWithTag("outer").assertRectDp(0.dp, 0.dp, 40.dp, 40.dp)
+ rule.onNodeWithTag("inner").assertRectDp(5.dp, 5.dp, 25.dp, 25.dp)
+ }
+
+ @Test
+ @SmallTest
+ fun testScrollingNestedLayout() {
+ rule.setContent {
+ val scrollState = rememberScrollState()
+ Column(Modifier.testTag("scroller").verticalScroll(scrollState)) {
+ for (i in 0 until 8) {
+ Box(
+ Modifier.background(if (i % 2 == 0) Color.Yellow else Color.LightGray)
+ .size(200.dp)
+ ) {
+ Text("Row $i", Modifier.testTag("text$i").width(100.dp).height(20.dp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("text0").assertRectDp(0.dp, 0.dp, 100.dp, 20.dp)
+
+ rule.onNodeWithTag("text1").assertRectDp(0.dp, 200.dp, 100.dp, 220.dp)
+
+ rule.onNodeWithTag("text2").assertRectDp(0.dp, 400.dp, 100.dp, 420.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("scroller").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 100.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.onNodeWithTag("text2").assertRectDp(0.dp, 300.dp, 100.dp, 320.dp)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ @SmallTest
+ fun testLazyColumn() {
+ rule.setContent {
+ LazyColumn(Modifier.testTag("lazy")) {
+ items(200) { Box(Modifier.testTag("foo$it").height(100.dp).width(100.dp)) }
+ }
+ }
+
+ rule.waitUntilExactlyOneExists(hasTestTag("foo1"))
+ rule.waitUntilDoesNotExist(hasTestTag("foo20"))
+
+ rule.onNodeWithTag("foo0").assertRectDp(0.dp, 0.dp, 100.dp, 100.dp)
+ rule.onNodeWithTag("foo1").assertRectDp(0.dp, 100.dp, 100.dp, 200.dp)
+
+ val scrollBy =
+ rule.onNodeWithTag("lazy").fetchSemanticsNode().config[ScrollBy].action
+ ?: error("No scrollByAction found")
+
+ val scrollDistance = with(rule.density) { 2000.dp.toPx() }
+
+ scrollBy(0f, scrollDistance)
+
+ rule.waitUntilDoesNotExist(hasTestTag("foo0"))
+ rule.waitUntilExactlyOneExists(hasTestTag("foo20"))
+
+ rule.onNodeWithTag("foo20").assertRectTopWithinRange(-4.dp, 4.dp)
+ }
+
+ internal fun SemanticsNodeInteraction.assertRectDp(
+ left: Dp,
+ top: Dp,
+ right: Dp,
+ bottom: Dp,
+ ) = withRect { l, t, r, b ->
+ if (
+ !approxEquals(left, l) ||
+ !approxEquals(top, t) ||
+ !approxEquals(right, r) ||
+ !approxEquals(bottom, b)
+ ) {
+ val actualL = convertToDp(l)
+ val actualT = convertToDp(t)
+ val actualR = convertToDp(r)
+ val actualB = convertToDp(b)
+
+ val expectDpString = "[$left, $top, $right, $bottom]"
+ val actualDpString = "[$actualL, $actualT, $actualR, $actualB]"
+
+ throw ComparisonFailure(
+ "expected <$expectDpString> but was: <$actualDpString>",
+ expectDpString,
+ actualDpString
+ )
+ }
+ }
+
+ internal fun SemanticsNodeInteraction.assertRectTopWithinRange(
+ min: Dp,
+ max: Dp,
+ ) = withRect { _, t, _, _ ->
+ val topDp = convertToDp(t)
+
+ if (topDp < min || topDp > max) {
+ error("top was $topDp but was expected to be between [$min, $max]")
+ }
+ }
+
+ inline internal fun SemanticsNodeInteraction.withRect(
+ crossinline block: Density.(l: Int, t: Int, r: Int, b: Int) -> Unit
+ ) {
+ val node = fetchSemanticsNode()
+ val owner = node.layoutNode.owner as? AndroidComposeView
+ val rectList = owner?.rectManager?.rects ?: error("Could not find rect list")
+
+ with(rule.density) {
+ val found = rectList.withRect(node.id) { l, t, r, b -> block(l, t, r, b) }
+
+ if (!found) {
+ error("Node with ${node.id} not found in rectlist")
+ }
+ }
+ }
+
+ private fun Density.convertToDp(px: Int): Dp {
+ return (px / density).roundToInt().dp
+ }
+
+ private fun Density.approxEquals(dp: Dp, px: Int): Boolean {
+ val lower = floor((dp.value - 1f) * density).toInt()
+ val upper = ceil((dp.value + 1f) * density).toInt()
+ return px in lower..upper
+ }
+ // TODO: assert on number of times insert/update/move called
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RulerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RulerTest.kt
index 6083ac6..8faad51 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RulerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/RulerTest.kt
@@ -35,19 +35,15 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -626,61 +622,4 @@
rule.waitForIdle()
assertThat(rulerValue).isWithin(0.01f).of(-100f)
}
-
- @Test
- fun rulerMovesWithView() {
- var offset by mutableIntStateOf(0)
- var rulerValue = 0f
- var rootX = 0f
- var rulerChanged = CountDownLatch(1)
- rule.setContent {
- Box(
- Modifier.onPlaced { rootX = it.positionInWindow().x }
- .offset { IntOffset(offset, 0) }
- ) {
- AndroidView(
- factory = { context ->
- ComposeView(context).apply {
- setContent {
- Box(
- Modifier.layout { m, constraints ->
- val p = m.measure(constraints)
- layout(
- p.width,
- p.height,
- rulers = {
- val position = coordinates.positionInWindow().x
- verticalRuler.provides(-position)
- }
- ) {
- p.place(0, 0)
- }
- }
- ) {
- Box(
- Modifier.layout { measurable, constraints ->
- val p = measurable.measure(constraints)
- layout(p.width, p.height) {
- rulerValue = verticalRuler.current(Float.NaN)
- rulerChanged.countDown()
- }
- }
- )
- }
- }
- }
- }
- )
- }
- }
- assertThat(rulerChanged.await(1, TimeUnit.SECONDS)).isTrue()
- rule.runOnUiThread {
- assertThat(rulerValue).isWithin(0.01f).of(-rootX)
- rulerChanged = CountDownLatch(1)
- offset = 100
- rule.activity.window.decorView.invalidate()
- }
- assertThat(rulerChanged.await(1, TimeUnit.SECONDS)).isTrue()
- rule.runOnIdle { assertThat(rulerValue).isWithin(0.01f).of(-100f - rootX) }
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
}
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitChildren(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
.inOrder()
}
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
}
- // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
- // visit children in the order of composition. So instead of a stack, we probably need
- // to use a queue to hold the intermediate nodes.
@Test
fun differentLayoutNodes() {
// Arrange.
@@ -79,10 +77,10 @@
val visitedChildren = mutableListOf<Modifier.Node>()
rule.setContent {
Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
- Box(Modifier.elementOf(child5).elementOf(child6)) {
- Box(Modifier.elementOf(child7).elementOf(child8))
+ Box(Modifier.elementOf(child3).elementOf(child4)) {
+ Box(Modifier.elementOf(child5).elementOf(child6))
}
- Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+ Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
}
}
@@ -95,6 +93,54 @@
.inOrder()
}
+ @Test
+ fun differentLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1))
+ Box(Modifier.elementOf(child2).zIndex(10f)) {
+ Box(Modifier.elementOf(child3).zIndex(-10f))
+ }
+ Box { Box(Modifier.elementOf(child4)) }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtree(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+ }
+
+ @Test
+ fun differentLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+ { Box { Box(Modifier.elementOf(child4)) } }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
- @Test
- fun testPushPopOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-
- @Test
- fun testPopInBetweenPushes() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3, 4))
- stack.pop()
- stack.push(mutableVectorOf(4, 5, 6))
- stack.pop()
- stack.pop()
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(5, 6, 7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index 61fe1dd..705a09d 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -47,6 +47,7 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.invertTo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -422,6 +423,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
override val fontFamilyResolver: FontFamily.Resolver
get() = TODO("Not yet implemented")
@@ -522,6 +525,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
@InternalComposeUiApi override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
@@ -565,6 +570,9 @@
matrix.timesAssign(transform)
}
+ override val underlyingMatrix: Matrix
+ get() = transform
+
override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(inverseTransform)
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.node
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.InspectorInfo
/**
@@ -38,3 +41,30 @@
name = "testNode"
}
}
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+ SubcomposeLayout(modifier) { constraints ->
+ var layoutWidth = constraints.minWidth
+ var layoutHeight = constraints.minHeight
+ val subcomposes = mutableListOf<List<Placeable>>()
+
+ // Measure in reverse order
+ contents.reversed().forEachIndexed { index, content ->
+ subcomposes.add(
+ 0,
+ subcompose(index, content).map {
+ it.measure(constraints).also { placeable ->
+ layoutWidth = maxOf(layoutWidth, placeable.width)
+ layoutHeight = maxOf(layoutHeight, placeable.height)
+ }
+ }
+ )
+ }
+
+ layout(layoutWidth, layoutHeight) {
+
+ // But place in direct order - it sets direct draw order
+ subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+ }
+ }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index f178b053..b36dd32 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -16,7 +16,9 @@
package androidx.compose.ui.viewinterop
+import android.animation.ValueAnimator
import android.content.Context
+import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Build
@@ -34,15 +36,20 @@
import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView
-import androidx.compose.foundation.background
+import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -56,6 +63,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
@@ -66,14 +74,18 @@
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.SubcompositionReusableContentHost
+import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.platform.findViewTreeCompositionContext
import androidx.compose.ui.platform.testTag
@@ -91,6 +103,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnCreate
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnRelease
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnReset
@@ -98,6 +111,13 @@
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewAttach
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.OnViewDetach
import androidx.compose.ui.viewinterop.AndroidViewTest.AndroidViewLifecycleEvent.ViewLifecycleEvent
+import androidx.core.view.SoftwareKeyboardControllerCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat
+import androidx.core.view.WindowInsetsAnimationCompat.Callback
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.Event.ON_CREATE
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
@@ -129,13 +149,18 @@
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.test.assertIs
import kotlin.test.assertNull
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
+import org.junit.After
import org.junit.Assert.assertEquals
+import org.junit.Assume.assumeTrue
+import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -146,6 +171,36 @@
class AndroidViewTest {
@get:Rule val rule = createAndroidComposeRule<TestActivity>()
+ private val setDurationScale =
+ ValueAnimator::class.java.getDeclaredMethod("setDurationScale", Float::class.java).apply {
+ isAccessible = true
+ }
+
+ private val getDurationScale =
+ ValueAnimator::class.java.getDeclaredMethod("getDurationScale").apply {
+ isAccessible = true
+ }
+
+ private var oldDurationScale = 1f
+
+ @Before
+ fun edgeToEdge() {
+ rule.runOnUiThread { rule.activity.enableEdgeToEdge() }
+ }
+
+ @Before
+ fun setDurationScale() {
+ rule.runOnUiThread {
+ oldDurationScale = getDurationScale.invoke(null) as Float
+ setDurationScale.invoke(null, 1f)
+ }
+ }
+
+ @After
+ fun resetDurationScale() {
+ rule.runOnUiThread { setDurationScale.invoke(null, oldDurationScale) }
+ }
+
@Test
fun androidViewWithConstructor() {
rule.setContent { AndroidView({ TextView(it).apply { text = "Test" } }) }
@@ -1766,6 +1821,321 @@
.isEqualTo(1)
}
+ @Test
+ fun insetsMoveWithChild() {
+ rule.runOnIdle {
+ WindowInsetsControllerCompat(rule.activity.window, rule.activity.window.decorView)
+ .show(WindowInsetsCompat.Type.systemBars())
+ }
+
+ var topPadding by mutableIntStateOf(0)
+ var topInset = 0
+ var outerTopInset = 0
+ var latch = CountDownLatch(1)
+ var isAnimating = false
+ lateinit var composeView: ComposeView
+
+ rule.setContent {
+ composeView = LocalView.current.parent as ComposeView
+ composeView.consumeWindowInsets = false // call this before accessing insets
+ val insets = WindowInsets.systemBars
+ Box(
+ Modifier.layout { m, c ->
+ outerTopInset = insets.getTop(this)
+ val p = m.measure(c.offset(vertical = -topPadding))
+ layout(p.width, p.height) { p.place(0, topPadding) }
+ }
+ .background(Color.Blue)
+ .fillMaxSize()
+ ) {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ ComposeView(context).apply {
+ setContent {
+ val systemBars = WindowInsets.systemBars
+ val density = LocalDensity.current
+ Box(
+ Modifier.fillMaxSize().onPlaced {
+ topInset = systemBars.getTop(density)
+ latch.countDown()
+ }
+ )
+ Box(Modifier.fillMaxSize().systemBarsPadding())
+ }
+ }
+ }
+ )
+ Box(Modifier.fillMaxSize().background(Color.White).safeContentPadding())
+ }
+ }
+
+ rule.runOnIdle {
+ ViewCompat.setWindowInsetsAnimationCallback(
+ composeView.parent as View,
+ object : Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat = insets
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: BoundsCompat
+ ): BoundsCompat {
+ isAnimating = true
+ return super.onStart(animation, bounds)
+ }
+
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ isAnimating = false
+ super.onEnd(animation)
+ }
+ }
+ )
+ }
+
+ rule.waitForIdle()
+
+ assumeTrue(outerTopInset > 0) // This device must have a status bar inset
+
+ rule.runOnIdle {
+ assertThat(topInset).isEqualTo(outerTopInset)
+ latch = CountDownLatch(1)
+ topPadding = 5
+ }
+
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ // For some reason, the status bar insets animate to the target
+ // value on older SDKs
+ rule.waitForIdle()
+ rule.waitUntil { !isAnimating }
+
+ rule.runOnIdle { assertThat(topInset).isEqualTo(outerTopInset - 5) }
+ }
+
+ @Test
+ fun insetsMoveWithChildSize() {
+ rule.runOnIdle {
+ WindowInsetsControllerCompat(rule.activity.window, rule.activity.window.decorView)
+ .show(WindowInsetsCompat.Type.systemBars())
+ }
+
+ var topInset = 0
+ var bottomInset = 0
+ var outerTopInset = 0
+ var outerBottomInset = 0
+ var latch = CountDownLatch(1)
+ lateinit var composeView: ComposeView
+ var childUsesMaxSize by mutableStateOf(false)
+
+ rule.setContent {
+ composeView = LocalView.current.parent as ComposeView
+ composeView.consumeWindowInsets = false // call this before accessing insets
+ val insets = WindowInsets.systemBars
+ Box(
+ Modifier.layout { m, c ->
+ outerTopInset = insets.getTop(this)
+ outerBottomInset = insets.getBottom(this)
+ val p = m.measure(c)
+ layout(p.width, p.height) { p.place(0, 0) }
+ }
+ .background(Color.Blue)
+ .fillMaxSize()
+ ) {
+ AndroidView(
+ modifier = Modifier.align(AbsoluteAlignment.TopLeft),
+ factory = { context ->
+ ComposeView(context).apply {
+ setContent {
+ val systemBars = WindowInsets.systemBars
+ val density = LocalDensity.current
+ val sizeModifier =
+ if (childUsesMaxSize) {
+ Modifier.fillMaxSize()
+ } else {
+ Modifier.size(100.dp)
+ }
+ Box(
+ sizeModifier
+ .onPlaced {
+ topInset = systemBars.getTop(density)
+ bottomInset = systemBars.getBottom(density)
+ latch.countDown()
+ }
+ .background(Color.White)
+ )
+ }
+ }
+ }
+ )
+ Box(Modifier.fillMaxSize().background(Color.White).safeContentPadding())
+ }
+ }
+
+ // The device must have system bars
+ assumeTrue(outerTopInset != 0 || outerBottomInset != 0)
+
+ rule.runOnIdle {
+ assertThat(topInset).isEqualTo(outerTopInset)
+ assertThat(bottomInset).isEqualTo(0)
+ latch = CountDownLatch(1)
+ childUsesMaxSize = true
+ }
+
+ rule.waitForIdle()
+
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ // On older devices, the insets animate over a few frames. Wait for that animation to
+ // finish.
+ var framesAtSameValue = 0
+ var lastBottom = 0
+ rule.waitUntil {
+ if (lastBottom == bottomInset) {
+ framesAtSameValue++
+ } else {
+ framesAtSameValue = 0
+ }
+ lastBottom = bottomInset
+ framesAtSameValue > 2
+ }
+ rule.runOnIdle {
+ assertThat(topInset).isEqualTo(outerTopInset)
+ assertThat(bottomInset).isEqualTo(outerBottomInset)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 30)
+ @Test
+ fun insetsAnimateForChildren() {
+ val hardKeyboardHidden = rule.activity.resources.configuration.hardKeyboardHidden
+ // can't test with a hardware keyboard active because we can't bring up the IME
+ assumeTrue(hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_NO)
+
+ lateinit var composeView: ComposeView
+ lateinit var outerBounds: BoundsCompat
+ lateinit var innerBounds: BoundsCompat
+ val outerProgressInsets = mutableListOf<WindowInsetsCompat>()
+ val innerProgressInsets = mutableListOf<WindowInsetsCompat>()
+ var isAnimating = false
+ var isImeVisible = false
+ var wasAnimated = false
+
+ rule.setContent {
+ composeView = LocalView.current.parent as ComposeView
+ composeView.consumeWindowInsets = false // call this before accessing insets
+ Box(Modifier.background(Color.White).fillMaxSize().systemBarsPadding()) {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ View(context).apply {
+ ViewCompat.setWindowInsetsAnimationCallback(
+ this,
+ object : Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ innerProgressInsets += insets
+ return insets
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: BoundsCompat
+ ): BoundsCompat {
+ innerBounds = bounds
+ return bounds
+ }
+ }
+ )
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ val view = composeView.parent as View
+ ViewCompat.setWindowInsetsAnimationCallback(
+ view,
+ object : Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ outerProgressInsets += insets
+ return insets
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: BoundsCompat
+ ): BoundsCompat {
+ outerBounds = bounds
+ isAnimating = true
+ wasAnimated = true
+ return bounds
+ }
+
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ isAnimating = false
+ }
+ }
+ )
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
+ isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
+ insets
+ }
+ WindowInsetsControllerCompat(rule.activity.window, composeView)
+ .show(WindowInsetsCompat.Type.systemBars())
+ }
+
+ // For some reason, the status bar insets animate to the target
+ // value on older SDKs
+ rule.waitForIdle()
+ rule.waitUntil { !isAnimating }
+
+ rule.runOnIdle {
+ assertThat(isImeVisible).isFalse()
+ outerProgressInsets.clear()
+ innerProgressInsets.clear()
+ wasAnimated = false
+ SoftwareKeyboardControllerCompat(composeView).show()
+ }
+
+ rule.waitForIdle()
+
+ rule.waitUntil { !isAnimating && isImeVisible }
+
+ // the IME wasn't animated, so we can't test
+ assumeTrue(wasAnimated)
+
+ rule.runOnIdle {
+ // With the system bars being part of the padding, the bounds should be different by
+ // the size of the system bars padding
+ assertThat(innerBounds.lowerBound.bottom).isEqualTo(0)
+ assertThat(innerBounds.upperBound.bottom).isLessThan(outerBounds.upperBound.bottom)
+
+ innerProgressInsets.forEach { insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ assertThat(systemBars.left).isEqualTo(0)
+ assertThat(systemBars.top).isEqualTo(0)
+ assertThat(systemBars.right).isEqualTo(0)
+ assertThat(systemBars.bottom).isEqualTo(0)
+ }
+ outerProgressInsets.forEach { insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ assertThat(
+ maxOf(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ )
+ .isGreaterThan(0)
+ }
+ }
+ }
+
@Composable
private inline fun <T : View> ReusableAndroidViewWithLifecycleTracking(
crossinline factory: (Context) -> T,
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
index 8d0b0ee..9ba98f3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
@@ -332,6 +332,90 @@
}
}
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureOne() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureOne(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureTwo() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureTwo(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
private fun createActivity(state: LazyListState) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.android_compose_lists_fling
@@ -385,7 +469,7 @@
@Composable
fun TestComposeList(state: LazyListState) {
LazyColumn(Modifier.fillMaxSize(), state = state) {
- items(1000) {
+ items(2000) {
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(Color.Black)) {
Text(text = it.toString(), color = Color.White)
}
@@ -394,7 +478,7 @@
}
private class ListAdapter : RecyclerView.Adapter<ListViewHolder>() {
- val items = (0 until 1000).map { it.toString() }
+ val items = (0 until 2000).map { it.toString() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
@@ -451,4 +535,4 @@
}
}
-private const val ItemDifferenceThreshold = 3
+private const val ItemDifferenceThreshold = 1
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
index 4fef305..2e76112 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
@@ -24,10 +24,9 @@
import androidx.activity.ComponentActivity
import androidx.annotation.LayoutRes
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.draggable2D
+import androidx.compose.foundation.gestures.rememberDraggable2DState
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -38,14 +37,10 @@
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
@@ -70,9 +65,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue
import kotlin.math.absoluteValue
import kotlin.test.assertTrue
-import kotlinx.coroutines.coroutineScope
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -272,7 +265,6 @@
}
@Test
- @Ignore("b/299092669")
fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_orthogonal() {
// Arrange
createActivity(true)
@@ -306,6 +298,70 @@
assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
}
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationOne() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureOne(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationTwo() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureTwo(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
private fun createActivity(twoDimensional: Boolean = false) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.velocity_tracker_compose_vs_view
@@ -394,6 +450,24 @@
)
}
+internal fun regularGestureOne(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(100),
+ GeneralLocation.CENTER,
+ GeneralLocation.BOTTOM_CENTER
+ )
+ )
+}
+
+internal fun regularGestureTwo(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(SwiperWithTime(70), GeneralLocation.CENTER, GeneralLocation.TOP_CENTER)
+ )
+}
+
private fun espressoSwipe(
swiper: Swiper,
start: CoordinatesProvider,
@@ -418,7 +492,10 @@
.background(Color.Black)
.then(
if (twoDimensional) {
- Modifier.draggable2D(onDragStopped)
+ Modifier.draggable2D(
+ rememberDraggable2DState {},
+ onDragStopped = onDragStopped
+ )
} else {
Modifier.draggable(
rememberDraggableState(onDelta = {}),
@@ -431,32 +508,6 @@
}
}
-fun Modifier.draggable2D(onDragStopped: (Velocity) -> Unit) =
- this.pointerInput(Unit) {
- coroutineScope {
- awaitEachGesture {
- val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- tracker.addPointerInputChange(initialDown)
-
- awaitTouchSlopOrCancellation(initialDown.id) { change, _ ->
- tracker.addPointerInputChange(change)
- change.consume()
- }
-
- val lastEvent =
- awaitDragOrUp(initialDown.id) {
- tracker.addPointerInputChange(it)
- it.consume()
- it.positionChangedIgnoreConsumed()
- }
- lastEvent?.let { tracker.addPointerInputChange(it) }
- onDragStopped(tracker.calculateVelocity())
- }
- }
- }
-
private fun ActivityScenario<*>.createActivityWithComposeContent(
@LayoutRes layout: Int,
content: @Composable () -> Unit,
@@ -512,8 +563,8 @@
return down.size == 1 && move.isNotEmpty() && up.size == 1
}
-// 1% tolerance
-private const val VelocityDifferenceTolerance = 0.1f
+// 5% tolerance
+private const val VelocityDifferenceTolerance = 0.05f
/** Copied from androidx.test.espresso.action.Swipe */
internal data class SwiperWithTime(val gestureDurationMs: Int) : Swiper {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..9885522 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,16 +15,22 @@
*/
package androidx.compose.ui.window
-import android.content.res.Configuration
+import android.util.DisplayMetrics
import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
+import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
@@ -32,11 +38,19 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
@@ -92,6 +106,20 @@
}
@Test
+ fun dialogTest_isNotDismissed_whenClicked_noClickableContent() {
+ setupDialogTest { DefaultDialogContent() }
+
+ val interaction = rule.onNodeWithTag(testTag)
+ interaction.assertIsDisplayed()
+
+ // Click inside the dialog
+ interaction.performClick()
+
+ // Check that the Clickable was pressed and the Dialog is still visible.
+ interaction.assertIsDisplayed()
+ }
+
+ @Test
fun dialogTest_isDismissed_whenSpecified() {
setupDialogTest()
val textInteraction = rule.onNodeWithTag(testTag)
@@ -102,6 +130,16 @@
}
@Test
+ fun dialogTest_isDismissed_whenSpecified_decorFitsFalse() {
+ setupDialogTest(dialogProperties = DialogProperties(decorFitsSystemWindows = false))
+ val textInteraction = rule.onNodeWithTag(testTag)
+ textInteraction.assertIsDisplayed()
+
+ clickOutsideDialog()
+ textInteraction.assertDoesNotExist()
+ }
+
+ @Test
fun dialogTest_isNotDismissed_whenNotSpecified() {
setupDialogTest(closeDialogOnDismiss = false)
val textInteraction = rule.onNodeWithTag(testTag)
@@ -124,6 +162,20 @@
}
@Test
+ fun dialogTest_isNotDismissed_whenDismissOnClickOutsideIsFalse_decorFitsFalse() {
+ setupDialogTest(
+ dialogProperties =
+ DialogProperties(dismissOnClickOutside = false, decorFitsSystemWindows = false)
+ )
+ val textInteraction = rule.onNodeWithTag(testTag)
+ textInteraction.assertIsDisplayed()
+
+ clickOutsideDialog()
+ // The Dialog should still be visible
+ textInteraction.assertIsDisplayed()
+ }
+
+ @Test
fun dialogTest_isDismissed_whenSpecified_backButtonPressed() {
setupDialogTest()
val textInteraction = rule.onNodeWithTag(testTag)
@@ -259,9 +311,9 @@
fun canFillScreenWidth_dependingOnProperty() {
var box1Width = 0
var box2Width = 0
- lateinit var configuration: Configuration
+ lateinit var displayMetrics: DisplayMetrics
rule.setContent {
- configuration = LocalConfiguration.current
+ displayMetrics = LocalView.current.context.resources.displayMetrics
Dialog(
onDismissRequest = {},
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +324,7 @@
Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
}
}
- val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+ val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
assertThat(box1Width).isEqualTo(expectedWidth)
assertThat(box2Width).isLessThan(box1Width)
}
@@ -313,6 +365,111 @@
}
}
+ @Test
+ fun dismissWhenClickingOutsideContent() {
+ var dismissed = false
+ var clicked = false
+ lateinit var composeView: View
+ val clickBoxTag = "clickBox"
+ rule.setContent {
+ Dialog(
+ onDismissRequest = { dismissed = true },
+ properties =
+ DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) {
+ composeView = LocalView.current
+ Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+ }
+ }
+
+ // click inside the compose view
+ rule.onNodeWithTag(clickBoxTag).performClick()
+
+ rule.waitForIdle()
+
+ assertThat(dismissed).isFalse()
+ assertThat(clicked).isTrue()
+
+ clicked = false
+
+ // click outside the compose view
+ rule.waitForIdle()
+ var root = composeView
+ while (root.parent is View) {
+ root = root.parent as View
+ }
+
+ rule.runOnIdle {
+ val x = root.width / 4f
+ val y = root.height / 4f
+ val down =
+ MotionEvent(
+ eventTime = 0,
+ action = ACTION_DOWN,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(down)
+ val up =
+ MotionEvent(
+ eventTime = 10,
+ action = ACTION_UP,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(up)
+ }
+ rule.waitForIdle()
+
+ assertThat(dismissed).isTrue()
+ assertThat(clicked).isFalse()
+ }
+
+ @Test
+ fun dialogInsetsWhenDecorFitsSystemWindows() {
+ var top = -1
+ var bottom = -1
+ val focusRequester = FocusRequester()
+ rule.setContent {
+ Dialog(onDismissRequest = {}) {
+ val density = LocalDensity.current
+ val insets = WindowInsets.safeContent
+ Box(
+ Modifier.fillMaxSize().onPlaced {
+ top = insets.getTop(density)
+ bottom = insets.getBottom(density)
+ }
+ ) {
+ TextField(
+ "Hello World",
+ onValueChange = {},
+ Modifier.align(Alignment.BottomStart).focusRequester(focusRequester)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(top).isEqualTo(0)
+ assertThat(bottom).isEqualTo(0)
+ focusRequester.requestFocus()
+ }
+
+ rule.runOnIdle {
+ assertThat(top).isEqualTo(0)
+ assertThat(bottom).isEqualTo(0)
+ }
+ }
+
private fun setupDialogTest(
closeDialogOnDismiss: Boolean = true,
dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..1109f7a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,35 +15,63 @@
*/
package androidx.compose.ui.window
+import android.animation.ValueAnimator
+import android.content.res.Configuration.HARDKEYBOARDHIDDEN_NO
+import android.os.Build
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.background
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import org.junit.After
import org.junit.Assert.assertNotEquals
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -53,6 +81,19 @@
class DialogWithInsetsTest {
@get:Rule val rule = createAndroidComposeRule<ActivityWithInsets>()
+ private val durationSetter =
+ ValueAnimator::class.java.getDeclaredMethod("setDurationScale", Float::class.java)
+
+ @Before
+ fun setDurationScale() {
+ durationSetter.invoke(null, 1f)
+ }
+
+ @After
+ fun resetDurationScale() {
+ durationSetter.invoke(null, 0f)
+ }
+
/** Make sure that insets are available in the Dialog. */
@Test
fun dialogSupportsWindowInsets() {
@@ -129,6 +170,222 @@
assertNotEquals(Insets.NONE, imeInsets)
}
+ @Test
+ fun dialogCanTakeEntireScreen() {
+ var size = IntSize.Zero
+ var displayWidth = 0
+ var displayHeight = 0
+ var insetsLeft = 0
+ var insetsTop = 0
+ var insetsRight = 0
+ var insetsBottom = 0
+ var textTop = 0
+ var controller: SoftwareKeyboardController? = null
+ rule.setContent {
+ val displayMetrics = LocalView.current.resources.displayMetrics
+ controller = LocalSoftwareKeyboardController.current
+ displayWidth = displayMetrics.widthPixels
+ displayHeight = displayMetrics.heightPixels
+ Box(Modifier.fillMaxSize()) {
+ Dialog(
+ {},
+ properties =
+ DialogProperties(
+ decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false
+ )
+ ) {
+ val insets = WindowInsets.safeDrawing
+
+ Box(
+ Modifier.fillMaxSize()
+ .layout { m, c ->
+ val p = m.measure(c)
+ size = IntSize(p.width, p.height)
+ insetsTop = insets.getTop(this)
+ insetsLeft = insets.getLeft(this, layoutDirection)
+ insetsBottom = insets.getBottom(this)
+ insetsRight = insets.getRight(this, layoutDirection)
+ layout(p.width, p.height) { p.place(0, 0) }
+ }
+ .safeDrawingPadding()
+ ) {
+ TextField(
+ value = "Hello",
+ onValueChange = {},
+ Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+ layoutCoordinates ->
+ textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ if (
+ Build.VERSION.SDK_INT >= 35 &&
+ rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+ ) {
+ // On SDK >= 35, the metrics is the size of the entire screen
+ assertThat(size.width).isEqualTo(displayWidth)
+ assertThat(size.height).isEqualTo(displayHeight)
+ } else {
+ // On SDK < 35, the metrics is the size of the screen with some insets removed
+ assertThat(size.width).isAtLeast(displayWidth)
+ assertThat(size.height).isAtLeast(displayHeight)
+ }
+ // There is going to be some insets
+ assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+ val hardKeyboardHidden =
+ rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+ if (hardKeyboardHidden == HARDKEYBOARDHIDDEN_NO) {
+ return // can't launch the IME when the hardware keyboard is up.
+ }
+ val bottomInsetsBeforeIme = insetsBottom
+ val textTopBeforeIme = textTop
+ rule.onNodeWithTag("textField").requestFocus()
+ rule.waitUntil {
+ controller?.show()
+ insetsBottom != bottomInsetsBeforeIme
+ }
+ rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+ }
+
+ @SdkSuppress(minSdkVersion = 30)
+ @Test
+ fun animatedWindowInsets() {
+ val hardKeyboardHidden = rule.activity.resources.configuration.hardKeyboardHidden
+ if (hardKeyboardHidden == HARDKEYBOARDHIDDEN_NO) {
+ return // can't test when IME doesn't launch
+ }
+
+ var fullHeight by mutableIntStateOf(0)
+ val outsideImeInsets = mutableListOf<Insets>()
+ lateinit var outsideImeBounds: BoundsCompat
+ val insideImeInsets = mutableListOf<Insets>()
+ lateinit var insideImeBounds: BoundsCompat
+ lateinit var dialogView: View
+ val focusRequester = FocusRequester()
+ var softwareKeyboardController: SoftwareKeyboardController? = null
+ var animationRunning = false
+
+ rule.setContent {
+ Box(Modifier.fillMaxSize().onPlaced { fullHeight = it.size.height }) {
+ Dialog(
+ onDismissRequest = {},
+ properties =
+ DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) {
+ dialogView = LocalView.current
+ var view = dialogView
+ while (view !is AbstractComposeView) {
+ view = view.parent as View
+ }
+ view.consumeWindowInsets = false
+ softwareKeyboardController = LocalSoftwareKeyboardController.current
+ // center the content vertically by 34 pixels
+ val height = with(LocalDensity.current) { maxOf(0, fullHeight - 34).toDp() }
+ Box(Modifier.fillMaxWidth().height(height)) {
+ TextField(
+ "Hello World",
+ onValueChange = {},
+ Modifier.focusRequester(focusRequester).safeDrawingPadding()
+ )
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = {
+ View(it).apply {
+ ViewCompat.setWindowInsetsAnimationCallback(
+ this,
+ object :
+ WindowInsetsAnimationCompat.Callback(
+ DISPATCH_MODE_CONTINUE_ON_SUBTREE
+ ) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations:
+ MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ insideImeInsets +=
+ insets.getInsets(WindowInsetsCompat.Type.ime())
+ return insets
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: BoundsCompat
+ ): BoundsCompat {
+ insideImeBounds = bounds
+ return bounds
+ }
+ }
+ )
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ var rootView = dialogView
+ while (rootView.parent is View) {
+ rootView = rootView.parent as View
+ }
+ ViewCompat.setWindowInsetsAnimationCallback(
+ rootView,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ outsideImeInsets += insets.getInsets(WindowInsetsCompat.Type.ime())
+ return insets
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: BoundsCompat
+ ): BoundsCompat {
+ outsideImeBounds = bounds
+ animationRunning = true
+ return bounds
+ }
+
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ animationRunning = false
+ }
+ }
+ )
+ }
+
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ softwareKeyboardController?.show()
+ }
+
+ rule.waitForIdle()
+
+ rule.waitUntil { !animationRunning }
+
+ rule.runOnIdle {
+ assertThat(insideImeBounds.upperBound.bottom)
+ .isEqualTo(outsideImeBounds.upperBound.bottom - 17)
+ for (i in insideImeInsets.size - 1 downTo 0) {
+ val inside = insideImeInsets[i]
+ val outside = outsideImeInsets[i + outsideImeInsets.size - insideImeInsets.size]
+ assertThat(inside.bottom).isEqualTo(maxOf(0, outside.bottom - 17))
+ }
+ }
+ }
+
private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
if (view is DialogWindowProvider) {
return view
diff --git a/compose/ui/ui/src/androidMain/baseline-prof.txt b/compose/ui/ui/src/androidMain/baseline-prof.txt
index e95c5d6..2c9eebe 100644
--- a/compose/ui/ui/src/androidMain/baseline-prof.txt
+++ b/compose/ui/ui/src/androidMain/baseline-prof.txt
@@ -49,6 +49,10 @@
# graphics include everything
HSPLandroidx/compose/ui/graphics/**->**(**)**
+#
+# spatial indexing include everything
+HSPLandroidx/compose/ui/spatial/**->**(**)**
+
# input
HSPLandroidx/compose/ui/input/InputMode;->**(**)**
HSPLandroidx/compose/ui/input/InputModeManagerImpl;->**(**)**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropManager.android.kt
new file mode 100644
index 0000000..4a9de60
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/draganddrop/AndroidDragAndDropManager.android.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.draganddrop
+
+import android.view.DragEvent
+import android.view.View
+import androidx.collection.ArraySet
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+
+/** A Class that provides access [View.OnDragListener] APIs for a [DragAndDropNode]. */
+internal class AndroidDragAndDropManager(
+ private val startDrag:
+ (
+ transferData: DragAndDropTransferData,
+ decorationSize: Size,
+ drawDragDecoration: DrawScope.() -> Unit
+ ) -> Boolean
+) : View.OnDragListener, DragAndDropManager {
+
+ private val rootDragAndDropNode = DragAndDropNode()
+
+ /**
+ * A collection [DragAndDropNode] instances that registered interested in a drag and drop
+ * session by returning true in [DragAndDropNode.onStarted].
+ */
+ private val interestedTargets = ArraySet<DragAndDropTarget>()
+
+ override val modifier: Modifier =
+ object : ModifierNodeElement<DragAndDropNode>() {
+ override fun create() = rootDragAndDropNode
+
+ override fun update(node: DragAndDropNode) = Unit
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "RootDragAndDropNode"
+ }
+
+ override fun hashCode(): Int = rootDragAndDropNode.hashCode()
+
+ override fun equals(other: Any?) = other === this
+ }
+
+ override val isRequestDragAndDropTransferRequired: Boolean
+ get() = true
+
+ override fun requestDragAndDropTransfer(node: DragAndDropNode, offset: Offset) {
+ var isTransferStarted = false
+ val dragAndDropSourceScope =
+ object : DragAndDropStartTransferScope {
+ override fun startDragAndDropTransfer(
+ transferData: DragAndDropTransferData,
+ decorationSize: Size,
+ drawDragDecoration: DrawScope.() -> Unit
+ ): Boolean {
+ isTransferStarted =
+ startDrag(
+ transferData,
+ decorationSize,
+ drawDragDecoration,
+ )
+ return isTransferStarted
+ }
+ }
+ with(node) { dragAndDropSourceScope.startDragAndDropTransfer(offset) { isTransferStarted } }
+ }
+
+ override fun onDrag(view: View, event: DragEvent): Boolean {
+ val dragAndDropEvent = DragAndDropEvent(dragEvent = event)
+ return when (event.action) {
+ DragEvent.ACTION_DRAG_STARTED -> {
+ val accepted = rootDragAndDropNode.acceptDragAndDropTransfer(dragAndDropEvent)
+ interestedTargets.forEach { it.onStarted(dragAndDropEvent) }
+ accepted
+ }
+ DragEvent.ACTION_DROP -> {
+ rootDragAndDropNode.onDrop(dragAndDropEvent)
+ }
+ DragEvent.ACTION_DRAG_ENTERED -> {
+ rootDragAndDropNode.onEntered(dragAndDropEvent)
+ false
+ }
+ DragEvent.ACTION_DRAG_LOCATION -> {
+ rootDragAndDropNode.onMoved(dragAndDropEvent)
+ false
+ }
+ DragEvent.ACTION_DRAG_EXITED -> {
+ rootDragAndDropNode.onExited(dragAndDropEvent)
+ false
+ }
+ DragEvent.ACTION_DRAG_ENDED -> {
+ rootDragAndDropNode.onEnded(dragAndDropEvent)
+ interestedTargets.clear()
+ false
+ }
+ else -> false
+ }
+ }
+
+ override fun registerTargetInterest(target: DragAndDropTarget) {
+ interestedTargets.add(target)
+ }
+
+ override fun isInterestedTarget(target: DragAndDropTarget): Boolean {
+ return interestedTargets.contains(target)
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
index ef4b85e..2a1655b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
@@ -45,4 +45,8 @@
super.onEndChanges()
root.owner?.onEndApplyChanges()
}
+
+ override fun reuse() {
+ current.onReuse()
+ }
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index e64198f..0a56be2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -33,7 +33,6 @@
import android.os.SystemClock
import android.util.LongSparseArray
import android.util.SparseArray
-import android.view.DragEvent
import android.view.FocusFinder
import android.view.KeyEvent as AndroidKeyEvent
import android.view.MotionEvent
@@ -64,7 +63,6 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
-import androidx.collection.ArraySet
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -72,6 +70,8 @@
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.SessionMutex
@@ -83,11 +83,8 @@
import androidx.compose.ui.autofill.performAutofill
import androidx.compose.ui.autofill.populateViewStructure
import androidx.compose.ui.contentcapture.AndroidContentCaptureManager
+import androidx.compose.ui.draganddrop.AndroidDragAndDropManager
import androidx.compose.ui.draganddrop.ComposeDragShadowBuilder
-import androidx.compose.ui.draganddrop.DragAndDropEvent
-import androidx.compose.ui.draganddrop.DragAndDropManager
-import androidx.compose.ui.draganddrop.DragAndDropModifierNode
-import androidx.compose.ui.draganddrop.DragAndDropNode
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Down
@@ -164,7 +161,6 @@
import androidx.compose.ui.node.LayoutNode.UsageByParent
import androidx.compose.ui.node.LayoutNodeDrawScope
import androidx.compose.ui.node.MeasureAndLayoutDelegate
-import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.Owner
@@ -179,6 +175,7 @@
import androidx.compose.ui.semantics.EmptySemanticsModifier
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.findClosestParentNode
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
@@ -265,8 +262,6 @@
onLayoutDirection = ::layoutDirection
)
- private val dragAndDropModifierOnDragListener = DragAndDropModifierOnDragListener(::startDrag)
-
override fun getImportantForAutofill(): Int {
return View.IMPORTANT_FOR_AUTOFILL_YES
}
@@ -296,7 +291,7 @@
}
}
- override val dragAndDropManager: DragAndDropManager = dragAndDropModifierOnDragListener
+ override val dragAndDropManager = AndroidDragAndDropManager(::startDrag)
private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
override val windowInfo: WindowInfo
@@ -425,7 +420,7 @@
.then(rotaryInputModifier)
.then(keyInputModifier)
.then(focusOwner.modifier)
- .then(dragAndDropModifierOnDragListener.modifier)
+ .then(dragAndDropManager.modifier)
}
override val rootForTest: RootForTest = this
@@ -799,7 +794,7 @@
clipChildren = false
ViewCompat.setAccessibilityDelegate(this, composeAccessibilityDelegate)
ViewRootForTest.onViewCreatedCallback?.invoke(this)
- setOnDragListener(this.dragAndDropModifierOnDragListener)
+ setOnDragListener(dragAndDropManager)
root.attach(this)
// Support for this feature in Compose is tracked here: b/207654434
@@ -1010,6 +1005,10 @@
override fun onDetach(node: LayoutNode) {
measureAndLayoutDelegate.onNodeDetached(node)
requestClearInvalidObservations()
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.remove(node)
+ }
}
fun requestClearInvalidObservations() {
@@ -1039,6 +1038,10 @@
// to the front of the list, so removing in a chunk is cheaper than removing one-by-one
endApplyChangesListeners.removeRange(0, size)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
}
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
@@ -1312,6 +1315,10 @@
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
dispatchPendingInteropLayoutCallbacks()
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
}
}
@@ -1462,7 +1469,12 @@
measureAndLayoutDelegate.dispatchOnPositionedCallbacks(forceDispatch = positionChanged)
}
- override fun onDraw(canvas: android.graphics.Canvas) {}
+ override fun onDraw(canvas: android.graphics.Canvas) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.dispatchCallbacks()
+ }
+ }
override fun createLayer(
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
@@ -1562,6 +1574,15 @@
}
}
+ override val rectManager = RectManager()
+
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isRectTrackingEnabled) {
+ rectManager.remove(layoutNode)
+ }
+ }
+
override fun onInteropViewLayoutChange(view: InteropView) {
isPendingInteropViewLayoutChangeDispatch = true
}
@@ -2744,88 +2765,6 @@
)
}
-/** A Class that provides access [View.OnDragListener] APIs for a [DragAndDropNode]. */
-private class DragAndDropModifierOnDragListener(
- private val startDrag:
- (
- transferData: DragAndDropTransferData,
- decorationSize: Size,
- drawDragDecoration: DrawScope.() -> Unit
- ) -> Boolean
-) : View.OnDragListener, DragAndDropManager {
-
- private val rootDragAndDropNode = DragAndDropNode { null }
-
- /**
- * A collection [DragAndDropModifierNode] instances that registered interested in a drag and
- * drop session by returning true in [DragAndDropModifierNode.onStarted].
- */
- private val interestedNodes = ArraySet<DragAndDropModifierNode>()
-
- override val modifier: Modifier =
- object : ModifierNodeElement<DragAndDropNode>() {
- override fun create() = rootDragAndDropNode
-
- override fun update(node: DragAndDropNode) = Unit
-
- override fun InspectorInfo.inspectableProperties() {
- name = "RootDragAndDropNode"
- }
-
- override fun hashCode(): Int = rootDragAndDropNode.hashCode()
-
- override fun equals(other: Any?) = other === this
- }
-
- override fun onDrag(view: View, event: DragEvent): Boolean {
- val dragAndDropEvent = DragAndDropEvent(dragEvent = event)
- return when (event.action) {
- DragEvent.ACTION_DRAG_STARTED -> {
- val accepted = rootDragAndDropNode.acceptDragAndDropTransfer(dragAndDropEvent)
- interestedNodes.forEach { it.onStarted(dragAndDropEvent) }
- accepted
- }
- DragEvent.ACTION_DROP -> rootDragAndDropNode.onDrop(dragAndDropEvent)
- DragEvent.ACTION_DRAG_ENTERED -> {
- rootDragAndDropNode.onEntered(dragAndDropEvent)
- false
- }
- DragEvent.ACTION_DRAG_LOCATION -> {
- rootDragAndDropNode.onMoved(dragAndDropEvent)
- false
- }
- DragEvent.ACTION_DRAG_EXITED -> {
- rootDragAndDropNode.onExited(dragAndDropEvent)
- false
- }
- DragEvent.ACTION_DRAG_ENDED -> {
- rootDragAndDropNode.onEnded(dragAndDropEvent)
- false
- }
- else -> false
- }
- }
-
- override fun drag(
- transferData: DragAndDropTransferData,
- decorationSize: Size,
- drawDragDecoration: DrawScope.() -> Unit,
- ): Boolean =
- startDrag(
- transferData,
- decorationSize,
- drawDragDecoration,
- )
-
- override fun registerNodeInterest(node: DragAndDropModifierNode) {
- interestedNodes.add(node)
- }
-
- override fun isInterestedNode(node: DragAndDropModifierNode): Boolean {
- return interestedNodes.contains(node)
- }
-}
-
private fun View.containsDescendant(other: View): Boolean {
if (other == this) return false
var viewParent = other.parent
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
index 60f3cf9..51bdd81 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
@@ -403,6 +403,9 @@
return matrixCache
}
+ override val underlyingMatrix: Matrix
+ get() = getMatrix()
+
private fun getInverseMatrix(): Matrix? {
val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it }
if (!isInverseMatrixDirty) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index 2d34ae9..5c56366 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -338,6 +338,9 @@
ownerView.recycle(this)
}
+ override val underlyingMatrix: Matrix
+ get() = matrixCache.calculateMatrix(renderNode)
+
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
return if (inverse) {
matrixCache.mapInverse(renderNode, point)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index a92d4f4..127641a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -77,6 +77,9 @@
private val matrixCache = LayerMatrixCache(getMatrix)
+ override val underlyingMatrix: Matrix
+ get() = matrixCache.calculateMatrix(this)
+
/**
* Local copy of the transform origin as GraphicsLayerModifier can be implemented as a model
* object. Update this field within [updateLayerProperties] and use it in [resize] or other
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt
new file mode 100644
index 0000000..a5bb81c
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/spatial/RectListDebugger.android.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.spatial
+
+import android.annotation.SuppressLint
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.unit.Constraints
+
+@Composable
+internal fun RectListDebugger(modifier: Modifier = Modifier) {
+ Layout(modifier.then(RectListDebuggerModifierElement), EmptyFillMeasurePolicy)
+}
+
+private object EmptyFillMeasurePolicy : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ return layout(constraints.maxWidth, constraints.maxHeight) {}
+ }
+}
+
+@SuppressLint("ModifierNodeInspectableProperties")
+private object RectListDebuggerModifierElement :
+ ModifierNodeElement<RectListDebuggerModifierNode>() {
+ override fun create() = RectListDebuggerModifierNode()
+
+ override fun hashCode() = 123
+
+ override fun equals(other: Any?) = other === this
+
+ override fun update(node: RectListDebuggerModifierNode) {}
+}
+
+private class RectListDebuggerModifierNode : DrawModifierNode, Modifier.Node() {
+ private var paint =
+ Paint()
+ .also {
+ it.color = Color.Red
+ it.style = PaintingStyle.Stroke
+ }
+ .asFrameworkPaint()
+
+ var token: Any? = null
+
+ override fun onAttach() {
+ token = requireOwner().rectManager.registerOnChangedCallback { invalidateDraw() }
+ }
+
+ override fun onDetach() {
+ requireOwner().rectManager.unregisterOnChangedCallback(token)
+ }
+
+ override fun ContentDrawScope.draw() {
+ val rectList = requireOwner().rectManager.rects
+ val canvas = drawContext.canvas.nativeCanvas
+ val paint = paint
+ rectList.forEachRect { _, l, t, r, b ->
+ canvas.drawRect(l.toFloat(), t.toFloat(), r.toFloat(), b.toFloat(), paint)
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 9dd30ec..8b1d1dc9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -44,6 +44,7 @@
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.LayoutNode
@@ -56,11 +57,19 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.graphics.Insets
import androidx.core.view.NestedScrollingParent3
import androidx.core.view.NestedScrollingParentHelper
+import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryOwner
@@ -82,7 +91,12 @@
private val dispatcher: NestedScrollDispatcher,
val view: View,
private val owner: Owner,
-) : ViewGroup(context), NestedScrollingParent3, ComposeNodeLifecycleCallback, OwnerScope {
+) :
+ ViewGroup(context),
+ NestedScrollingParent3,
+ ComposeNodeLifecycleCallback,
+ OwnerScope,
+ OnApplyWindowInsetsListener {
init {
// Any [Abstract]ComposeViews that are descendants of this view will host
@@ -93,6 +107,21 @@
isSaveFromParentEnabled = false
@Suppress("LeakingThis") addView(view)
+ ViewCompat.setWindowInsetsAnimationCallback(
+ this,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: WindowInsetsAnimationCompat.BoundsCompat
+ ): WindowInsetsAnimationCompat.BoundsCompat = insetBounds(bounds)
+
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat = insetToLayoutPosition(insets)
+ }
+ )
+ ViewCompat.setOnApplyWindowInsetsListener(this, this)
}
// Keep nullable to match the `expect` declaration of InteropViewFactoryHolder
@@ -154,6 +183,9 @@
}
}
+ private val position = IntArray(2)
+ private var size = IntSize.Zero
+
/**
* The [OwnerSnapshotObserver] of this holder's [Owner]. Will be null when this view is not
* attached, since the observer is not valid unless the view is attached.
@@ -365,6 +397,14 @@
// these cases, we need to inform the View.
layoutAccordingTo(layoutNode)
@OptIn(InternalComposeUiApi::class) owner.onInteropViewLayoutChange(this)
+ val previousX = position[0]
+ val previousY = position[1]
+ view.getLocationOnScreen(position)
+ val oldSize = size
+ size = it.size
+ if (previousX != position[0] || previousY != position[1] || oldSize != size) {
+ view.requestApplyInsets()
+ }
}
layoutNode.compositeKeyHash = compositeKeyHash
layoutNode.modifier = modifier.then(coreModifier)
@@ -577,6 +617,52 @@
return view.isNestedScrollingEnabled
}
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ return insetToLayoutPosition(insets)
+ }
+
+ private fun insetToLayoutPosition(insets: WindowInsetsCompat): WindowInsetsCompat {
+ if (!insets.hasInsets()) {
+ return insets
+ }
+ return insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
+ }
+
+ private fun insetBounds(bounds: BoundsCompat): BoundsCompat =
+ insetValue(bounds) { l, t, r, b ->
+ BoundsCompat(bounds.lowerBound.inset(l, t, r, b), bounds.upperBound.inset(l, t, r, b))
+ }
+
+ private inline fun <T> insetValue(value: T, block: (l: Int, t: Int, r: Int, b: Int) -> T): T {
+ val coordinates = layoutNode.innerCoordinator
+ if (!coordinates.isAttached) {
+ return value
+ }
+ val topLeft = coordinates.positionInRoot().round()
+ val left = topLeft.x.fastCoerceAtLeast(0)
+ val top = topLeft.y.fastCoerceAtLeast(0)
+ val (rootWidth, rootHeight) = coordinates.findRootCoordinates().size
+ val (width, height) = coordinates.size
+ val bottomRight = coordinates.localToRoot(Offset(width.toFloat(), height.toFloat())).round()
+ val right = (rootWidth - bottomRight.x).fastCoerceAtLeast(0)
+ val bottom = (rootHeight - bottomRight.y).fastCoerceAtLeast(0)
+
+ return if (left == 0 && top == 0 && right == 0 && bottom == 0) {
+ value
+ } else {
+ block(left, top, right, bottom)
+ }
+ }
+
+ private fun Insets.inset(left: Int, top: Int, right: Int, bottom: Int): Insets {
+ return Insets.of(
+ (this.left - left).fastCoerceAtLeast(0),
+ (this.top - top).fastCoerceAtLeast(0),
+ (this.right - right).fastCoerceAtLeast(0),
+ (this.bottom - bottom).fastCoerceAtLeast(0)
+ )
+ }
+
companion object {
private val OnCommitAffectingUpdate: (AndroidViewHolder) -> Unit = {
it.handler.post(it.runUpdate)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..0e07822 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,17 @@
import android.graphics.Outline
import android.os.Build
import android.view.ContextThemeWrapper
+import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.Window
import android.view.WindowManager
+import android.widget.FrameLayout
import androidx.activity.ComponentDialog
import androidx.activity.addCallback
import androidx.compose.runtime.Composable
@@ -57,8 +61,12 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.graphics.Insets
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -66,6 +74,7 @@
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID
+import kotlin.math.roundToInt
/**
* Properties used to customize the behavior of a [Dialog].
@@ -77,16 +86,19 @@
* @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
* dialog's window.
* @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- * the platform default, which is smaller than the screen width.
+ * the platform default, which is smaller than the screen width. It is recommended to use
+ * [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ * using the entire screen and avoiding UI glitches on some devices when the IME animates in.
* @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
* `false` to use WindowInsets. If `false`, the
* [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
* [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- * `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ * `false` when [decorFitsSystemWindows] is false. When
+ * `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ * `false` and this property doesn't have any effect.
*/
@Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +230,7 @@
private var content: @Composable () -> Unit by mutableStateOf({})
var usePlatformDefaultWidth = false
+ var decorFitsSystemWindows = false
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@@ -229,50 +242,16 @@
createComposition()
}
- override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (usePlatformDefaultWidth) {
- super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
- } else {
- // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
- // platform default. Therefore, we create a new measure spec for width, which
- // corresponds to the full screen width. We do the same for height, even if
- // ViewRootImpl gives it to us from the first measure.
- val displayWidthMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
- val displayHeightMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
- super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
- }
- }
-
- override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.internalOnLayout(changed, left, top, right, bottom)
- // Now set the content size as fixed layout params, such that ViewRootImpl knows
- // the exact window size.
- if (!usePlatformDefaultWidth) {
- val child = getChildAt(0) ?: return
- window.setLayout(child.measuredWidth, child.measuredHeight)
- }
- }
-
- private val displayWidth: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
- }
-
- private val displayHeight: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
- }
-
@Composable
override fun Content() {
content()
}
}
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+ dialogProperties.decorFitsSystemWindows &&
+ context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
private class DialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: DialogProperties,
@@ -288,16 +267,17 @@
*/
ContextThemeWrapper(
composeView.context,
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
- ) {
+ if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
R.style.DialogWindowTheme
} else {
R.style.FloatingDialogWindowTheme
}
)
),
- ViewRootForInspector {
+ ViewRootForInspector,
+ OnApplyWindowInsetsListener,
+ OnLayoutChangeListener,
+ OnTouchListener {
private val dialogLayout: DialogLayout
@@ -308,15 +288,12 @@
override val subCompositionView: AbstractComposeView
get() = dialogLayout
- private val defaultSoftInputMode: Int
-
init {
val window = window ?: error("Dialog has no window")
- defaultSoftInputMode =
- window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setBackgroundDrawableResource(android.R.color.transparent)
- WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
dialogLayout =
DialogLayout(context, window).apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +313,8 @@
override fun getOutline(view: View, result: Outline) {
result.setRect(0, 0, view.width, view.height)
// We set alpha to 0 to hide the view's shadow and let the composable to
- // draw
- // its own shadow. This still enables us to get the extra space needed
- // in the
- // surface.
+ // draw its own shadow. This still enables us to get the extra space
+ // needed in the surface.
result.alpha = 0f
}
}
@@ -359,7 +334,56 @@
// Turn of all clipping so shadows can be drawn outside the window
(window.decorView as? ViewGroup)?.disableClipping()
- setContentView(dialogLayout)
+ // Center the ComposeView in a FrameLayout
+ val frameLayout = FrameLayout(context)
+ frameLayout.addView(
+ dialogLayout,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ .also { it.gravity = Gravity.CENTER }
+ )
+ frameLayout.clipChildren = false
+ ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+ ViewCompat.setWindowInsetsAnimationCallback(
+ frameLayout,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ private inline fun <T> insetValue(
+ unchangedValue: T,
+ block: (left: Int, top: Int, right: Int, bottom: Int) -> T
+ ): T {
+ if (properties.decorFitsSystemWindows) {
+ return unchangedValue
+ }
+ val left = maxOf(0, dialogLayout.left)
+ val top = maxOf(0, dialogLayout.top)
+ val right = maxOf(0, frameLayout.width - dialogLayout.right)
+ val bottom = maxOf(0, frameLayout.height - dialogLayout.bottom)
+ return if (left == 0 && top == 0 && right == 0 && bottom == 0) {
+ unchangedValue
+ } else {
+ block(left, top, right, bottom)
+ }
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: WindowInsetsAnimationCompat.BoundsCompat
+ ): WindowInsetsAnimationCompat.BoundsCompat =
+ insetValue(bounds) { l, t, r, b -> bounds.inset(Insets.of(l, t, r, b)) }
+
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat =
+ insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
+ }
+ )
+ dialogLayout.addOnLayoutChangeListener(this)
+ frameLayout.addOnLayoutChangeListener(this)
+
+ setContentView(frameLayout)
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +454,45 @@
this.properties = properties
setSecurePolicy(properties.securePolicy)
setLayoutDirection(layoutDirection)
- if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
- // Undo fixed size in internalOnLayout, which would suppress size changes when
- // usePlatformDefaultWidth is true.
- window?.setLayout(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT
- )
- }
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- if (properties.decorFitsSystemWindows) {
- window?.setSoftInputMode(defaultSoftInputMode)
- } else {
- @Suppress("DEPRECATION")
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+ setCanceledOnTouchOutside(properties.dismissOnClickOutside)
+ val frameLayout = dialogLayout.parent as View
+ frameLayout.setOnTouchListener(if (properties.dismissOnClickOutside) this else null)
+ val window = window
+ if (window != null) {
+ val softInput =
+ when {
+ decorFitsSystemWindows ->
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+ @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ }
+ window.setSoftInputMode(softInput)
+ val attrs = window.attributes
+ val measurementWidth =
+ if (properties.usePlatformDefaultWidth) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ val measurementHeight =
+ if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ if (
+ attrs.width != measurementWidth ||
+ attrs.height != measurementHeight ||
+ attrs.gravity != Gravity.CENTER
+ ) {
+ attrs.width = measurementWidth
+ attrs.height = measurementHeight
+ attrs.gravity = Gravity.CENTER
+ window.attributes = attrs
}
}
}
@@ -466,6 +514,49 @@
// Prevents the dialog from dismissing itself
return
}
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ if (properties.decorFitsSystemWindows) {
+ return insets
+ }
+ val left = maxOf(0, dialogLayout.left)
+ val top = maxOf(0, dialogLayout.top)
+ val right = maxOf(0, v.width - dialogLayout.right)
+ val bottom = maxOf(0, v.height - dialogLayout.bottom)
+ return insets.inset(left, top, right, bottom)
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ v.requestApplyInsets()
+ }
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ // This handler only set when properties.dismissOnClickOutside is true
+ if (event.actionMasked == MotionEvent.ACTION_UP) {
+ val x = event.x.roundToInt()
+ val y = event.y.roundToInt()
+ val insideContent =
+ x in dialogLayout.left..dialogLayout.right &&
+ y in dialogLayout.top..dialogLayout.bottom
+ if (!insideContent) {
+ onDismissRequest()
+ return true
+ }
+ }
+ // We must always accept the ACTION_DOWN or else we don't receive the rest of the
+ // event stream.
+ return event.actionMasked == MotionEvent.ACTION_DOWN
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..edfe7a1 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,11 @@
<style name="DialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
</style>
- <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
- be set on Dialogs without android:windowIsFloating set to false. -->
+ <!-- Style for decorFitsSystemWindows = false -->
<style name="FloatingDialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
<item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+ <item name="android:backgroundDimEnabled">true</item>
</style>
<style name="FloatingDialogTheme">
<item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
layout(d)
}
val recorder = Recorder()
- x.visitSubtree(Nodes.Draw, recorder)
+ x.visitSubtree(Nodes.Draw, block = recorder)
assertThat(recorder.recorded)
.isEqualTo(
listOf(
a.wrapped,
b,
- d,
c.wrapped,
+ d,
)
)
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 48999dc..09e3b36 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -67,6 +67,7 @@
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -2367,6 +2368,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -2508,6 +2511,9 @@
matrix.timesAssign(transform)
}
+ override val underlyingMatrix: Matrix
+ get() = transform
+
override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(inverseTransform)
}
@@ -2526,6 +2532,8 @@
layoutChangeCount++
}
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
@InternalComposeUiApi override fun onInteropViewLayoutChange(view: InteropView) {}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index bea163c..c4fce35 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -376,6 +377,8 @@
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
+ override val rectManager: RectManager = RectManager()
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -441,6 +444,8 @@
override fun onLayoutChange(layoutNode: LayoutNode) = TODO("Not yet implemented")
+ override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {}
+
override fun onInteropViewLayoutChange(view: InteropView) = TODO("Not yet implemented")
override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
- @Test
- fun testEnumerationOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderFullyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
- Truth.assertThat(stack.pop()).isEqualTo(2)
- Truth.assertThat(stack.pop()).isEqualTo(1)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
- }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
- val result = mutableListOf<T>()
- var item: T? = pop()
- while (item != null) {
- result.add(item)
- item = if (isNotEmpty()) pop() else null
- }
- return result
-}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt
new file mode 100644
index 0000000..21050a3
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/RectListTest.kt
@@ -0,0 +1,764 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.spatial
+
+import androidx.collection.mutableIntListOf
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.random.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RectListTest {
+
+ @Test
+ fun testInsert() {
+ val list = RectList()
+ list.insert(1, 1, 1, 2, 2)
+ assertIntersections(list, 1, 1, 2, 2, setOf(1))
+ }
+
+ @Test
+ fun testInsertsAndIntersections() {
+ val list = RectList()
+ // top left, 1x1 rect at 1,1
+ list.insert(1, 1, 1, 2, 2)
+ // top right, 1x1 rect at 11,1
+ list.insert(2, 11, 1, 12, 2)
+ // bottom left, 1x1 rect at 1,11
+ list.insert(3, 1, 11, 2, 12)
+ // bottom right, 1x1 rect at 11,11
+ list.insert(4, 11, 11, 12, 12)
+ // middle, 2,2 rect at 9,9
+ list.insert(5, 9, 9, 11, 11)
+ // top left, 1x1 rect at 5,5
+ list.insert(6, 5, 5, 6, 6)
+
+ // 1x1 rect at 3,3. nothing intersects.
+ assertIntersections(list, 3, 3, 4, 4, emptySet())
+
+ // top left
+ assertIntersections(list, 0, 0, 10, 10, setOf(1, 5, 6))
+
+ // top right
+ assertIntersections(list, 10, 0, 20, 10, setOf(5, 2))
+
+ // bottom left
+ assertIntersections(list, 0, 10, 10, 20, setOf(5, 3))
+
+ // bottom right
+ assertIntersections(list, 10, 10, 20, 20, setOf(5, 4))
+ }
+
+ @Test
+ fun testInsertExampleData() {
+ val list = RectList()
+ val testData = exampleLayoutRects
+ for (i in testData.indices) {
+ val rect = testData[i]
+ list.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ }
+
+ @Test
+ fun testFindIntersectingPoint() {
+ val testData = exampleLayoutRects
+ val queries = pointerInputQueries
+
+ // first 17 rects are big enough that they cover all queries
+ val bigRects = listOf(0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 210, 223)
+
+ val expectedResults =
+ arrayOf(
+ bigRects +
+ listOf(43, 44, 45, 46, 88, 106, 107, 161, 162, 163, 164, 170, 171, 173, 174),
+ bigRects + listOf(43, 44, 45, 46, 47, 48, 49, 55, 56),
+ bigRects + listOf(43, 44, 45, 46, 47, 48, 57, 58, 59, 65, 66, 67, 68),
+ bigRects + listOf(43, 44, 45, 46, 88, 89, 93, 94, 95, 96, 97, 98, 99, 104),
+ bigRects + listOf(16, 17, 18, 25, 26),
+ )
+
+ // we do a manual `rectContainsPoint` query here to validate that for all of the expected
+ // results, it returns true
+ for (i in expectedResults.indices) {
+ val x = queries[i][0]
+ val y = queries[i][1]
+ val results = expectedResults[i]
+ for (j in results.indices) {
+ val itemId = results[j]
+ val rect = exampleLayoutRects[itemId]
+ val (l, r, t, b) = rect
+ assert(
+ rectContainsPoint(
+ x,
+ y,
+ l,
+ r,
+ t,
+ b,
+ )
+ )
+ }
+ }
+
+ // populate the list
+ val rectList = RectList()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ rectList.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ // assert that forEachIntersection returns the expected results for each query
+ for (i in queries.indices) {
+ val list = mutableListOf<Int>()
+ val point = queries[i]
+ rectList.forEachIntersection(
+ point[0],
+ point[1],
+ ) {
+ list.add(it)
+ }
+ assertEquals(expectedResults[i].sorted(), list.sorted())
+ }
+ }
+
+ private fun insertRecursive(qt: RectList, item: Item, scrollableId: Int) {
+ val bounds = item.bounds
+
+ qt.insert(
+ item.id,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ parentId = scrollableId,
+ )
+ item.children.fastForEach {
+ insertRecursive(qt, it, if (item.scrollable) item.id else scrollableId)
+ }
+ }
+
+ @Test
+ fun testUpdate() {
+ val testData = exampleLayoutRects
+ val list = RectList()
+ insertRecursive(list, rootItem, -1)
+ val bounds = testData[100]
+
+ assertValueHasRect(list, 100, bounds[0], bounds[1], bounds[2], bounds[3])
+
+ list.update(100, 1, 2, 3, 4)
+
+ assertValueHasRect(list, 100, 1, 2, 3, 4)
+ }
+
+ private fun assertValueHasRect(list: RectList, itemId: Int, l: Int, t: Int, r: Int, b: Int) {
+ var called = false
+ val expected = "[$l,$t,$r,$b]"
+ list.withRect(itemId) { w, x, y, z ->
+ called = true
+ assertEquals(expected, "[$w,$x,$y,$z]")
+ }
+ if (!called) {
+ error("RectList did not have an item with id $itemId")
+ }
+ }
+
+ @Test
+ fun testUpdateAllItems() {
+ val testData = exampleLayoutRects
+ val r = Random(1234)
+ val list = RectList()
+ insertRecursive(list, rootItem, -1)
+ for (i in testData.indices) {
+ val rect = testData[i]
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ list.update(
+ i,
+ min(max(rect[0] + x, 0), 1439),
+ min(max(rect[1] + y, 0), 3119),
+ min(max(rect[2] + x, 0), 1439),
+ min(max(rect[3] + y, 0), 3119),
+ )
+ }
+ }
+
+ @Test
+ fun testUpdateScrollableContainer() {
+ val scrollableItems = scrollableItems
+ val r = Random(1234)
+ val qt = RectList()
+ insertRecursive(qt, rootItem, -1)
+ scrollableItems.fastForEach {
+ val x = r.nextInt(-100, 100)
+ val y = r.nextInt(-100, 100)
+ val bounds = it.bounds
+ qt.update(
+ it.id,
+ max(bounds[0] + x, 0),
+ max(bounds[1] + y, 0),
+ max(bounds[2] + x, 0),
+ max(bounds[3] + y, 0),
+ )
+ }
+ }
+
+ @Test
+ fun testNearestNeighbor() {
+ val list = RectList()
+ for (x in 0 until 10) {
+ for (y in 0 until 10) {
+ val id = x * 10 + y
+ list.insert(id, 10 * x, 10 * y, 10 * x + 10, 10 * y + 10)
+ }
+ }
+
+ val expectedResults =
+ arrayOf(
+ // arrays of [x, y, score]
+ intArrayOf(4, 3, 1), // immediate to the right should definitely be the winner
+ intArrayOf(
+ 4,
+ 2,
+ 11
+ ), // "up one" should tie with "down one" but still be a lowish score
+ intArrayOf(
+ 4,
+ 4,
+ 11
+ ), // "up one" should tie with "down one" but still be a lowish score
+ // TODO: we can tweak the scoring algorithm to have a higher penalty for not
+ // overlapping, which might put this rectangle in 2nd place. The current focus algo
+ // seems to heavily prioritize "in beam" elements, which are ones that would have
+ // overlap, and might place this rectangle higher
+ intArrayOf(5, 3, 11), // two to the right should not win, but also have a low score.
+ )
+
+ var i = 0
+ // nearest neighbor to the right of
+ list.findKNearestNeighbors(AxisEast, 4, 30, 30, 40, 40) { score, id, _, _, _, _ ->
+ val x = id / 10
+ val y = id % 10
+ val expected = expectedResults[i]
+ assertEquals(expected[0], x)
+ assertEquals(expected[1], y)
+ assertEquals(expected[2], score)
+ i++
+ }
+ }
+
+ @Test
+ fun testFindNearestNeighborInDirection() {
+ val testData = exampleLayoutRects
+ val queries = nearestNeighborQueries
+ val numberOfResults = 4
+ val qt = RectList()
+ for (i in testData.indices) {
+ val rect = testData[i]
+ qt.insert(
+ i,
+ rect[0],
+ rect[1],
+ rect[2],
+ rect[3],
+ )
+ }
+ for (i in queries.indices) {
+ for (direction in 1..4) {
+ val list = mutableIntListOf()
+ val bounds = queries[i]
+ qt.findKNearestNeighbors(
+ direction,
+ numberOfResults,
+ bounds[0],
+ bounds[1],
+ bounds[2],
+ bounds[3],
+ ) { _, id, _, _, _, _ ->
+ list.add(id)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testRectanglePacking() {
+ val rect = packXY(1, 2)
+ assertEquals(1, unpackX(rect))
+ assertEquals(2, unpackY(rect))
+ }
+
+ @Test
+ fun testMaxValueRectanglePacking() {
+ val maxValue = Int.MAX_VALUE
+
+ val rect = packXY(maxValue, 0)
+ assertEquals(maxValue, unpackX(rect))
+ assertEquals(0, unpackY(rect))
+
+ val rect1 = packXY(0, maxValue)
+ assertEquals(0, unpackX(rect1))
+ assertEquals(maxValue, unpackY(rect1))
+
+ val rect2 = packXY(maxValue, maxValue)
+ assertEquals(maxValue, unpackX(rect2))
+ assertEquals(maxValue, unpackY(rect2))
+ }
+
+ @Test
+ fun testMinValueRectanglePacking() {
+ val minValue = Int.MIN_VALUE
+
+ val rect = packXY(minValue, 0)
+ assertEquals(minValue, unpackX(rect))
+ assertEquals(0, unpackY(rect))
+
+ val rect1 = packXY(0, minValue)
+ assertEquals(0, unpackX(rect1))
+ assertEquals(minValue, unpackY(rect1))
+
+ val rect2 = packXY(minValue, minValue)
+ assertEquals(minValue, unpackX(rect2))
+ assertEquals(minValue, unpackY(rect2))
+ }
+
+ @Test
+ fun testMetaPacking() {
+ val meta =
+ packMeta(
+ itemId = 1,
+ parentId = 2,
+ lastChildOffset = 3,
+ focusable = false,
+ gesturable = true
+ )
+ assertEquals(1, unpackMetaValue(meta))
+ assertEquals(2, unpackMetaParentId(meta))
+ assertEquals(3, unpackMetaLastChildOffset(meta))
+ assertEquals(0, unpackMetaFocusable(meta))
+ assertEquals(1, unpackMetaGesturable(meta))
+ }
+
+ @Test
+ fun testMetaPackingNegativeScrollableValue() {
+ val meta =
+ packMeta(
+ itemId = 10,
+ parentId = -1,
+ lastChildOffset = 0,
+ focusable = true,
+ gesturable = false,
+ )
+ assertEquals(10, unpackMetaValue(meta))
+ // TODO: this actually returns 268,435,455. Not sure if we need to change this or not.
+ // assertEquals(-1, unpackMetaParentScrollableValue(meta))
+ assertEquals(1, unpackMetaFocusable(meta))
+ assertEquals(0, unpackMetaGesturable(meta))
+ }
+
+ private fun rectIntersectsRect(src: Rect, l: Int, t: Int, r: Int, b: Int): Boolean {
+ return rectIntersectsRect(
+ packXY(src.l, src.t),
+ packXY(src.r, src.b),
+ packXY(l, t),
+ packXY(r, b),
+ )
+ }
+
+ private fun distanceScore(axis: Int, query: Rect, target: Rect): Int {
+ return distanceScore(
+ axis,
+ query.l,
+ query.t,
+ query.r,
+ query.b,
+ target.l,
+ target.t,
+ target.r,
+ target.b,
+ )
+ }
+
+ @Test
+ fun testRectIntersectsRect() {
+ val src = Rect(10, 10, 20, 20)
+
+ // Not overlapping or touching
+ // ====
+
+ // top left
+ assertFalse(rectIntersectsRect(src, 1, 1, 2, 2))
+
+ // top right
+ assertFalse(rectIntersectsRect(src, 24, 1, 25, 2))
+
+ // bottom left
+ assertFalse(rectIntersectsRect(src, 1, 23, 2, 24))
+
+ // bottom right
+ assertFalse(rectIntersectsRect(src, 24, 24, 25, 25))
+
+ // top
+ assertFalse(rectIntersectsRect(src, 15, 5, 16, 6))
+
+ // left
+ assertFalse(rectIntersectsRect(src, 5, 15, 6, 16))
+
+ // bottom
+ assertFalse(rectIntersectsRect(src, 15, 25, 16, 26))
+
+ // right
+ assertFalse(rectIntersectsRect(src, 25, 15, 26, 16))
+
+ // Touching but not Overlapping
+ // ====
+
+ // just touches top left corner
+ assertTrue(rectIntersectsRect(src, 1, 1, 10, 10))
+
+ // just touches top right corner
+ assertTrue(rectIntersectsRect(src, 20, 1, 30, 10))
+
+ // just touches bottom right corner
+ assertTrue(rectIntersectsRect(src, 20, 20, 30, 30))
+
+ // just touches bottom left corner
+ assertTrue(rectIntersectsRect(src, 1, 20, 10, 30))
+
+ // left side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 1, 10, 10, 20))
+
+ // right side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 20, 10, 30, 20))
+
+ // top side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 10, 1, 20, 10))
+
+ // bottom side is touching but not overlapping
+ assertTrue(rectIntersectsRect(src, 10, 20, 20, 30))
+
+ // Clear Intersection
+ // ===
+
+ // partial overlap in top left corner
+ assertTrue(rectIntersectsRect(src, 1, 1, 11, 11))
+
+ // src is inside of dest
+ assertTrue(rectIntersectsRect(src, 1, 1, 30, 30))
+
+ // dest is inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 16, 16))
+
+ // full exact overlap
+ assertTrue(rectIntersectsRect(src, 10, 10, 20, 20))
+
+ // Zero Area Rectangles
+ // ===
+
+ // destination is zero rect outside of src
+ assertFalse(rectIntersectsRect(src, 1, 1, 1, 1))
+
+ // destination is zero rect inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 15, 15))
+
+ // destination is zero rect with height inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 15, 16))
+
+ // destination is zero rect with width inside of src
+ assertTrue(rectIntersectsRect(src, 15, 15, 16, 15))
+
+ // src is zero rect outside of dest
+ assertFalse(
+ rectIntersectsRect(
+ Rect(1, 1, 1, 1),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 15, 15),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect with height inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 15, 16),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+
+ // src is zero rect with width inside of dest
+ assertTrue(
+ rectIntersectsRect(
+ Rect(15, 15, 16, 15),
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+ )
+ }
+
+ @Test
+ fun testDistanceScore() {
+ val queryRect = Rect(10, 10, 20, 20)
+
+ // Negative Distance (opposite axis)
+ // ===
+
+ // Any rectangle which overlaps with the query rectangle should be
+ // disallowed (negative value)
+ assertNegative(distanceScore(AxisEast, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisSouth, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisWest, queryRect, Rect(11, 11, 19, 19)))
+
+ assertNegative(distanceScore(AxisNorth, queryRect, Rect(11, 11, 19, 19)))
+
+ // Perfect Overlaps, edges touching
+ // ===
+
+ // "perfect" overlap to the east of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(20, 10, 30, 20)), 1)
+
+ // "perfect" overlap to the south of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(10, 20, 20, 30)), 1)
+
+ // "perfect" overlap to the west of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(0, 10, 10, 20)), 1)
+
+ // "perfect" overlap to the north of this rect, we expect
+ // a low but positive score
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(10, 0, 20, 10)), 1)
+
+ // 1,1 rectangle 2px away along axis. Should be positive, but smaller number
+ // ===
+
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(22, 15, 23, 16)), 30)
+
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(15, 22, 16, 23)), 30)
+
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(7, 15, 8, 16)), 30)
+
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(15, 7, 16, 8)), 30)
+
+ // 1,1 rectangle 10px away along axis. Should be positive, but larger number
+ // ===
+
+ assertEquals(distanceScore(AxisEast, queryRect, Rect(30, 15, 31, 16)), 110)
+
+ assertEquals(distanceScore(AxisSouth, queryRect, Rect(15, 30, 16, 31)), 110)
+
+ assertEquals(distanceScore(AxisWest, queryRect, Rect(0, 15, 1, 16)), 100)
+
+ assertEquals(distanceScore(AxisNorth, queryRect, Rect(15, 0, 16, 1)), 100)
+ }
+
+ @Test
+ fun testDefragment() {
+ val r = RectList()
+
+ val toRemove = listOf(2, 7, 8)
+
+ for (i in 0 until 10) {
+ r.insert(
+ i,
+ 1,
+ 1,
+ 2,
+ 2,
+ )
+ }
+
+ assertEquals(30, r.itemsSize)
+
+ for (i in toRemove) {
+ r.remove(i)
+ }
+
+ // itemsSize still won't change, since the removed items are just
+ // tombstoned at this point
+ assertEquals(30, r.itemsSize)
+
+ for (i in 0 until 10) {
+ if (i !in toRemove) {
+ assertRectWithIdEquals(r, i, 1, 1, 2, 2)
+ }
+ }
+
+ r.defragment()
+
+ assertEquals(21, r.itemsSize)
+
+ for (i in 0 until 10) {
+ if (i !in toRemove) {
+ assertRectWithIdEquals(r, i, 1, 1, 2, 2)
+ }
+ }
+ }
+
+ @Test
+ fun testUpdateScrollable2() {
+ val r = RectList()
+
+ // insert scrollable container
+ r.insert(
+ 1,
+ 10,
+ 10,
+ 20,
+ 20,
+ )
+
+ // insert child container
+ r.insert(
+ 2,
+ 10,
+ 10,
+ 20,
+ 20,
+ parentId = 1,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, 10, 20, 20)
+
+ // move child items up by 1
+ r.updateSubhierarchy(
+ id = 1,
+ deltaX = 0,
+ deltaY = -1,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, 9, 20, 19)
+
+ // move child items up by 10 more
+ r.updateSubhierarchy(
+ id = 1,
+ deltaX = 0,
+ deltaY = -10,
+ )
+
+ assertRectWithIdEquals(r, 2, 10, -1, 20, 9)
+ }
+
+ // TODO: test update scrollable behavior
+ // TODO: test point intersection
+
+ private data class Rect(val l: Int, val t: Int, val r: Int, val b: Int)
+}
+
+fun assertNegative(actual: Int) {
+ assert(actual < 0) { "Expected negative value, got $actual" }
+}
+
+internal fun assertIntersections(
+ grid: RectList,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ expected: Set<Int>
+) {
+ val actualSet = mutableSetOf<Int>()
+ grid.forEachIntersection(l, t, r, b) {
+ assert(actualSet.add(it)) { "Encountered $it more than once" }
+ }
+ assertEquals(expected, actualSet)
+}
+
+internal fun rectContainsPoint(
+ x: Int,
+ y: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+): Boolean {
+ return (l < x) and (x < r) and (t < y) and (y < b)
+}
+
+internal fun assertRectWithIdEquals(
+ rectList: RectList,
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+) {
+ rectList.withRect(id) { w, x, y, z ->
+ assertRectEquals(
+ l,
+ t,
+ r,
+ b,
+ w,
+ x,
+ y,
+ z,
+ )
+ }
+}
+
+fun assertRectEquals(
+ l1: Int,
+ t1: Int,
+ r1: Int,
+ b1: Int,
+ l2: Int,
+ t2: Int,
+ r2: Int,
+ b2: Int,
+) {
+ assert(l1 == l2 && t1 == t2 && r1 == r2 && b1 == b2) {
+ "Expected: [$l1, $t1, $r1, $b1] Actual: [$l2, $t2, $r2, $b2]"
+ }
+}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt
new file mode 100644
index 0000000..6283fde
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/spatial/SpatialTestData.kt
@@ -0,0 +1,2309 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.spatial
+
+val occludingRectQueries =
+ arrayOf(
+ intArrayOf(490, 2073, 945, 2693),
+ intArrayOf(84, 2777, 1356, 2993),
+ intArrayOf(966, 2074, 1379, 2703),
+ intArrayOf(84, 1548, 1356, 1821),
+ intArrayOf(35, 2073, 490, 2693),
+ )
+
+val nearestNeighborQueries =
+ arrayOf(
+ intArrayOf(56, 2074, 469, 2703), // left side app image
+ intArrayOf(288, 2812, 576, 3036), // bottom nav bar middle button
+ intArrayOf(1048, 478, 1187, 548), // install button
+ intArrayOf(983, 1580, 1300, 1790), // show button
+ intArrayOf(1258, 1933, 1342, 2017), // more icon
+ )
+
+val pointerInputQueries =
+ arrayOf(
+ intArrayOf(1120, 1654), // Show button
+ intArrayOf(1263, 1943), // three dots button
+ intArrayOf(615, 2312), // app image
+ intArrayOf(1100, 496), // install button
+ intArrayOf(710, 215), // search bar
+ )
+
+class Item(
+ val id: Int,
+ val bounds: IntArray,
+ val scrollable: Boolean,
+ val focusable: Boolean,
+ val pointerInput: Boolean,
+) {
+ val children: MutableList<Item> = mutableListOf()
+
+ operator fun Item.unaryPlus() {
+ @Suppress("LABEL_RESOLVE_WILL_CHANGE") [email protected](this)
+ }
+}
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+): Item = Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput)
+
+fun Item(
+ id: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ scrollable: Boolean,
+ focusable: Boolean,
+ pointerInput: Boolean,
+ scope: Item.() -> Unit
+): Item {
+ return Item(id, intArrayOf(l, t, r, b), scrollable, focusable, pointerInput).apply { scope() }
+}
+
+val rootItem =
+ Item(0, 0, 0, 1440, 3120, false, false, false) {
+ +Item(1, 0, 0, 1440, 3120, false, false, false) {
+ +Item(2, 0, 0, 1, 1, false, false, false)
+ +Item(3, 0, 0, 1440, 3120, false, false, false) {
+ +Item(4, 0, 0, 1440, 3120, false, false, false) {
+ +Item(5, 0, 0, 1440, 3120, false, false, false) {
+ +Item(6, 0, 0, 1440, 3120, false, false, false) {
+ +Item(7, 0, 0, 1440, 3120, false, false, false) {
+ +Item(8, 0, 0, 1440, 3120, false, false, false) {
+ +Item(9, 0, 0, 1440, 3120, false, false, false) {
+ +Item(10, 0, 0, 1440, 3036, false, false, false) {
+ +Item(11, 0, 0, 1440, 2812, false, false, false) {
+ +Item(12, 0, 0, 1440, 2812, false, false, false) {
+ +Item(
+ 13,
+ 0,
+ 0,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 14,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 15,
+ 0,
+ 145,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 16,
+ 0,
+ 145,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 17,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 18,
+ 0,
+ 145,
+ 1440,
+ 369,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 19,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 20,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 21,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 22,
+ 14,
+ 173,
+ 182,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 23,
+ 28,
+ 187,
+ 168,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 24,
+ 56,
+ 215,
+ 140,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 25,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 26,
+ 196,
+ -1076,
+ 1076,
+ 1591,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 27,
+ 196,
+ 216,
+ 337,
+ 300,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 28,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 29,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 30,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 31,
+ 1090,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 32,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 33,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 34,
+ 1090,
+ 173,
+ 1258,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 35,
+ 1104,
+ 187,
+ 1244,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 36,
+ 1132,
+ 215,
+ 1216,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 37,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 38,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 39,
+ 1258,
+ 173,
+ 1426,
+ 341,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 40,
+ 1272,
+ 187,
+ 1412,
+ 327,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 41,
+ 1300,
+ 215,
+ 1384,
+ 299,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 42,
+ 0,
+ 369,
+ 1440,
+ 373,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 43,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 44,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 45,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 46,
+ 0,
+ 373,
+ 1440,
+ 2812,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 47,
+ 0,
+ 1877,
+ 1440,
+ 2735,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 48,
+ 0,
+ 1877,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 49,
+ 84,
+ 1877,
+ 1356,
+ 2073,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 50,
+ 84,
+ 1947,
+ 354,
+ 2003,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 51,
+ 84,
+ 1947,
+ 302,
+ 2003,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 52,
+ 354,
+ 1933,
+ 1188,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 53,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 54,
+ 354,
+ 1933,
+ 1012,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 55,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 56,
+ 1258,
+ 1933,
+ 1342,
+ 2017,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 57,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 58,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 59,
+ 0,
+ 2073,
+ 1440,
+ 2693,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 60,
+ 945,
+ 2073,
+ 1400,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 61,
+ 966,
+ 2074,
+ 1379,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 62,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 63,
+ 966,
+ 2074,
+ 1379,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 64,
+ 966,
+ 2515,
+ 1379,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 65,
+ 490,
+ 2073,
+ 945,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 66,
+ 511,
+ 2074,
+ 924,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 67,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 68,
+ 511,
+ 2074,
+ 924,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 69,
+ 511,
+ 2515,
+ 924,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 70,
+ 1400,
+ 2073,
+ 1855,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 71,
+ 1421,
+ 2074,
+ 1834,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 72,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 73,
+ 1421,
+ 2074,
+ 1834,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 74,
+ 1421,
+ 2515,
+ 1834,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 75,
+ 35,
+ 2073,
+ 490,
+ 2693,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 76,
+ 56,
+ 2074,
+ 469,
+ 2703,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 77,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 78,
+ 56,
+ 2074,
+ 469,
+ 2487,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 79,
+ 56,
+ 2515,
+ 469,
+ 2703,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 80,
+ 0,
+ 2777,
+ 1440,
+ 2993,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 81,
+ 84,
+ 2777,
+ 1356,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 82,
+ 84,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 83,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 84,
+ 84,
+ 2777,
+ 280,
+ 2973,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 85,
+ 336,
+ 2777,
+ 1188,
+ 2993,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 86,
+ 1188,
+ 2777,
+ 1356,
+ 2945,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 87,
+ 1272,
+ 2826,
+ 1356,
+ 2910,
+ false,
+ true,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 88,
+ 0,
+ 373,
+ 1440,
+ 1849,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 89,
+ 84,
+ 429,
+ 1356,
+ 645,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 90,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 91,
+ 84,
+ 436,
+ 280,
+ 632,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 92,
+ 336,
+ 436,
+ 950,
+ 652,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 93,
+ 992,
+ 429,
+ 1356,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 94,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 95,
+ 992,
+ 429,
+ 1356,
+ 597,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 96,
+ 992,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 97,
+ 1048,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 98,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 99,
+ 1048,
+ 478,
+ 1187,
+ 548,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 100,
+ 1229,
+ 443,
+ 1356,
+ 583,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 101,
+ 1229,
+ 443,
+ 1233,
+ 583,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 102,
+ 1233,
+ 443,
+ 1356,
+ 583,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 103,
+ 1256,
+ 482,
+ 1319,
+ 545,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 104,
+ 1056,
+ 429,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 105,
+ 1056,
+ 583,
+ 1293,
+ 639,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 106,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 107,
+ 0,
+ 687,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 108,
+ 0,
+ 687,
+ 1440,
+ 911,
+ true,
+ false,
+ false
+ ) {
+ +Item(
+ 109,
+ 0,
+ 799,
+ 84,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 110,
+ 84,
+ 729,
+ 377,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 111,
+ 171,
+ 733,
+ 291,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 112,
+ 171,
+ 733,
+ 242,
+ 803,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 113,
+ 249,
+ 747,
+ 291,
+ 789,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 114,
+ 84,
+ 810,
+ 377,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 115,
+ 338,
+ 818,
+ 377,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 116,
+ 377,
+ 799,
+ 433,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 117,
+ 433,
+ 762,
+ 437,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 118,
+ 437,
+ 799,
+ 493,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 119,
+ 493,
+ 729,
+ 839,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 120,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 121,
+ 631,
+ 733,
+ 701,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 122,
+ 493,
+ 810,
+ 839,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 123,
+ 799,
+ 818,
+ 838,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 124,
+ 839,
+ 799,
+ 895,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 125,
+ 895,
+ 762,
+ 899,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 126,
+ 899,
+ 799,
+ 955,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 127,
+ 955,
+ 729,
+ 1235,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 128,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 129,
+ 1060,
+ 733,
+ 1130,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 130,
+ 1022,
+ 810,
+ 1168,
+ 866,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 131,
+ 1129,
+ 818,
+ 1168,
+ 857,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 132,
+ 1235,
+ 799,
+ 1291,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 133,
+ 1291,
+ 762,
+ 1295,
+ 837,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 134,
+ 1295,
+ 799,
+ 1351,
+ 799,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 135,
+ 1351,
+ 729,
+ 1631,
+ 869,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 136,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 137,
+ 1451,
+ 733,
+ 1532,
+ 803,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 138,
+ 1383,
+ 810,
+ 1599,
+ 866,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 139,
+ 1631,
+ 799,
+ 1687,
+ 799,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 140,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 141,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 142,
+ 0,
+ 911,
+ 1440,
+ 1506,
+ true,
+ false,
+ true
+ ) {
+ +Item(
+ 143,
+ 1348,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 144,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 145,
+ 1380,
+ 911,
+ 2644,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 146,
+ 1380,
+ 911,
+ 1975,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 147,
+ 2031,
+ 911,
+ 2588,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 148,
+ 2031,
+ 967,
+ 2588,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 149,
+ 2031,
+ 1051,
+ 2588,
+ 1121,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 150,
+ 2031,
+ 1149,
+ 2588,
+ 1289,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 151,
+ 2031,
+ 1345,
+ 2031,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ +Item(
+ 152,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 153,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 154,
+ 84,
+ 911,
+ 1348,
+ 1506,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 155,
+ 84,
+ 911,
+ 679,
+ 1506,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 156,
+ 735,
+ 911,
+ 1292,
+ 1506,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 157,
+ 735,
+ 967,
+ 1292,
+ 1023,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 158,
+ 735,
+ 1051,
+ 1292,
+ 1191,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 159,
+ 735,
+ 1219,
+ 1292,
+ 1359,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 160,
+ 735,
+ 1415,
+ 735,
+ 1506,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 161,
+ 0,
+ 1506,
+ 1440,
+ 1849,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 162,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 163,
+ 84,
+ 1548,
+ 1356,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 164,
+ 140,
+ 1548,
+ 1300,
+ 1821,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 165,
+ 140,
+ 1604,
+ 927,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 166,
+ 140,
+ 1604,
+ 865,
+ 1688,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 167,
+ 140,
+ 1702,
+ 469,
+ 1765,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 168,
+ 140,
+ 1702,
+ 203,
+ 1765,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 169,
+ 217,
+ 1706,
+ 469,
+ 1762,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(
+ 170,
+ 983,
+ 1580,
+ 1300,
+ 1790,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 171,
+ 1040,
+ 1615,
+ 1243,
+ 1755,
+ false,
+ true,
+ true
+ ) {
+ +Item(
+ 172,
+ 1040,
+ 1654,
+ 1103,
+ 1717,
+ false,
+ false,
+ false
+ )
+ +Item(
+ 173,
+ 1103,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 174,
+ 1117,
+ 1650,
+ 1243,
+ 1720,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(
+ 175,
+ 42,
+ 2770,
+ 1398,
+ 2770,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(176, 0, 2812, 1440, 3036, false, false, false) {
+ +Item(
+ 177,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ true
+ ) {
+ +Item(
+ 178,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 179,
+ 0,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 180,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 181,
+ 0,
+ 2812,
+ 288,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 182,
+ 46,
+ 2847,
+ 242,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 183,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 184,
+ 14,
+ 2931,
+ 274,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 185,
+ 74,
+ 2945,
+ 214,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 186,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 187,
+ 288,
+ 2812,
+ 576,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 188,
+ 334,
+ 2847,
+ 530,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 189,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 190,
+ 302,
+ 2931,
+ 562,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 191,
+ 381,
+ 2945,
+ 484,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 192,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 193,
+ 576,
+ 2812,
+ 864,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 194,
+ 622,
+ 2847,
+ 818,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 195,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 196,
+ 590,
+ 2931,
+ 850,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 197,
+ 649,
+ 2945,
+ 792,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 198,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 199,
+ 864,
+ 2812,
+ 1152,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 200,
+ 910,
+ 2847,
+ 1106,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 201,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 202,
+ 878,
+ 2931,
+ 1138,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 203,
+ 964,
+ 2945,
+ 1052,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ +Item(
+ 204,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ true,
+ false
+ ) {
+ +Item(
+ 205,
+ 1152,
+ 2812,
+ 1440,
+ 3036,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 206,
+ 1198,
+ 2847,
+ 1394,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 207,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ true,
+ false
+ )
+ }
+ +Item(
+ 208,
+ 1166,
+ 2931,
+ 1426,
+ 3001,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 209,
+ 1235,
+ 2945,
+ 1358,
+ 3001,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ +Item(210, 0, 0, 1440, 3120, false, false, false) {
+ +Item(211, 102, 2847, 186, 2931, false, false, false) {
+ +Item(
+ 212,
+ 102,
+ 2847,
+ 186,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(213, 390, 2847, 474, 2931, false, false, false) {
+ +Item(
+ 214,
+ 390,
+ 2847,
+ 474,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(215, 678, 2847, 762, 2931, false, false, false) {
+ +Item(
+ 216,
+ 678,
+ 2847,
+ 762,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(217, 966, 2847, 1050, 2931, false, false, false) {
+ +Item(
+ 218,
+ 966,
+ 2847,
+ 1050,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ +Item(
+ 219,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ ) {
+ +Item(
+ 220,
+ 1254,
+ 2847,
+ 1338,
+ 2931,
+ false,
+ false,
+ false
+ )
+ }
+ }
+ +Item(221, 0, 0, 1, 1, false, false, false) {
+ +Item(222, 0, 0, 1440, 196, false, false, false)
+ }
+ }
+ }
+ +Item(223, 0, 0, 1440, 3120, false, false, false) {
+ +Item(224, 636, 1476, 804, 1644, false, false, false)
+ }
+ +Item(225, 0, 0, 1, 1, false, false, false)
+ }
+ +Item(226, 0, 0, 1440, 145, false, false, false) {
+ +Item(227, 0, 145, 1440, 145, false, false, false)
+ }
+ }
+ }
+ +Item(228, 0, 0, 1, 1, false, false, false)
+ }
+ }
+ }
+ +Item(229, 0, 3036, 1440, 3120, false, false, false)
+ +Item(230, 0, 0, 1440, 145, false, false, false)
+ }
+
+val exampleLayoutRects: Array<IntArray> = run {
+ val emptyIntArray = IntArray(0)
+ val results = Array(231) { emptyIntArray }
+
+ fun push(item: Item) {
+ results[item.id] = item.bounds
+ item.children.forEach { child -> push(child) }
+ }
+ push(rootItem)
+ for (bounds in results) {
+ assert(bounds !== emptyIntArray)
+ }
+ results
+}
+
+val scrollableItems: List<Item> = run {
+ val results = mutableListOf<Item>()
+
+ fun traverse(item: Item) {
+ if (item.scrollable) {
+ results.add(item)
+ }
+ item.children.forEach { child -> traverse(child) }
+ }
+ traverse(rootItem)
+ results
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
new file mode 100644
index 0000000..536ef76
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("ComposeRuntimeFlags")
+
+package androidx.compose.ui
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmName
+
+/**
+ * This is a collection of flags which are used to guard against regressions in some of the
+ * "riskier" refactors or new feature support that is added to this module. These flags are always
+ * "on" in the published artifact of this module, however these flags allow end consumers of this
+ * module to toggle them "off" in case this new path is causing a regression.
+ *
+ * These flags are considered temporary, and there should be no expectation for these flags be
+ * around for an extended period of time. If you have a regression that one of these flags fixes, it
+ * is strongly encouraged for you to file a bug ASAP.
+ *
+ * **Usage:**
+ *
+ * In order to turn a feature off in a debug environment, it is recommended to set this to false in
+ * as close to the initial loading of the application as possible. Changing this value after compose
+ * library code has already been loaded can result in undefined behavior.
+ *
+ * class MyApplication : Application() {
+ * override fun onCreate() {
+ * ComposeUiFlags.SomeFeatureEnabled = false
+ * super.onCreate()
+ * }
+ * }
+ *
+ * In order to turn this off in a release environment, it is recommended to additionally utilize R8
+ * rules which force a single value for the entire build artifact. This can result in the new code
+ * paths being completely removed from the artifact, which can often have nontrivial positive
+ * performance impact.
+ *
+ * -assumevalues class androidx.compose.runtime.ComposeUiFlags {
+ * public static int isRectTrackingEnabled return false
+ * }
+ */
+@ExperimentalComposeUiApi
+object ComposeUiFlags {
+ /**
+ * With this flag on, during layout we will do some additional work to store the minimum
+ * bounding rectangles for all Layout Nodes. This introduces some additional maintenance burden,
+ * but will be used in the future to enable certain features that are not possible to do
+ * efficiently at this point, as well as speed up some other areas of the system such as
+ * semantics, focus, pointer input, etc. If significant performance overhead is noticed during
+ * layout phases, it is possible that the addition of this tracking is the culprit.
+ */
+ @Suppress("MutableBareField") @JvmField var isRectTrackingEnabled: Boolean = true
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 0bb8464..09354a6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -206,6 +206,9 @@
internal var updatedNodeAwaitingAttachForInvalidation = false
private var onAttachRunExpected = false
private var onDetachRunExpected = false
+
+ internal var detachedListener: (() -> Unit)? = null
+
/**
* Indicates that the node is attached to a [androidx.compose.ui.layout.Layout] which is
* part of the UI tree. This will get set to true right before [onAttach] is called, and set
@@ -273,6 +276,7 @@
"markAsDetached()"
}
onDetachRunExpected = false
+ detachedListener?.invoke()
onDetach()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.kt
index ff75eb9..f511843 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.draganddrop
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.DrawScope
/**
* Definition for a type representing transferable data. It could be a remote URI, rich text data on
@@ -33,6 +35,27 @@
*/
internal expect val DragAndDropEvent.positionInRoot: Offset
+/** A scope that allows starting a drag and drop session. */
+interface DragAndDropStartTransferScope {
+ /**
+ * Initiates a drag-and-drop operation for transferring data.
+ *
+ * @param transferData the data to be transferred after successful completion of the drag and
+ * drop gesture.
+ * @param decorationSize the size of the drag decoration to be drawn.
+ * @param drawDragDecoration provides the visual representation of the item dragged during the
+ * drag and drop gesture.
+ * @return true if the method completes successfully, or false if it fails anywhere. Returning
+ * false means the system was unable to do a drag because of another ongoing operation or some
+ * other reasons.
+ */
+ fun startDragAndDropTransfer(
+ transferData: DragAndDropTransferData,
+ decorationSize: Size,
+ drawDragDecoration: DrawScope.() -> Unit,
+ ): Boolean
+}
+
/** Provides a means of receiving a transfer data from a drag and drop session. */
interface DragAndDropTarget {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropManager.kt
index ae06107..5425cd9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropManager.kt
@@ -17,9 +17,9 @@
package androidx.compose.ui.draganddrop
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.geometry.Offset
+/** A platform implementation for drag and drop functionality. */
internal interface DragAndDropManager {
/**
@@ -29,32 +29,28 @@
val modifier: Modifier
/**
- * Initiates a drag-and-drop operation for transferring data.
- *
- * @param transferData the data to be transferred after successful completion of the drag and
- * drop gesture.
- * @param decorationSize the size of the drag decoration to be drawn.
- * @param drawDragDecoration provides the visual representation of the item dragged during the
- * drag and drop gesture.
- * @return true if the method completes successfully, or false if it fails anywhere. Returning
- * false means the system was unable to do a drag because of another ongoing operation or some
- * other reasons.
+ * Returns a boolean value indicating whether requesting drag and drop transfer is required. If
+ * it's not, the transfer might be initiated only be system and calling
+ * [requestDragAndDropTransfer] will be ignored.
*/
- fun drag(
- transferData: DragAndDropTransferData,
- decorationSize: Size,
- drawDragDecoration: DrawScope.() -> Unit,
- ): Boolean
+ val isRequestDragAndDropTransferRequired: Boolean
/**
- * Called to notify this [DragAndDropManager] that a [DragAndDropModifierNode] is interested in
+ * Requests a drag and drop transfer. It might ignored in case if the operation performed by
+ * system. [isRequestDragAndDropTransferRequired] can be used to check if it should be used
+ * explicitly.
+ */
+ fun requestDragAndDropTransfer(node: DragAndDropNode, offset: Offset)
+
+ /**
+ * Called to notify this [DragAndDropManager] that a [DragAndDropTarget] is interested in
* receiving events for a particular drag and drop session.
*/
- fun registerNodeInterest(node: DragAndDropModifierNode)
+ fun registerTargetInterest(target: DragAndDropTarget)
/**
- * Called to check if a [DragAndDropModifierNode] has previously registered interest for a drag
- * and drop session.
+ * Called to check if a [DragAndDropTarget] has previously registered interest for a drag and
+ * drop session.
*/
- fun isInterestedNode(node: DragAndDropModifierNode): Boolean
+ fun isInterestedTarget(target: DragAndDropTarget): Boolean
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropNode.kt
index 20dba97..480d5f8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropNode.kt
@@ -19,10 +19,12 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction.CancelTraversal
@@ -31,12 +33,22 @@
import androidx.compose.ui.node.requireLayoutNode
import androidx.compose.ui.node.requireOwner
import androidx.compose.ui.node.traverseDescendants
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
+import kotlin.js.JsName
+import kotlin.jvm.JvmName
/**
* A [Modifier.Node] providing low level access to platform drag and drop operations. In most cases,
* you will want to delegate to the [DragAndDropModifierNode] returned by the eponymous factory
* method.
*/
+@Deprecated(
+ message =
+ "This interface is deprecated in favor to " +
+ "DragAndDropSourceModifierNode and DragAndDropTargetModifierNode",
+ replaceWith = ReplaceWith("DragAndDropSourceModifierNode")
+)
interface DragAndDropModifierNode : DelegatableNode, DragAndDropTarget {
/**
* Begins a drag and drop session for transferring data.
@@ -47,6 +59,7 @@
* @param drawDragDecoration provides the visual representation of the item dragged during the
* drag and drop gesture.
*/
+ @Deprecated("Use DragAndDropSourceModifierNode.requestDragAndDropTransfer instead")
fun drag(
transferData: DragAndDropTransferData,
decorationSize: Size,
@@ -67,10 +80,51 @@
}
/**
+ * A [Modifier.Node] that can be used as a source for platform drag and drop operations. In most
+ * cases, you will want to delegate to the [DragAndDropSourceModifierNode] returned by the eponymous
+ * factory method.
+ */
+sealed interface DragAndDropSourceModifierNode : LayoutAwareModifierNode {
+ /**
+ * Returns a boolean value indicating whether requesting drag and drop transfer is required.
+ *
+ * This variable is used to check if the platform requires drag and drop transfer initiated by
+ * application explicitly, for example via a custom gesture.
+ *
+ * @see requestDragAndDropTransfer
+ */
+ val isRequestDragAndDropTransferRequired: Boolean
+
+ /**
+ * Requests a drag and drop transfer. [isRequestDragAndDropTransferRequired] can be used to
+ * check if it required to be performed.
+ *
+ * @param offset the offset value representing position of the input pointer.
+ */
+ fun requestDragAndDropTransfer(offset: Offset)
+}
+
+/**
+ * A [Modifier.Node] that can be used as a target for platform drag and drop operations. In most
+ * cases, you will want to delegate to the [DragAndDropTargetModifierNode] returned by the eponymous
+ * factory method.
+ *
+ * This interface does not define any additional methods or properties. It simply serves as a marker
+ * interface to identify nodes that can be used as drag and drop target modifiers.
+ */
+sealed interface DragAndDropTargetModifierNode : LayoutAwareModifierNode
+
+/**
* Creates a [Modifier.Node] for starting platform drag and drop sessions with the intention of
* transferring data. A drag and stop session is started by calling [DragAndDropModifierNode.drag].
*/
-fun DragAndDropModifierNode(): DragAndDropModifierNode = DragAndDropNode { null }
+@Deprecated(
+ message = "Use DragAndDropSourceModifierNode instead",
+ replaceWith = ReplaceWith("DragAndDropSourceModifierNode")
+)
+@Suppress("DEPRECATION")
+@JsName("funDragAndDropModifierNode1")
+fun DragAndDropModifierNode(): DragAndDropModifierNode = DragAndDropNode(onStartTransfer = null)
/**
* Creates a [Modifier.Node] for receiving transfer data from platform drag and drop sessions. All
@@ -82,12 +136,47 @@
* it.
* @param target allows for receiving events and transfer data from a given drag and drop session.
*/
+@Deprecated(
+ message = "Use DragAndDropTargetModifierNode instead",
+ replaceWith = ReplaceWith("DragAndDropTargetModifierNode")
+)
+@Suppress("DEPRECATION")
+@JsName("funDragAndDropModifierNode2")
fun DragAndDropModifierNode(
shouldStartDragAndDrop: (event: DragAndDropEvent) -> Boolean,
target: DragAndDropTarget
-): DragAndDropModifierNode = DragAndDropNode { startEvent ->
- if (shouldStartDragAndDrop(startEvent)) target else null
-}
+): DragAndDropModifierNode =
+ DragAndDropNode(
+ onDropTargetValidate = { event -> if (shouldStartDragAndDrop(event)) target else null }
+ )
+
+/**
+ * Creates a [DragAndDropSourceModifierNode] for starting platform drag and drop sessions with the
+ * intention of transferring data.
+ *
+ * @param onStartTransfer the callback function that is invoked when drag and drop session starts.
+ * It takes an [Offset] parameter representing the start position of the drag.
+ */
+fun DragAndDropSourceModifierNode(
+ onStartTransfer: DragAndDropStartTransferScope.(Offset) -> Unit,
+): DragAndDropSourceModifierNode = DragAndDropNode(onStartTransfer = onStartTransfer)
+
+/**
+ * Creates a [DragAndDropTargetModifierNode] for receiving transfer data from platform drag and drop
+ * sessions.
+ *
+ * @param shouldStartDragAndDrop allows for inspecting the start [DragAndDropEvent] for a given
+ * session to decide whether or not the provided [DragAndDropTarget] would like to receive from
+ * it.
+ * @param target allows for receiving events and transfer data from a given drag and drop session.
+ */
+fun DragAndDropTargetModifierNode(
+ shouldStartDragAndDrop: (event: DragAndDropEvent) -> Boolean,
+ target: DragAndDropTarget
+): DragAndDropTargetModifierNode =
+ DragAndDropNode(
+ onDropTargetValidate = { event -> if (shouldStartDragAndDrop(event)) target else null }
+ )
/**
* Core implementation of drag and drop. This [Modifier.Node] implements tree traversal for drag and
@@ -102,23 +191,47 @@
*
* This optimizes traversal for the common case of move events where the event remains within a
* single node, or moves to a sibling of the node.
+ *
+ * This intended to be used directly only by [DragAndDropManager].
*/
+@Suppress("DEPRECATION")
internal class DragAndDropNode(
- private val onDragAndDropStart: (event: DragAndDropEvent) -> DragAndDropTarget?
-) : Modifier.Node(), TraversableNode, DragAndDropModifierNode {
- companion object {
+ private var onStartTransfer: (DragAndDropStartTransferScope.(Offset) -> Unit)? = null,
+ private val onDropTargetValidate: ((DragAndDropEvent) -> DragAndDropTarget?)? = null,
+) :
+ Modifier.Node(),
+ TraversableNode,
+ DragAndDropModifierNode,
+ DragAndDropSourceModifierNode,
+ DragAndDropTargetModifierNode,
+ DragAndDropTarget {
+ private companion object {
private object DragAndDropTraversableKey
}
override val traverseKey: Any = DragAndDropTraversableKey
- /** Child currently receiving drag gestures for dropping into * */
- private var lastChildDragAndDropModifierNode: DragAndDropModifierNode? = null
+ private val dragAndDropManager: DragAndDropManager
+ get() = requireOwner().dragAndDropManager
- /** This as a drop target if eligible for processing * */
+ /** Child currently receiving drag gestures for dropping into */
+ private var lastChildDragAndDropModifierNode: DragAndDropNode? = null
+
+ /** This as a drop target if eligible for processing */
private var thisDragAndDropTarget: DragAndDropTarget? = null
+ /**
+ * Indicates whether there is a child that is eligible to receive a drop gesture immediately.
+ * This is true if the last move happened over a child that is interested in receiving a drop.
+ */
+ @get:JvmName("hasEligibleDropTarget")
+ val hasEligibleDropTarget: Boolean
+ get() = lastChildDragAndDropModifierNode != null || thisDragAndDropTarget != null
+
+ internal var size: IntSize = IntSize.Zero
+
// start Node
+
override fun onDetach() {
// Clean up
thisDragAndDropTarget = null
@@ -127,22 +240,91 @@
// end Node
+ // start LayoutAwareModifierNode
+
+ override fun onRemeasured(size: IntSize) {
+ this.size = size
+ }
+
+ // end LayoutAwareModifierNode
+
+ // start DragAndDropSourceModifierNode
+
+ override val isRequestDragAndDropTransferRequired: Boolean
+ get() = dragAndDropManager.isRequestDragAndDropTransferRequired
+
+ override fun requestDragAndDropTransfer(offset: Offset) {
+ checkPrecondition(onStartTransfer != null)
+ dragAndDropManager.requestDragAndDropTransfer(this, offset)
+ }
+
+ // end DragAndDropSourceModifierNode
+
+ /**
+ * Initiates a drag-and-drop operation for transferring data.
+ *
+ * @param offset the offset value representing position of the input pointer.
+ * @param isTransferStarted a lambda function that returns true if the drag-and-drop transfer
+ * has started, or false otherwise.
+ */
+ fun DragAndDropStartTransferScope.startDragAndDropTransfer(
+ offset: Offset,
+ isTransferStarted: () -> Boolean
+ ) {
+ val nodeCoordinates = requireLayoutNode().coordinates
+ traverseSelfAndDescendants { currentNode ->
+ // TODO: b/303904810 unattached nodes should not be found from an attached
+ // root drag and drop node
+ if (!currentNode.isAttached) {
+ return@traverseSelfAndDescendants SkipSubtreeAndContinueTraversal
+ }
+
+ val onStartTransfer =
+ currentNode.onStartTransfer ?: return@traverseSelfAndDescendants ContinueTraversal
+
+ if (offset != Offset.Unspecified) {
+ val currentCoordinates = currentNode.requireLayoutNode().coordinates
+ val localPosition = currentCoordinates.localPositionOf(nodeCoordinates, offset)
+ if (!currentNode.size.toSize().toRect().contains(localPosition)) {
+ return@traverseSelfAndDescendants ContinueTraversal
+ }
+
+ onStartTransfer.invoke(this, localPosition)
+ } else {
+ onStartTransfer.invoke(this, Offset.Unspecified)
+ }
+
+ if (isTransferStarted()) {
+ CancelTraversal
+ } else {
+ ContinueTraversal
+ }
+ }
+ }
+
// start DragAndDropModifierNode
+ @Deprecated("Use DragAndDropSourceModifierNode.requestDragAndDropTransfer instead")
override fun drag(
transferData: DragAndDropTransferData,
decorationSize: Size,
- drawDragDecoration: DrawScope.() -> Unit,
+ drawDragDecoration: DrawScope.() -> Unit
) {
- requireOwner()
- .dragAndDropManager
- .drag(
- transferData = transferData,
- decorationSize = decorationSize,
- drawDragDecoration = drawDragDecoration
- )
+ checkPrecondition(onStartTransfer == null)
+ onStartTransfer = {
+ startDragAndDropTransfer(transferData, decorationSize, drawDragDecoration)
+ }
+ dragAndDropManager.requestDragAndDropTransfer(this, Offset.Unspecified)
+ onStartTransfer = null
}
+ /**
+ * The entry point to register interest in a drag and drop session for receiving data.
+ *
+ * @return true to indicate interest in the contents of a drag and drop session, false indicates
+ * no interest. If false is returned, this [Modifier] will not receive any [DragAndDropTarget]
+ * events.
+ */
override fun acceptDragAndDropTransfer(startEvent: DragAndDropEvent): Boolean {
var handled = false
traverseSelfAndDescendants { currentNode ->
@@ -158,11 +340,11 @@
}
// Start receiving events
- currentNode.thisDragAndDropTarget = currentNode.onDragAndDropStart(startEvent)
+ currentNode.thisDragAndDropTarget = currentNode.onDropTargetValidate?.invoke(startEvent)
val accepted = currentNode.thisDragAndDropTarget != null
if (accepted) {
- requireOwner().dragAndDropManager.registerNodeInterest(currentNode)
+ dragAndDropManager.registerTargetInterest(currentNode)
}
handled = handled || accepted
ContinueTraversal
@@ -189,8 +371,8 @@
}
override fun onMoved(event: DragAndDropEvent) {
- val currentChildNode: DragAndDropModifierNode? = lastChildDragAndDropModifierNode
- val newChildNode: DragAndDropModifierNode? =
+ val currentChildNode: DragAndDropNode? = lastChildDragAndDropModifierNode
+ val newChildNode: DragAndDropNode? =
when {
// Moved within child.
currentChildNode?.contains(event.positionInRoot) == true -> currentChildNode
@@ -198,7 +380,7 @@
else ->
firstDescendantOrNull { child ->
// Only dispatch to children who previously accepted the onStart gesture
- requireOwner().dragAndDropManager.isInterestedNode(child) &&
+ dragAndDropManager.isInterestedTarget(child) &&
child.contains(event.positionInRoot)
}
}
@@ -270,17 +452,19 @@
}
/** Hit test for a [DragAndDropNode]. */
-private fun DragAndDropModifierNode.contains(position: Offset): Boolean {
+private fun DragAndDropNode.contains(positionInRoot: Offset): Boolean {
if (!node.isAttached) return false
val currentCoordinates = requireLayoutNode().coordinates
if (!currentCoordinates.isAttached) return false
- val (width, height) = currentCoordinates.size
val (x1, y1) = currentCoordinates.positionInRoot()
- val x2 = x1 + width
- val y2 = y1 + height
- return position.x in x1..x2 && position.y in y1..y2
+ // Use measured size instead of size from currentCoordinates because it might be different
+ // (eg if padding is applied)
+ val x2 = x1 + size.width
+ val y2 = y1 + size.height
+
+ return positionInRoot.x in x1..x2 && positionInRoot.y in y1..y2
}
private fun <T : TraversableNode> T.traverseSelfAndDescendants(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 02f83bf..3dbaa83 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -73,33 +73,42 @@
eachPin@ for (i in pointerInputNodes.indices) {
val pointerInputNode = pointerInputNodes[i]
- if (merging) {
- val node = parent.children.firstOrNull { it.modifierNode == pointerInputNode }
-
- if (node != null) {
- node.markIsIn()
- node.pointerIds.add(pointerId)
-
- val mutableObjectList =
- hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
-
- mutableObjectList.add(node)
- parent = node
- continue@eachPin
- } else {
- merging = false
+ // Doesn't add nodes that aren't attached
+ if (pointerInputNode.isAttached) {
+ pointerInputNode.detachedListener = {
+ removePointerInputModifierNode(pointerInputNode)
}
+
+ if (merging) {
+ val node = parent.children.firstOrNull { it.modifierNode == pointerInputNode }
+
+ if (node != null) {
+ node.markIsIn()
+ node.pointerIds.add(pointerId)
+
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) {
+ mutableObjectListOf()
+ }
+
+ mutableObjectList.add(node)
+ parent = node
+ continue@eachPin
+ } else {
+ merging = false
+ }
+ }
+ // TODO(lmr): i wonder if Node here and PointerInputNode ought to be the same thing?
+ val node = Node(pointerInputNode).apply { pointerIds.add(pointerId) }
+
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+ mutableObjectList.add(node)
+
+ parent.children.add(node)
+ parent = node
}
- // TODO(lmr): i wonder if Node here and PointerInputNode ought to be the same thing?
- val node = Node(pointerInputNode).apply { pointerIds.add(pointerId) }
-
- val mutableObjectList =
- hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
-
- mutableObjectList.add(node)
-
- parent.children.add(node)
- parent = node
}
if (prunePointerIdsAndChangesNotInNodesList) {
@@ -109,6 +118,10 @@
}
}
+ private fun removePointerInputModifierNode(pointerInputNode: Modifier.Node) {
+ root.removePointerInputModifierNode(pointerInputNode)
+ }
+
// Removes pointers/changes that are not in the latest hit test
private fun removeInvalidPointerIdsAndChanges(
pointerId: Long,
@@ -163,14 +176,6 @@
root.dispatchCancel()
clearPreviouslyHitModifierNodeCache()
}
-
- /** Removes detached Pointer Input Modifier Nodes. */
- // TODO(shepshapard): Ideally, we can process the detaching of PointerInputFilters at the time
- // that either their associated LayoutNode is removed from the three, or their
- // associated PointerInputModifier is removed from a LayoutNode.
- fun removeDetachedPointerInputNodes() {
- root.removeDetachedPointerInputModifierNodes()
- }
}
/**
@@ -183,6 +188,9 @@
internal open class NodeParent {
val children: MutableVector<Node> = mutableVectorOf()
+ // Supports removePointerInputModifierNode() function
+ private val removeMatchingPointerInputModifierNodeList = MutableObjectList<NodeParent>(10)
+
open fun buildCache(
changes: LongSparseArray<PointerInputChange>,
parentCoordinates: LayoutCoordinates,
@@ -249,6 +257,35 @@
children.forEach { it.dispatchCancel() }
}
+ open fun removePointerInputModifierNode(pointerInputModifierNode: Modifier.Node) {
+ removeMatchingPointerInputModifierNodeList.clear()
+
+ // adds root first
+ removeMatchingPointerInputModifierNodeList.add(this)
+
+ while (removeMatchingPointerInputModifierNodeList.isNotEmpty()) {
+ val parent =
+ removeMatchingPointerInputModifierNodeList.removeAt(
+ removeMatchingPointerInputModifierNodeList.size - 1
+ )
+
+ var index = 0
+ while (index < parent.children.size) {
+ val child = parent.children[index]
+
+ if (child.modifierNode == pointerInputModifierNode) {
+ parent.children.remove(child)
+ child.dispatchCancel()
+ // TODO(JJW): Break here if we change tree structure so same node can't be in
+ // multiple locations (they can be now).
+ } else {
+ removeMatchingPointerInputModifierNodeList.add(child)
+ index++
+ }
+ }
+ }
+ }
+
/** Removes all child nodes. */
fun clear() {
children.clear()
@@ -261,22 +298,6 @@
children.forEach { it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes) }
}
- /** Removes all child [Node]s that are no longer attached to the compose tree. */
- fun removeDetachedPointerInputModifierNodes() {
- var index = 0
- while (index < children.size) {
- val child = children[index]
-
- if (!child.modifierNode.isAttached) {
- child.dispatchCancel()
- children.removeAt(index)
- } else {
- index++
- child.removeDetachedPointerInputModifierNodes()
- }
- }
- }
-
open fun cleanUpHits(internalPointerEvent: InternalPointerEvent) {
for (i in children.lastIndex downTo 0) {
val child = children[i]
@@ -610,7 +631,6 @@
}
override fun toString(): String {
- return "Node(pointerInputFilter=$modifierNode, children=$children, " +
- "pointerIds=$pointerIds)"
+ return "Node(modifierNode=$modifierNode, children=$children, " + "pointerIds=$pointerIds)"
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 7ee0aad..49aa673 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -111,8 +111,6 @@
}
}
- hitPathTracker.removeDetachedPointerInputNodes()
-
// Dispatch to PointerInputFilters
val dispatchedToSomething =
hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -365,7 +365,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -399,7 +399,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
* Alignment lines that can be used by parents to align this layout. This only includes the
* alignment lines of this layout and not children.
*/
- val alignmentLines: Map<AlignmentLine, Int>
+ val alignmentLines: Map<out AlignmentLine, Int>
/**
* An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = layout(width, height, alignmentLines, null, placementBlock)
@@ -69,7 +69,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -923,7 +923,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index ffc88f5..f967cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -99,48 +99,33 @@
return null
}
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
- // TODO(lmr): we might want to add some safety wheels to prevent this from being called
- // while one of the chains is being diffed / updated.
- checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
- var node: Modifier.Node? = node.child
- var layout: LayoutNode? = requireLayoutNode()
- // we use this bespoke data structure here specifically for traversing children. In the
- // depth first traversal you would typically do a `stack.addAll(node.children)` type
- // call, but to avoid enumerating the vector and moving into our stack, we simply keep
- // a stack of vectors and keep track of where we are in each
- val nodes = NestedVectorStack<LayoutNode>()
- while (layout != null) {
- // NOTE: the ?: is important here for the starting condition, since we are starting
- // at THIS node, and not the head of this node chain.
- node = node ?: layout.nodes.head
- if (node.aggregateChildKindSet and mask != 0) {
- while (node != null) {
- if (node.kindSet and mask != 0) {
- block(node)
- }
- node = node.child
- }
- }
- node = null
- nodes.push(layout._children)
- layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+ if (zOrder) {
+ zSortedChildren
+ } else {
+ _children
}
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+ node: Modifier.Node,
+ zOrder: Boolean,
+) {
+ node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
}
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
- node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Unit
+) {
check(node.isAttached) { "visitChildren called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.lastIndex)
if (branch.aggregateChildKindSet and mask == 0) {
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
// none of these nodes match the mask, so don't bother traversing them
continue
}
@@ -159,11 +144,15 @@
* visit the shallow tree of children of a given mask, but if block returns true, we will continue
* traversing below it
*/
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Boolean
+) {
checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
outer@ while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.size - 1)
if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
node = node.child
}
}
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
}
}
@@ -264,33 +253,41 @@
return null
}
-internal inline fun <reified T> DelegatableNode.visitSubtree(
- type: NodeKind<T>,
- block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
internal inline fun <reified T> DelegatableNode.visitChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
) {
node.dispatchForKind(type, block)
- visitChildren(type.mask) { it.dispatchForKind(type, block) }
+ visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
}
internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Boolean
) =
- visitSubtreeIf(type.mask) foo@{ node ->
+ visitSubtreeIf(type.mask, zOrder) foo@{ node ->
node.dispatchForKind(type) { if (!block(it)) return@foo false }
true
}
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+ type: NodeKind<T>,
+ zOrder: Boolean = false,
+ block: (T) -> Unit
+) =
+ visitSubtreeIf(type.mask, zOrder) {
+ it.dispatchForKind(type, block)
+ true
+ }
+
internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
node.aggregateChildKindSet and type.mask != 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 86be6a2..ebb06c8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -160,8 +160,6 @@
// our position in order ot know how to offset the value we provided).
if (isShallowPlacing) return
- onPlaced()
-
layoutNode.measurePassDelegate.onNodePlaced()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 92695e5..4229de0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -59,6 +59,8 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.viewinterop.InteropView
import androidx.compose.ui.viewinterop.InteropViewFactoryHolder
@@ -91,6 +93,11 @@
InteroperableComposeUiNode,
Owner.OnLayoutCompletedListener {
+ internal var offsetFromRoot: IntOffset = IntOffset.Max
+ internal var lastSize: IntSize = IntSize.Zero
+ internal var outerToInnerOffset: IntOffset = IntOffset.Max
+ internal var outerToInnerOffsetDirty: Boolean = true
+
var forceUseOldLayers: Boolean = false
override var compositeKeyHash: Int = 0
@@ -814,7 +821,7 @@
/** The inner-most layer coordinator. Used for performance for NodeCoordinator.findLayer(). */
private var _innerLayerCoordinator: NodeCoordinator? = null
internal var innerLayerCoordinatorIsDirty = true
- private val innerLayerCoordinator: NodeCoordinator?
+ internal val innerLayerCoordinator: NodeCoordinator?
get() {
if (innerLayerCoordinatorIsDirty) {
var coordinator: NodeCoordinator? = innerCoordinator
@@ -1056,6 +1063,7 @@
* measurement need to be re-done. Such events include modifier change, attach/detach, etc.
*/
internal fun invalidateMeasurements() {
+ outerToInnerOffsetDirty = true
if (lookaheadRoot != null) {
requestLookaheadRemeasure()
} else {
@@ -1077,6 +1085,7 @@
/** Used to request a new layout pass from the owner. */
internal fun requestRelayout(forceRequest: Boolean = false) {
+ outerToInnerOffsetDirty = true
if (!isVirtual) {
owner?.onRequestRelayout(this, forceRequest = forceRequest)
}
@@ -1325,6 +1334,7 @@
if (isAttached) {
invalidateSemantics()
}
+ owner?.onLayoutNodeDeactivated(this)
}
override fun onRelease() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
/** The alignment lines of this layout, inherited + intrinsic */
private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
- fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+ fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
- protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
@@ -201,7 +201,7 @@
internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = lookaheadDelegate!!.measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 3df4e68..00bc77c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -509,6 +509,8 @@
isPlaced = true
with(layoutNode) {
if (!wasPlaced) {
+ innerCoordinator.onPlaced()
+
// if the node was not placed previous remeasure request could have been ignored
if (measurePending) {
requestRemeasure(forceRequest = true)
@@ -597,6 +599,10 @@
// visible, so we need to relayout the parent to get the `placeOrder`.
parent?.requestRelayout()
}
+ } else {
+ // Call onPlaced callback on each placement, even if it was already placed,
+ // but without subtree invalidation.
+ layoutNode.innerCoordinator.onPlaced()
}
if (parent != null) {
@@ -841,6 +847,7 @@
}
layoutState = LayoutState.LayingOut
+ val firstPlacement = !placedOnce
lastPosition = position
lastZIndex = zIndex
lastLayerBlock = layerBlock
@@ -849,6 +856,7 @@
onNodePlacedCalled = false
val owner = layoutNode.requireOwner()
+ owner.rectManager.onLayoutPositionChanged(layoutNode, position, firstPlacement)
if (!layoutPending && isPlaced) {
outerCoordinator.placeSelfApparentToRealOffset(position, zIndex, layerBlock, layer)
onNodePlaced()
@@ -950,7 +958,7 @@
return true
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
// Mark alignments used by modifier
if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1286,7 @@
}
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
if (layoutState == LayoutState.LookaheadMeasuring) {
// Mark alignments used by modifier
@@ -1894,7 +1902,7 @@
fun layoutChildren()
/** Recalculate the alignment lines if dirty, and layout children as needed. */
- fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+ fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
/**
* Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index f5e4439..8bf8d59 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -203,7 +203,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: PlacementScope.() -> Unit
): MeasureResult {
@@ -215,7 +215,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
- // number of vectors in the stack
- private var size = 0
- // holds the current "top" index for each vector
- private var currentIndexes = IntArray(16)
- private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
- fun isNotEmpty(): Boolean {
- return size > 0 && currentIndexes[size - 1] >= 0
- }
-
- fun pop(): T {
- check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
- val indexOfVector = size - 1
- val indexOfItem = currentIndexes[indexOfVector]
- val vector = vectors[indexOfVector]!!
- if (indexOfItem > 0) currentIndexes[indexOfVector]--
- else if (indexOfItem == 0) {
- vectors[indexOfVector] = null
- size--
- }
- return vector[indexOfItem]
- }
-
- fun push(vector: MutableVector<T>) {
- // if the vector is empty there is no reason for us to add it
- if (vector.isEmpty()) return
- val nextIndex = size
- // check to see that we have capacity to add another vector
- if (nextIndex >= currentIndexes.size) {
- currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
- vectors = vectors.copyOf(vectors.size * 2)
- }
- currentIndexes[nextIndex] = vector.size - 1
- vectors[nextIndex] = vector
- size++
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index b522a01..23b9014 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -521,7 +521,13 @@
layoutNode.innerLayerCoordinatorIsDirty = true
invalidateParentLayer()
} else if (updateParameters) {
- updateLayerParameters()
+ val positionalPropertiesChanged = updateLayerParameters()
+ if (positionalPropertiesChanged) {
+ layoutNode
+ .requireOwner()
+ .rectManager
+ .onLayoutLayerPositionalPropertiesChanged(layoutNode)
+ }
}
} else {
this.layerBlock = null
@@ -1340,7 +1346,9 @@
layoutDelegate.measurePassDelegate
.notifyChildrenUsingCoordinatesWhilePlacing()
}
- layoutNode.owner?.requestOnPositionedCallback(layoutNode)
+ val owner = layoutNode.requireOwner()
+ owner.rectManager.onLayoutLayerPositionalPropertiesChanged(layoutNode)
+ owner.requestOnPositionedCallback(layoutNode)
}
}
}
@@ -1407,7 +1415,7 @@
@Suppress("PrimitiveInCollection")
private fun compareEquals(
a: MutableObjectIntMap<AlignmentLine>?,
- b: Map<AlignmentLine, Int>
+ b: Map<out AlignmentLine, Int>
): Boolean {
if (a == null) return false
if (a.size != b.size) return false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index 6090fdb..926bc9f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -91,6 +91,9 @@
*/
fun transform(matrix: Matrix)
+ /** The matrix associated with the affine transform of this layer */
+ val underlyingMatrix: Matrix
+
/**
* Calculates the transform from the layer to the parent and multiplies [matrix] by the
* transform.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 30d11e0..b2d7cec5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -46,6 +46,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
@@ -133,6 +134,9 @@
/** Provide information about the window that hosts this [Owner]. */
val windowInfo: WindowInfo
+ /** Provides a queryable and observable index of nodes' bounding rectangles */
+ val rectManager: RectManager
+
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
@@ -258,6 +262,8 @@
/** The position and/or size of the [layoutNode] changed. */
fun onLayoutChange(layoutNode: LayoutNode)
+ fun onLayoutNodeDeactivated(layoutNode: LayoutNode)
+
/**
* The position and/or size of an interop view (typically, an android.view.View) has changed. On
* Android, this schedules view tree layout observer callback to be invoked for the underlying
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
new file mode 100644
index 0000000..1105c4f
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectList.kt
@@ -0,0 +1,819 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.compose.ui.spatial
+
+import kotlin.jvm.JvmField
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This is a fairly straight-forward data structure. It stores an Int value and a corresponding
+ * rectangle, and allows for efficient querying based on the spatial relationships between the
+ * objects contained in it, but it does so just by storing all of the information packed into a
+ * single LongArray. Because of the simplicity and tight loops / locality of information, this ends
+ * up being faster than most other data structures in most things for the size data sets that we
+ * will be using this for. For O(10**2) items, this outperformas other data structures. Each
+ * meta/rect pair is stored contiguously as 3 Longs in an LongArray. This makes insert and update
+ * extremely cheap. Query operations require scanning the entire array, but due to cache locality
+ * and fairly efficient math, it is competitive with data structures which use mechanisms to prune
+ * the size of the data set to query less.
+ *
+ * This data structure comes with some assumptions:
+ * 1. the "identifier" values for this data structure are positive Ints. For performance reasons, we
+ * only store 26 bits of precision here, so practically speaking the item id is limited to 26
+ * bits (~67,000,000).
+ * 2. The coordinate system used for this data structure has the positive "x" axis pointing to the
+ * "right", and the positive "y" axis pointing "down". As a result, a rectangle will always have
+ * top <= bottom, and left <= right.
+ */
+@Suppress("NAME_SHADOWING")
+internal class RectList {
+ /**
+ * This is the primary data storage. We store items linearly, with each "item" taking up three
+ * longs (192 bits) of space. The partitioning generally looks like:
+ *
+ * Long 1 (64 bits): the "top left" long
+ * 32 bits: left
+ * 32 bits: top
+ * Long 2 (64 bits): the "bottom right" long
+ * 32 bits: right
+ * 32 bits: bottom
+ * Long 3 (64 bits): the "meta" long
+ * 26 bits: item id
+ * 26 bits: parent id
+ * 10 bits: last child offset
+ * 1 bits: focusable
+ * 1 bits: gesturable
+ */
+ @JvmField internal var items: LongArray = LongArray(LongsPerItem * 64) // 64 items
+
+ /**
+ * We allocate a 2nd LongArray. This is always going to be sized identical to [items], and
+ * during [defragment] we will swap the two in order to have a cheap defragment algorithm that
+ * preserves order.
+ *
+ * Additionally, this "double buffering" ends up having a side benefit where we can use this
+ * array during [updateSubhierarchy] as a local stack which will never have to grow since it
+ * cannot exceed the size of the items array itself. This allows for RectList to have as few
+ * allocations as possible, however this does double the memory footprint.
+ *
+ * @see [defragment]
+ * @see [updateSubhierarchy]
+ */
+ @JvmField internal var stack: LongArray = LongArray(LongsPerItem * 64) // 64 items
+
+ /**
+ * The size of the items array that is filled with actual data. This is different from
+ * `items.size` since the array is larger than the data it contains so that inserts can be
+ * cheap.
+ */
+ @JvmField internal var itemsSize: Int = 0
+
+ /** The number of items */
+ val size: Int
+ get() = itemsSize / LongsPerItem
+
+ /**
+ * Returns the 0th index of which 3 contiguous Longs can be stored in the items array. If space
+ * is available at the end of the array, it will use that. If not, this will grow the items
+ * array.This method will return an Int index that you can use, BUT, this method has side
+ * effects and may mutate the [items] and [itemsSize] fields on this class. It is important to
+ * keep this in mind if you call this method and have cached any of those values in a local
+ * variable, you may need to refresh them.
+ */
+ internal fun allocateItemsIndex(): Int {
+ val currentItems = items
+ val currentSize = itemsSize
+ itemsSize = currentSize + LongsPerItem
+ val actualSize = currentItems.size
+ if (actualSize <= currentSize + LongsPerItem) {
+ val newSize = max(actualSize * 2, currentSize + LongsPerItem)
+ items = currentItems.copyOf(newSize)
+ stack = stack.copyOf(newSize)
+ }
+ return currentSize
+ }
+
+ /**
+ * Insert a value and corresponding bounding rectangle into the RectList. This method does not
+ * check to see that [value] doesn't already exist somewhere in the list.
+ *
+ * NOTE: -1 is NOT a valid value for this collection since it is used as a tombstone value.
+ *
+ * @param value The value to be stored. Intended to be a layout node id. Must be a positive
+ * integer of 28 bits or less
+ * @param l the left coordinate of the rectangle
+ * @param t the top coordinate of the rectangle
+ * @param r the right coordinate of the rectangle
+ * @param b the bottom coordinate of the rectangle
+ * @param parentId If this element is inside of a "scrollable" container which we want to update
+ * with the [updateSubhierarchy] API, then this is the id of that scroll container.
+ * @param focusable true if this element is focusable. This is a flag which we can use to limit
+ * the results of certain queries for
+ * @param gesturable true if this element is a pointer input gesture detector. This is a flag
+ * which we can use to limit the results of certain queries for
+ */
+ fun insert(
+ value: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ parentId: Int = -1,
+ focusable: Boolean = false,
+ gesturable: Boolean = false,
+ ) {
+ val value = value and Lower26Bits
+ val index = allocateItemsIndex()
+ val items = items
+
+ items[index + 0] = packXY(l, t)
+ items[index + 1] = packXY(r, b)
+ items[index + 2] = packMeta(value, parentId, lastChildOffset = 0, focusable, gesturable)
+
+ if (parentId < 0) return
+ val parentId = parentId and Lower26Bits
+ // After inserting, find the item with id = parentId and update it's "last child offset".
+ var i = index - LongsPerItem
+ while (i > 0) {
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == parentId) {
+ // TODO: right now this number will always be a multiple of 3. Since the last child
+ // offset only has 10 bits of precision, we probably want to encode this more
+ // efficiently. It doesn't have to be exact, it just can't be too small. We could
+ // obviously divide by LongsPerItem, but we may also want to do something cheaper
+ // like dividing by 2 or 4
+ val lastChildOffset = index - i
+ items[i + 2] = metaWithLastChildOffset(meta, lastChildOffset)
+ return
+ }
+ i -= LongsPerItem
+ }
+ }
+
+ /**
+ * Remove a value from this collection.
+ *
+ * @return Whether or not a value was found and removed from this list successfully.
+ * @see defragment
+ */
+ fun remove(value: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ // NOTE: We are assuming that the value can only be here once.
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == value) {
+ // To "remove" an item, we make the rectangle [max, max, max, max] so that it won't
+ // match any queries, and we mark meta as tombStone so we can detect it later
+ // in the defragment method
+ items[i + 0] = 0xffff_ffff_ffff_ffffUL.toLong()
+ items[i + 1] = 0xffff_ffff_ffff_ffffUL.toLong()
+ items[i + 2] = TombStone
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ /**
+ * Updates the rectangle associated with this value.
+ *
+ * @return true if the value was found and updated, false if this value is not currently in the
+ * collection
+ */
+ fun update(value: Int, l: Int, t: Int, r: Int, b: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ items[i + 0] = packXY(l, t)
+ items[i + 1] = packXY(r, b)
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ /**
+ * Moves the rectangle associated with this value to the specified rectangle, and updates every
+ * item that is "below" the specified rectangle by the associated offset. move() is generally
+ * more efficient than calling update() for all of the rectangles included in the subhierarchy
+ * of the item.
+ */
+ fun move(value: Int, l: Int, t: Int, r: Int, b: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ val prevLT = items[i + 0]
+ items[i + 0] = packXY(l, t)
+ items[i + 1] = packXY(r, b)
+ val deltaX = l - unpackX(prevLT)
+ val deltaY = t - unpackY(prevLT)
+ if ((deltaX != 0) or (deltaY != 0)) {
+ updateSubhierarchy(metaWithParentId(meta, i + LongsPerItem), deltaX, deltaY)
+ }
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ fun updateSubhierarchy(id: Int, deltaX: Int, deltaY: Int) {
+ updateSubhierarchy(
+ //
+ stackMeta =
+ packMeta(
+ itemId = id,
+ parentId = 0,
+ lastChildOffset = items.size,
+ focusable = false,
+ gesturable = false,
+ ),
+ deltaX = deltaX,
+ deltaY = deltaY
+ )
+ }
+
+ /**
+ * Updates a subhierarchy of items by the specified delta. For efficiency, the [stackMeta]
+ * provided is a Long encoded with the same scheme of the "meta" long of each item, where the
+ * encoding has the following semantic specific to this method:
+ *
+ * Long (64 bits): the "stack meta" encoding
+ * 26 bits: the "parent id" that we are matching on (normally item id)
+ * 26 bits: the minimum index that a child can have (normally parent id)
+ * 10 bits: max offset from start index a child can have (normally last child offset)
+ * 1 bits: unused (normally focusable)
+ * 1 bits: unused (normally gesturable)
+ *
+ * We use this essentially as a way to encode three integers into a long, which includes all of
+ * the data needed to efficiently iterate through the below algorithm. It is effectively an id
+ * and a range. The range isn't strictly needed, but it helps turn this O(n^2) algorithm into
+ * something that is ~O(n) in the average case (still O(n^2) worst case though). By using the
+ * same encoding as "meta" longs, we only need to update the start index when we
+ */
+ private fun updateSubhierarchy(stackMeta: Long, deltaX: Int, deltaY: Int) {
+ val items = items
+ val stack = stack
+ val size = size
+ stack[0] = stackMeta
+ var stackSize = 1
+ while (stackSize > 0) {
+ val idAndStartAndOffset = stack[--stackSize]
+ val parentId = unpackMetaValue(idAndStartAndOffset) // parent id is in the id slot
+ var i = unpackMetaParentId(idAndStartAndOffset) // start index is in the parent id slot
+ val offset = unpackMetaLastChildOffset(idAndStartAndOffset)
+ val endIndex = if (offset == Lower10Bits) size else offset + i
+ if (i < 0) break
+ while (i < items.size - 2) {
+ if (i >= endIndex) break
+ val meta = items[i + 2]
+ if (unpackMetaParentId(meta) == parentId) {
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ items[i + 0] = packXY(unpackX(topLeft) + deltaX, unpackY(topLeft) + deltaY)
+ items[i + 1] =
+ packXY(unpackX(bottomRight) + deltaX, unpackY(bottomRight) + deltaY)
+ if (unpackMetaLastChildOffset(meta) > 0) {
+ // we need to store itemId, lastChildOffset, and a "start index".
+ // For convenience, we just use `meta` which already encodes two of those
+ // values, and we add `i` into the slot for "parentId"
+ stack[stackSize++] = metaWithParentId(meta, i + LongsPerItem)
+ }
+ }
+ i += LongsPerItem
+ }
+ }
+ }
+
+ fun withRect(value: Int, block: (Int, Int, Int, Int) -> Unit): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ block(
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ operator fun contains(value: Int): Boolean {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ if (unpackMetaValue(meta) == value) {
+ return true
+ }
+ i += LongsPerItem
+ }
+ return false
+ }
+
+ fun metaFor(value: Int): Long {
+ val value = value and Lower26Bits
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val meta = items[i + 2]
+ // NOTE: We are assuming that the value can only be here once.
+ if (unpackMetaValue(meta) == value) {
+ return meta
+ }
+ i += LongsPerItem
+ }
+ return TombStone
+ }
+
+ /**
+ * For a provided rectangle, executes [block] for each value in the collection whose associated
+ * rectangle intersects the provided one. The argument passed into [block] will be the value.
+ */
+ inline fun forEachIntersection(
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ block: (Int) -> Unit,
+ ) {
+ val destTopLeft = packXY(l, t)
+ val destTopRight = packXY(r, b)
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ if (rectIntersectsRect(topLeft, bottomRight, destTopLeft, destTopRight)) {
+ // TODO: it might make sense to include the rectangle in the block since calling
+ // code may want to filter this list using that geometry, and it would be
+ // beneficial to not have to look up the layout node in order to do so.
+ block(unpackMetaValue(items[i + 2]))
+ }
+ i += LongsPerItem
+ }
+ }
+
+ inline fun forEachRect(
+ block: (Int, Int, Int, Int, Int) -> Unit,
+ ) {
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val meta = items[i + 2]
+ block(
+ unpackMetaValue(meta),
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ i += LongsPerItem
+ }
+ }
+
+ // TODO: add ability to filter to just gesture detectors (the main use case for this function)
+ /**
+ * For a provided point, executes [block] for each value in the collection whose associated
+ * rectangle contains the provided point. The argument passed into [block] will be the value.
+ */
+ inline fun forEachIntersection(
+ x: Int,
+ y: Int,
+ block: (Int) -> Unit,
+ ) {
+ val destXY = packXY(x, y)
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ if (rectIntersectsRect(topLeft, bottomRight, destXY, destXY)) {
+ val meta = items[i + 2]
+ block(unpackMetaValue(meta))
+ }
+ i += LongsPerItem
+ }
+ }
+
+ internal fun neighborsScoredByDistance(
+ searchAxis: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ ): IntArray {
+ val items = items
+ val size = itemsSize / LongsPerItem
+ var i = 0
+ // build up an array of size N with each element being the score for the item at that index
+ val results = IntArray(size)
+
+ while (i < results.size) {
+ val itemsIndex = i * LongsPerItem
+ if (itemsIndex < 0 || itemsIndex >= items.size - 1) break
+ val topLeft = items[itemsIndex + 0]
+ val bottomRight = items[itemsIndex + 1]
+ val score =
+ distanceScore(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ results[i] = score
+ i++
+ }
+ return results
+ }
+
+ // TODO: add ability to filter to just focusable (the main use case for this function)
+ // TODO: add an overload which just takes in searchAxis, k, and item id
+ inline fun findKNearestNeighbors(
+ searchAxis: Int,
+ k: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ block: (score: Int, id: Int, l: Int, t: Int, r: Int, b: Int) -> Unit,
+ ) {
+ // this list is 1:1 with items and holds the score for each item
+ val list =
+ neighborsScoredByDistance(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ )
+ val items = items
+
+ var sent = 0
+ var min = 1
+ var nextMin = Int.MAX_VALUE
+ var loops = 0
+ var i = 0
+ while (loops <= k) {
+ while (i < list.size) {
+ val score = list[i]
+ // update nextmin if score is smaller than nextMin but larger than min
+ if (score > min) {
+ nextMin = min(nextMin, score)
+ }
+ if (score == min) {
+ val itemIndex = i * LongsPerItem
+ val topLeft = items[itemIndex + 0]
+ val bottomRight = items[itemIndex + 1]
+ val meta = items[itemIndex + 2]
+ block(
+ score,
+ unpackMetaValue(meta),
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ sent++
+ if (sent == k) return
+ }
+ i++
+ }
+ min = nextMin
+ nextMin = Int.MAX_VALUE
+ loops++
+ i = 0
+ }
+ }
+
+ inline fun findNearestNeighbor(searchAxis: Int, l: Int, t: Int, r: Int, b: Int): Int {
+ val items = items
+ val size = itemsSize
+ var minScore = Int.MAX_VALUE
+ var minIndex = -1
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val score =
+ distanceScore(
+ searchAxis,
+ l,
+ t,
+ r,
+ b,
+ unpackX(topLeft),
+ unpackY(topLeft),
+ unpackX(bottomRight),
+ unpackY(bottomRight),
+ )
+ val isNewMin = (score > 0) and (score < minScore)
+ minScore = if (isNewMin) score else minScore
+ minIndex = if (isNewMin) i + 1 else minIndex
+ i += LongsPerItem
+ }
+ return if (minIndex < 0 || minIndex >= items.size) {
+ -1
+ } else {
+ unpackMetaValue(items[minIndex])
+ }
+ }
+
+ /** */
+ fun defragment() {
+ val from = items
+ val size = itemsSize
+ val to = stack
+ var i = 0
+ var j = 0
+ while (i < from.size - 2) {
+ if (j >= to.size - 2) break
+ if (i >= size) break
+ if (from[i + 2] != TombStone) {
+ to[j + 0] = from[i + 0]
+ to[j + 1] = from[i + 1]
+ to[j + 2] = from[i + 2]
+ j += LongsPerItem
+ }
+ i += LongsPerItem
+ }
+ itemsSize = j
+ // NOTE: this could be a reasonable time to shrink items/stack to a smaller array if for
+ // some reason they have gotten very large. I'm choosing NOT to do this because I think
+ // if the arrays have gotten to a large size it is very likely that they will get to that
+ // size again, and avoiding the thrash here is probably desirable
+ items = to
+ stack = from
+ }
+
+ fun debugString(): String = buildString {
+ val items = items
+ val size = itemsSize
+ var i = 0
+ while (i < items.size - 2) {
+ if (i >= size) break
+ val topLeft = items[i + 0]
+ val bottomRight = items[i + 1]
+ val meta = items[i + 2]
+ val id = unpackMetaValue(meta)
+ val parentId = unpackMetaParentId(meta)
+ val l = unpackX(topLeft)
+ val t = unpackY(topLeft)
+ val r = unpackX(bottomRight)
+ val b = unpackY(bottomRight)
+ appendLine("id=$id, rect=[$l,$t,$r,$b], parent=$parentId")
+ i += LongsPerItem
+ }
+ }
+}
+
+internal const val LongsPerItem = 3
+internal const val Lower26Bits = 0b0000_0011_1111_1111_1111_1111_1111_1111
+internal const val Lower10Bits = 0b0000_0000_0000_0000_0000_0011_1111_1111
+internal const val EverythingButParentId = 0xfff0_0000_03ff_ffffUL
+internal const val EverythingButLastChildOffset = 0xc00f_ffff_ffff_ffffUL
+
+/**
+ * This is the "meta" value that we assign to every removed value.
+ *
+ * @see RectList.remove
+ * @see packMeta
+ */
+internal const val TombStone = 0x3fff_ffff_ffff_ffffL // packMeta(-1, -1, -1, false, false)
+
+internal const val AxisNorth: Int = 0
+internal const val AxisSouth: Int = 1
+internal const val AxisWest: Int = 2
+internal const val AxisEast: Int = 3
+
+internal inline fun packXY(x: Int, y: Int) = (x.toLong() shl 32) or (y.toLong() and 0xffff_ffff)
+
+internal inline fun packMeta(
+ itemId: Int,
+ parentId: Int,
+ lastChildOffset: Int,
+ focusable: Boolean,
+ gesturable: Boolean,
+): Long =
+ // 26 bits: item id
+ // 26 bits: parent id
+ // 10 bits: last child offset
+ // 1 bits: focusable
+ // 1 bits: gesturable
+ (gesturable.toLong() shl 63) or
+ (focusable.toLong() shl 62) or
+ ((lastChildOffset and Lower10Bits).toLong() shl 52) or
+ ((parentId and Lower26Bits).toLong() shl 26) or
+ ((itemId and Lower26Bits).toLong() shl 0)
+
+internal inline fun unpackMetaValue(meta: Long): Int = meta.toInt() and Lower26Bits
+
+internal inline fun unpackMetaParentId(meta: Long): Int = (meta shr 26).toInt() and Lower26Bits
+
+internal inline fun unpackMetaLastChildOffset(meta: Long): Int =
+ (meta shr 52).toInt() and Lower10Bits
+
+internal inline fun metaWithParentId(meta: Long, parentId: Int): Long =
+ (meta and EverythingButParentId.toLong()) or ((parentId and Lower26Bits).toLong() shl 26)
+
+internal inline fun metaWithLastChildOffset(meta: Long, lastChildOffset: Int): Long =
+ (meta and EverythingButLastChildOffset.toLong()) or
+ ((lastChildOffset and Lower10Bits).toLong() shl 52)
+
+internal inline fun unpackMetaFocusable(meta: Long): Int = (meta shr 62).toInt() and 0b1
+
+internal inline fun unpackMetaGesturable(meta: Long): Int = (meta shr 63).toInt() and 0b1
+
+internal inline fun unpackX(xy: Long): Int = (xy shr 32).toInt()
+
+internal inline fun unpackY(xy: Long): Int = (xy).toInt()
+
+/** */
+internal inline fun rectIntersectsRect(
+ srcLT: Long,
+ srcRB: Long,
+ destLT: Long,
+ destRB: Long
+): Boolean {
+ // destRB - srcLT = [r2 - l1, b2 - t1]
+ // srcRB - destLT = [r1 - l2, b1 - t2]
+
+ // Both of the above expressions represent two long subtractions which are effectively each two
+ // int subtractions. If any of the individual subtractions would have resulted in a negative
+ // value, then the rectangle has an intersection. If this is true, then there will be
+ // "underflow" from one 32bit component to the next, which we can detect by isolating the top
+ // bits of each component using 0x8000_0000_8000_0000UL.toLong()
+ val a = (destRB - srcLT) or (srcRB - destLT)
+ return a and 0x8000_0000_8000_0000UL.toLong() == 0L
+}
+
+/**
+ * Turns a boolean into a long of 1L/0L for true/false. It is written precisely this way as this
+ * results in a single ARM instruction where as other approaches are more expensive. For example,
+ * `if (this) 1L else 0L` is several instructions instead of just one. DO NOT change this without
+ * looking at the corresponding arm code and verifying that it is better.
+ */
+internal inline fun Boolean.toLong(): Long = (if (this) 1 else 0).toLong()
+
+/**
+ * This function will return a "score" of a rectangle relative to a query. A negative score means
+ * that the rectangle should be ignored, and a lower (but non-negative) score means that the
+ * rectangle is close and overlapping in the direction of the axis in question.
+ *
+ * @param axis the direction/axis along which we are scoring
+ * @param queryL the left of the rect we are finding the nearest neighbors of
+ * @param queryT the top of the rect we are finding the nearest neighbors of
+ * @param queryR the right of the rect we are finding the nearest neighbors of
+ * @param queryB the bottom of the rect we are finding the nearest neighbors of
+ * @param l the left of the rect which is the "neighbor" we are scoring
+ * @param t the top of the rect which is the "neighbor" we are scoring
+ * @param r the right of the rect which is the "neighbor" we are scoring
+ * @param b the bottom of the rect which is the "neighbor" we are scoring
+ * @see AxisNorth
+ * @see AxisWest
+ * @see AxisEast
+ * @see AxisSouth
+ */
+// TODO: consider just passing in TopLeft/BottomRight longs in order to reduce the number of
+// parameters here.
+internal fun distanceScore(
+ axis: Int,
+ queryL: Int,
+ queryT: Int,
+ queryR: Int,
+ queryB: Int,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+): Int {
+ return when (axis) {
+ AxisNorth ->
+ distanceScoreAlongAxis(
+ distanceMin = queryT,
+ distanceMax = b,
+ queryCrossAxisMax = queryR,
+ queryCrossAxisMin = queryL,
+ crossAxisMax = r,
+ crossAxisMin = l,
+ )
+ AxisEast ->
+ distanceScoreAlongAxis(
+ distanceMin = l,
+ distanceMax = queryR,
+ queryCrossAxisMax = queryB,
+ queryCrossAxisMin = queryT,
+ crossAxisMax = b,
+ crossAxisMin = t,
+ )
+ AxisSouth ->
+ distanceScoreAlongAxis(
+ distanceMin = t,
+ distanceMax = queryB,
+ queryCrossAxisMax = queryR,
+ queryCrossAxisMin = queryL,
+ crossAxisMax = r,
+ crossAxisMin = l,
+ )
+ AxisWest ->
+ distanceScoreAlongAxis(
+ distanceMin = queryL,
+ distanceMax = r,
+ queryCrossAxisMax = queryB,
+ queryCrossAxisMin = queryT,
+ crossAxisMax = b,
+ crossAxisMin = t,
+ )
+ else -> Int.MAX_VALUE
+ }
+}
+
+/**
+ * This function will return a "score" of a rectangle relative to a query. A negative score means
+ * that the rectangle should be ignored, and a low score means that
+ */
+internal fun distanceScoreAlongAxis(
+ distanceMin: Int,
+ distanceMax: Int,
+ queryCrossAxisMax: Int,
+ queryCrossAxisMin: Int,
+ crossAxisMax: Int,
+ crossAxisMin: Int,
+): Int {
+ // small positive means it is close to the right, negative means there is overlap or it is to
+ // the left, which we will reject. We want small and positive.
+ val distanceAlongAxis = distanceMin - distanceMax
+ val maxOverlapPossible = queryCrossAxisMax - queryCrossAxisMin
+ // 0 with full overlap, increasingly large negative numbers without
+ val overlap =
+ maxOverlapPossible + max(queryCrossAxisMin, crossAxisMin) -
+ min(queryCrossAxisMax, crossAxisMax)
+
+ return (distanceAlongAxis + 1) * (overlap + 1)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
new file mode 100644
index 0000000..63751ce
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.spatial
+
+import androidx.collection.mutableObjectListOf
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.MutableRect
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.isIdentity
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.NodeCoordinator
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.plus
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.toOffset
+
+internal class RectManager {
+
+ val rects: RectList = RectList()
+
+ private val callbacks = mutableObjectListOf<() -> Unit>()
+ private var isDirty = false
+ private var isFragmented = false
+
+ fun invalidate() {
+ isDirty = true
+ }
+
+ // TODO: we need to make sure these are dispatched after draw if needed
+ fun dispatchCallbacks() {
+ if (isDirty) {
+ isDirty = false
+ // The hierarchy is "settled" in terms of nodes being added/removed for this frame
+ // This makes it a reasonable time to "defragment" the RectList data structure. This
+ // will keep operations on this data structure efficient over time. This is a fairly
+ // cheap operation to run, so we just do it every time
+ if (isFragmented) {
+ isFragmented = false
+ rects.defragment()
+ }
+ callbacks.forEach { it() }
+ }
+ }
+
+ fun registerOnChangedCallback(callback: () -> Unit): Any? {
+ callbacks.add(callback)
+ return callback
+ }
+
+ fun unregisterOnChangedCallback(token: Any?) {
+ @Suppress("UNCHECKED_CAST")
+ token as? (() -> Unit) ?: return
+ callbacks.remove(token)
+ }
+
+ fun onLayoutLayerPositionalPropertiesChanged(layoutNode: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
+ val outerToInnerOffset = layoutNode.outerToInnerOffset()
+ if (outerToInnerOffset.isSet) {
+ // translational properties only. AARB still valid.
+ layoutNode.outerToInnerOffset = outerToInnerOffset
+ layoutNode.outerToInnerOffsetDirty = false
+ layoutNode.forEachChild {
+ // NOTE: this calls rectlist.move(...) so does not need to be recursive
+ onLayoutPositionChanged(it, it.outerCoordinator.position, false)
+ }
+ } else {
+ // there are rotations/skews/scales going on, so we need to do a more expensive update
+ insertOrUpdateTransformedNodeSubhierarchy(layoutNode)
+ }
+ }
+
+ fun onLayoutPositionChanged(
+ layoutNode: LayoutNode,
+ position: IntOffset,
+ firstPlacement: Boolean
+ ) {
+ @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
+ // Our goal here is to get the right "root" coordinates for every layout. We can use
+ // LayoutCoordinates.localToRoot to calculate this somewhat readily, however this function
+ // is getting called with a very high frequency and so it is important that extracting these
+ // coordinates remains relatively cheap to limit the overhead of this tracking. The
+ // LayoutCoordinates will traverse up the entire "spine" of the hierarchy, so as we do this
+ // calculation for many nodes, we would be making many redundant calculations. In order to
+ // minimize this, we store the "offsetFromRoot" of each layout node as we calculate it, and
+ // attempt to utilize this value when calculating it for a node that is below it.
+ // Additionally, we calculate and cache the parent's "outer to inner offset" which may
+ val delegate = layoutNode.measurePassDelegate
+ val width = delegate.measuredWidth
+ val height = delegate.measuredHeight
+
+ val parent = layoutNode.parent
+ val offset: IntOffset
+
+ val lastOffset = layoutNode.offsetFromRoot
+ val lastSize = layoutNode.lastSize
+ val lastWidth = lastSize.width
+ val lastHeight = lastSize.height
+
+ var hasNonTranslationTransformations = false
+
+ if (parent != null) {
+ val parentOffsetDirty = parent.outerToInnerOffsetDirty
+ val parentOffset = parent.offsetFromRoot
+ val prevOuterToInnerOffset = parent.outerToInnerOffset
+
+ offset =
+ if (parentOffset.isSet) {
+ val parentOuterInnerOffset =
+ if (parentOffsetDirty) {
+ val it = parent.outerToInnerOffset()
+
+ parent.outerToInnerOffset = it
+ parent.outerToInnerOffsetDirty = false
+ it
+ } else {
+ prevOuterToInnerOffset
+ }
+ hasNonTranslationTransformations = !parentOuterInnerOffset.isSet
+ parentOffset + parentOuterInnerOffset + position
+ } else {
+ layoutNode.outerCoordinator.positionInRoot()
+ }
+ } else {
+ // root
+ offset = position
+ }
+
+ // If unset is returned then that means there is a rotation/skew/scale
+ if (hasNonTranslationTransformations || !offset.isSet) {
+ insertOrUpdateTransformedNode(layoutNode, position, firstPlacement)
+ return
+ }
+
+ layoutNode.offsetFromRoot = offset
+ layoutNode.lastSize = IntSize(width, height)
+
+ val l = offset.x
+ val t = offset.y
+ val r = l + width
+ val b = t + height
+
+ if (!firstPlacement && offset == lastOffset && lastWidth == width && lastHeight == height) {
+ return
+ }
+
+ insertOrUpdate(layoutNode, firstPlacement, l, t, r, b)
+ }
+
+ private fun insertOrUpdateTransformedNodeSubhierarchy(layoutNode: LayoutNode) {
+ layoutNode.forEachChild {
+ insertOrUpdateTransformedNode(it, it.outerCoordinator.position, false)
+ insertOrUpdateTransformedNodeSubhierarchy(it)
+ }
+ }
+
+ private val cachedRect = MutableRect(0f, 0f, 0f, 0f)
+
+ private fun insertOrUpdateTransformedNode(
+ layoutNode: LayoutNode,
+ position: IntOffset,
+ firstPlacement: Boolean,
+ ) {
+ val coord = layoutNode.outerCoordinator
+ val delegate = layoutNode.measurePassDelegate
+ val width = delegate.measuredWidth
+ val height = delegate.measuredHeight
+ val rect = cachedRect
+
+ rect.set(
+ left = position.x.toFloat(),
+ top = position.y.toFloat(),
+ right = (position.x + width).toFloat(),
+ bottom = (position.y + height).toFloat(),
+ )
+
+ coord.boundingRectInRoot(rect)
+
+ val l = rect.left.toInt()
+ val t = rect.top.toInt()
+ val r = rect.right.toInt()
+ val b = rect.bottom.toInt()
+ val id = layoutNode.semanticsId
+ // NOTE: we call update here instead of move since the subhierarchy will not be moved by a
+ // simple delta since we are dealing with rotation/skew/scale/etc.
+ if (firstPlacement || !rects.update(id, l, t, r, b)) {
+ val parentId = layoutNode.parent?.semanticsId ?: -1
+ rects.insert(
+ id,
+ l,
+ t,
+ r,
+ b,
+ parentId = parentId,
+ )
+ }
+ invalidate()
+ }
+
+ private fun insertOrUpdate(
+ layoutNode: LayoutNode,
+ firstPlacement: Boolean,
+ l: Int,
+ t: Int,
+ r: Int,
+ b: Int,
+ ) {
+ val id = layoutNode.semanticsId
+ if (firstPlacement || !rects.move(id, l, t, r, b)) {
+ val parentId = layoutNode.parent?.semanticsId ?: -1
+ rects.insert(
+ id,
+ l,
+ t,
+ r,
+ b,
+ parentId = parentId,
+ )
+ }
+ invalidate()
+ }
+
+ private fun NodeCoordinator.positionInRoot(): IntOffset {
+ // TODO: can we use offsetFromRoot here to speed up calculation?
+ var position = Offset.Zero
+ var coordinator: NodeCoordinator? = this
+ while (coordinator != null) {
+ val layer = coordinator.layer
+ position += coordinator.position
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ val analysis = matrix.analyzeComponents()
+ if (analysis == 0b11) continue
+ val hasNonTranslationComponents = analysis and 0b10 == 0
+ if (hasNonTranslationComponents) {
+ return IntOffset.Max
+ }
+ position = matrix.map(position)
+ }
+ }
+ return position.round()
+ }
+
+ private fun NodeCoordinator.boundingRectInRoot(rect: MutableRect) {
+ // TODO: can we use offsetFromRoot here to speed up calculation?
+ var coordinator: NodeCoordinator? = this
+ while (coordinator != null) {
+ val layer = coordinator.layer
+ rect.translate(coordinator.position.toOffset())
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ if (!matrix.isIdentity()) {
+ matrix.map(rect)
+ }
+ }
+ }
+ }
+
+ private fun LayoutNode.outerToInnerOffset(): IntOffset {
+ val terminator = outerCoordinator
+ var position = Offset.Zero
+ var coordinator: NodeCoordinator? = innerCoordinator
+ while (coordinator != null) {
+ if (coordinator === terminator) break
+ val layer = coordinator.layer
+ position += coordinator.position
+ coordinator = coordinator.wrappedBy
+ if (layer != null) {
+ val matrix = layer.underlyingMatrix
+ val analysis = matrix.analyzeComponents()
+ if (analysis.isIdentity) continue
+ if (analysis.hasNonTranslationComponents) {
+ return IntOffset.Max
+ }
+ position = matrix.map(position)
+ }
+ }
+ return position.round()
+ }
+
+ fun remove(layoutNode: LayoutNode) {
+ rects.remove(layoutNode.semanticsId)
+ invalidate()
+ isFragmented = true
+ }
+}
+
+/**
+ * Returns true if the offset is not IntOffset.Max. In this class we are using `IntOffset.Max` to be
+ * a sentinel value for "unspecified" so that we can avoid boxing.
+ */
+private val IntOffset.isSet: Boolean
+ get() = this != IntOffset.Max
+
+/**
+ * We have logic that looks at whether or not a Matrix is an identity matrix, in which case we avoid
+ * doing expensive matrix calculations. Additionally, even if the matrix is non-identity, we can
+ * avoid a lot of extra work if the matrix is only doing translations, and no rotations/skews/scale.
+ *
+ * Since checking for these conditions involves a lot of overlapping work, we have this bespoke
+ * function which will return an Int that encodes the answer to both questions. If the 2nd bit of
+ * the result is set, this means that there are no rotations/skews/scales. If the first bit of the
+ * result is set, it means that there are no translations.
+ *
+ * This also means that the result of `0b11` indicates that it is the identity matrix.
+ */
+private fun Matrix.analyzeComponents(): Int {
+ // See top-level comment
+ val v = values
+ if (v.size < 16) return 0
+ val isIdentity3x3 =
+ v[0] == 1f &&
+ v[1] == 0f &&
+ v[2] == 0f &&
+ v[4] == 0f &&
+ v[5] == 1f &&
+ v[6] == 0f &&
+ v[8] == 0f &&
+ v[9] == 0f &&
+ v[10] == 1f
+
+ // translation components
+ val hasNoTranslationComponents = v[12] == 0f && v[13] == 0f && v[14] == 0f && v[15] == 1f
+
+ return isIdentity3x3.toInt() shl 1 or hasNoTranslationComponents.toInt()
+}
+
+@Suppress("NOTHING_TO_INLINE")
+private inline val Int.isIdentity: Boolean
+ get() = this == 0b11
+
+@Suppress("NOTHING_TO_INLINE")
+private inline val Int.hasNonTranslationComponents: Boolean
+ get() = this and 0b10 == 0
+
+@Suppress("NOTHING_TO_INLINE") private inline fun Boolean.toInt(): Int = if (this) 1 else 0
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 1c0b519..07eed97 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -50,29 +50,29 @@
}
}
- jvmMain {
+ jvmCommonMain {
+ dependsOn(commonMain)
dependencies {
}
}
androidMain {
- dependsOn(commonMain)
- dependsOn(jvmMain)
+ dependsOn(jvmCommonMain)
dependencies {
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.core:core-ktx:1.5.0")
}
}
- jvmTest {
+ jvmCommonTest {
dependsOn(commonTest)
dependencies {
}
}
androidInstrumentedTest {
- dependsOn(jvmTest)
+ dependsOn(jvmCommonTest)
dependencies {
implementation(libs.kotlinTest)
implementation(libs.testRules)
@@ -89,7 +89,7 @@
}
androidUnitTest {
- dependsOn(jvmTest)
+ dependsOn(jvmCommonTest)
dependencies {
implementation(libs.kotlinTest)
implementation(libs.testRules)
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
index f79e18c..7699379 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
@@ -32,7 +32,7 @@
implementation(project(":compose:material:material"))
implementation("androidx.compose.material:material-icons-core:1.6.7")
implementation(project(":compose:ui:ui-tooling-preview"))
- debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation(project(":compose:ui:ui-tooling"))
}
android {
diff --git a/constraintlayout/constraintlayout/build.gradle b/constraintlayout/constraintlayout/build.gradle
index 9b2ddcd..1844e81 100644
--- a/constraintlayout/constraintlayout/build.gradle
+++ b/constraintlayout/constraintlayout/build.gradle
@@ -33,7 +33,7 @@
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.core:core:1.3.2")
implementation(project(":constraintlayout:constraintlayout-core"))
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
testImplementation(libs.junit)
diff --git a/coordinatorlayout/coordinatorlayout/build.gradle b/coordinatorlayout/coordinatorlayout/build.gradle
index 8f27824..3f4475a 100644
--- a/coordinatorlayout/coordinatorlayout/build.gradle
+++ b/coordinatorlayout/coordinatorlayout/build.gradle
@@ -27,8 +27,8 @@
})
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.coordinatorlayout", module: "coordinatorlayout"
})
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 4824910..39be349 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -42,8 +42,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.espressoIntents)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
}
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index 5ed44fb..21462c7 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -30,7 +30,7 @@
dependencies {
implementation(project(":core:core-ktx"))
- implementation(projectOrArtifact(":appcompat:appcompat"))
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.annotation:annotation:1.8.1")
compileOnly(project(":annotation:annotation-sampled"))
}
diff --git a/core/core-splashscreen/build.gradle b/core/core-splashscreen/build.gradle
index 58806d7..3ffa382 100644
--- a/core/core-splashscreen/build.gradle
+++ b/core/core-splashscreen/build.gradle
@@ -45,8 +45,9 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation(libs.truth)
- androidTestImplementation(projectOrArtifact(":appcompat:appcompat"))
+ androidTestImplementation(project(":appcompat:appcompat"))
androidTestImplementation(project(":test:screenshot:screenshot"))
+ androidTestImplementation(project(":internal-testutils-runtime"))
}
androidx {
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 404af32..09a3102 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -41,7 +41,7 @@
dependencies {
implementation(project(":core:core-splashscreen"))
implementation(project(":core:core-ktx"))
- implementation(projectOrArtifact(":appcompat:appcompat"))
+ implementation(project(":appcompat:appcompat"))
implementation("androidx.annotation:annotation:1.8.1")
- implementation(projectOrArtifact(":interpolator:interpolator"))
+ implementation(project(":interpolator:interpolator"))
}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
index 0e0e9b1..992f588 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
@@ -44,11 +44,9 @@
/**
* If set to true, takes a screenshot of the splash screen and saves it in
- * [SplashScreenTestController.splashScreenScreenshot] and a second screenshot of
- * [androidx.core.splashscreen.SplashScreenViewProvider.view] and saves it in
- * [SplashScreenTestController.splashScreenViewScreenShot]
+ * [SplashScreenTestController.splashScreenScreenshot]
*/
-internal const val EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT = "SplashScreenViewScreenShot"
+internal const val EXTRA_SPLASHSCREEN_SCREENSHOT = "SplashScreenScreenShot"
public interface SplashScreenTestControllerHolder {
public var controller: SplashScreenTestController
@@ -56,7 +54,6 @@
public class SplashScreenTestController(internal val activity: Activity) {
- public var splashScreenViewScreenShot: Bitmap? = null
public var splashScreenScreenshot: Bitmap? = null
public var splashscreenIconId: Int = 0
public var splashscreenBackgroundId: Int = 0
@@ -95,7 +92,7 @@
val extras = intent.extras ?: Bundle.EMPTY
val useListener = extras.getBoolean(EXTRA_ANIMATION_LISTENER)
- val takeScreenShot = extras.getBoolean(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT)
+ val takeScreenShot = extras.getBoolean(EXTRA_SPLASHSCREEN_SCREENSHOT)
val waitForSplashscreen = extras.getBoolean(EXTRA_SPLASHSCREEN_WAIT)
val tv = TypedValue()
@@ -137,17 +134,7 @@
if (onExitAnimationListener(splashScreenViewProvider)) {
return@setOnExitAnimationListener
}
- if (takeScreenShot) {
- splashScreenViewProvider.view.postDelayed(
- {
- splashScreenViewScreenShot =
- getInstrumentation().uiAutomation.takeScreenshot()
- splashScreenViewProvider.remove()
- exitAnimationListenerLatch.countDown()
- },
- 100
- )
- } else {
+ if (!takeScreenShot) {
splashScreenViewProvider.remove()
exitAnimationListenerLatch.countDown()
}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index 74c7f09..5bcdaec 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -30,8 +30,10 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.screenshot.matchers.MSSIMMatcher
import androidx.test.uiautomator.UiDevice
+import androidx.testutils.PollingCheck
import java.io.ByteArrayOutputStream
import java.io.File
+import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.CountDownLatch
@@ -144,18 +146,62 @@
@SdkSuppress(minSdkVersion = 23)
@Test
public fun splashscreenViewScreenshotComparison() {
- val activity = startActivityWithSplashScreen {
+ val controller = startActivityWithSplashScreen {
// Clear out any previous instances
it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
it.putExtra(EXTRA_ANIMATION_LISTENER, true)
- it.putExtra(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT, true)
+ it.putExtra(EXTRA_SPLASHSCREEN_SCREENSHOT, true)
}
- assertTrue(activity.waitedLatch.await(2, TimeUnit.SECONDS))
- activity.waitBarrier.set(false)
- activity.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
- compareBitmaps(activity.splashScreenScreenshot!!, activity.splashScreenViewScreenShot!!)
+ var splashScreenViewScreenShot: Bitmap? = null
+
+ controller.doOnExitAnimation {
+ // b/355716686
+ // During the transition from the splash screen of system starting window to the
+ // activity, there may be a moment that `PhoneWindowManager`'s
+ // `mTopFullscreenOpaqueWindowState` would be `null`, which might lead to the flicker of
+ // status bar (b/64291272,
+ // https://android.googlesource.com/platform/frameworks/base/+/c0c9324fcb03c85ef7bed2d997c441119823d31c%5E%21/)
+ val topFullscreenWinState = "mTopFullscreenOpaqueWindowState"
+
+ // We should take the screenshot when `mTopFullscreenOpaqueWindowState` is window of the
+ // activity
+ val topFullscreenWinStateBelongsToActivity =
+ Regex(
+ topFullscreenWinState +
+ "=Window\\{.*" +
+ controller.activity.componentName.className +
+ "\\}"
+ )
+
+ val isTopFullscreenWinStateReady: () -> Boolean = {
+ val dumpedWindowPolicy =
+ InstrumentationRegistry.getInstrumentation()
+ .uiAutomation
+ .executeShellCommand("dumpsys window p")
+ .use { FileInputStream(it.fileDescriptor).reader().readText() }
+
+ !dumpedWindowPolicy.contains(topFullscreenWinState) ||
+ dumpedWindowPolicy.contains(topFullscreenWinStateBelongsToActivity)
+ }
+
+ PollingCheck.waitFor(2000, isTopFullscreenWinStateReady)
+ if (!isTopFullscreenWinStateReady())
+ fail("$topFullscreenWinState is not ready, cannot take screenshot")
+
+ splashScreenViewScreenShot =
+ InstrumentationRegistry.getInstrumentation().uiAutomation.takeScreenshot()
+ it.remove()
+ controller.exitAnimationListenerLatch.countDown()
+ true
+ }
+
+ assertTrue(controller.waitedLatch.await(2, TimeUnit.SECONDS))
+ controller.waitBarrier.set(false)
+ controller.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
+
+ compareBitmaps(controller.splashScreenScreenshot!!, splashScreenViewScreenShot!!)
}
/**
diff --git a/core/core-telecom/api/current.txt b/core/core-telecom/api/current.txt
index c94b261..57b1ced 100644
--- a/core/core-telecom/api/current.txt
+++ b/core/core-telecom/api/current.txt
@@ -124,6 +124,7 @@
package androidx.core.telecom.extensions {
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface CallExtensionScope {
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtensionRemote addLocalCallSilenceExtension(kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onIsLocallySilencedUpdated);
method public androidx.core.telecom.extensions.ParticipantExtensionRemote addParticipantExtension(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant?,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onActiveParticipantChanged, kotlin.jvm.functions.Function2<? super java.util.Set<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onParticipantsUpdated);
method public void onConnected(kotlin.jvm.functions.Function2<? super android.telecom.Call,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
}
@@ -137,6 +138,7 @@
}
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
}
@@ -148,6 +150,16 @@
property public abstract boolean isSupported;
}
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface LocalCallSilenceExtension {
+ method public suspend Object? updateIsLocallySilenced(boolean isSilenced, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface LocalCallSilenceExtensionRemote {
+ method public boolean isSupported();
+ method public suspend Object? requestLocalCallSilenceUpdate(boolean isSilenced, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ property public abstract boolean isSupported;
+ }
+
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public final class Participant {
ctor public Participant(String id, CharSequence name);
method public String getId();
diff --git a/core/core-telecom/api/restricted_current.txt b/core/core-telecom/api/restricted_current.txt
index c94b261..57b1ced 100644
--- a/core/core-telecom/api/restricted_current.txt
+++ b/core/core-telecom/api/restricted_current.txt
@@ -124,6 +124,7 @@
package androidx.core.telecom.extensions {
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface CallExtensionScope {
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtensionRemote addLocalCallSilenceExtension(kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onIsLocallySilencedUpdated);
method public androidx.core.telecom.extensions.ParticipantExtensionRemote addParticipantExtension(kotlin.jvm.functions.Function2<? super androidx.core.telecom.extensions.Participant?,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onActiveParticipantChanged, kotlin.jvm.functions.Function2<? super java.util.Set<? extends androidx.core.telecom.extensions.Participant>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onParticipantsUpdated);
method public void onConnected(kotlin.jvm.functions.Function2<? super android.telecom.Call,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block);
}
@@ -137,6 +138,7 @@
}
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface ExtensionInitializationScope {
+ method public androidx.core.telecom.extensions.LocalCallSilenceExtension addLocalSilenceExtension(boolean initialCallSilenceState, kotlin.jvm.functions.Function2<? super java.lang.Boolean,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onLocalSilenceUpdate);
method public androidx.core.telecom.extensions.ParticipantExtension addParticipantExtension(optional java.util.Set<androidx.core.telecom.extensions.Participant> initialParticipants, optional androidx.core.telecom.extensions.Participant? initialActiveParticipant);
method public void onCall(kotlin.jvm.functions.Function2<? super androidx.core.telecom.CallControlScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> onCall);
}
@@ -148,6 +150,16 @@
property public abstract boolean isSupported;
}
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface LocalCallSilenceExtension {
+ method public suspend Object? updateIsLocallySilenced(boolean isSilenced, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ }
+
+ @SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public interface LocalCallSilenceExtensionRemote {
+ method public boolean isSupported();
+ method public suspend Object? requestLocalCallSilenceUpdate(boolean isSilenced, kotlin.coroutines.Continuation<? super androidx.core.telecom.CallControlResult>);
+ property public abstract boolean isSupported;
+ }
+
@SuppressCompatibility @androidx.core.telecom.util.ExperimentalAppActions public final class Participant {
ctor public Participant(String id, CharSequence name);
method public String getId();
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
index f4ec9ac..44f5287 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallListAdapter.kt
@@ -47,6 +47,7 @@
val callIdTextView: TextView = itemView.findViewById(R.id.callIdTextView)
val currentState: TextView = itemView.findViewById(R.id.callStateTextView)
val currentEndpoint: TextView = itemView.findViewById(R.id.endpointStateTextView)
+ val participants: TextView = itemView.findViewById(R.id.participantsTextView)
// Call State Buttons
val activeButton: Button = itemView.findViewById(R.id.activeButton)
@@ -57,6 +58,10 @@
val earpieceButton: Button = itemView.findViewById(R.id.selectEndpointButton)
val speakerButton: Button = itemView.findViewById(R.id.speakerButton)
val bluetoothButton: Button = itemView.findViewById(R.id.bluetoothButton)
+
+ // Participant Buttons
+ val addParticipantButton: Button = itemView.findViewById(R.id.addParticipantButton)
+ val removeParticipantButton: Button = itemView.findViewById(R.id.removeParticipantButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -180,6 +185,25 @@
}
}
}
+
+ holder.addParticipantButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ ItemsViewModel.callObject.mParticipantControl?.onParticipantAdded?.invoke()
+ }
+ }
+
+ holder.removeParticipantButton.setOnClickListener {
+ CoroutineScope(Dispatchers.Main).launch {
+ ItemsViewModel.callObject.mParticipantControl?.onParticipantRemoved?.invoke()
+ }
+ }
+ }
+ }
+
+ fun updateParticipants(callId: String, participants: List<ParticipantState>) {
+ CoroutineScope(Dispatchers.Main).launch {
+ val holder = mCallIdToViewHolder[callId]
+ holder?.participants?.text = "participants=[${printParticipants(participants)}]"
}
}
@@ -197,6 +221,32 @@
}
}
+ private fun printParticipants(participants: List<ParticipantState>): String {
+ if (participants.isEmpty()) return "<NONE>"
+ val builder = StringBuilder()
+ val iterator = participants.iterator()
+ while (iterator.hasNext()) {
+ val participant = iterator.next()
+ builder.append("<")
+ if (participant.isActive) {
+ builder.append(" * ")
+ }
+ builder.append(participant.name)
+ if (participant.isSelf) {
+ builder.append("(me)")
+ }
+ if (participant.isHandRaised) {
+ builder.append(" ")
+ builder.append("(RH)")
+ }
+ builder.append(">")
+ if (iterator.hasNext()) {
+ builder.append(", ")
+ }
+ }
+ return builder.toString()
+ }
+
private fun endAudioRecording() {
try {
// Stop audio recording
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
index 77025be..79752752 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
@@ -27,6 +27,8 @@
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallEndpointCompat
import androidx.core.telecom.CallsManager
+import androidx.core.telecom.extensions.RaiseHandState
+import androidx.core.telecom.util.ExperimentalAppActions
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -34,6 +36,9 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@RequiresApi(34)
@@ -65,6 +70,22 @@
mCallsManager = CallsManager(this)
mCallCount = 0
+ val raiseHandCheckBox = findViewById<CheckBox>(R.id.RaiseHandCheckbox)
+ val kickParticipantCheckBox = findViewById<CheckBox>(R.id.KickPartCheckbox)
+ val participantCheckBox = findViewById<CheckBox>(R.id.ParticipantsCheckbox)
+
+ participantCheckBox.setOnCheckedChangeListener { _, isChecked ->
+ if (!isChecked) {
+ raiseHandCheckBox.isEnabled = false
+ raiseHandCheckBox.isChecked = false
+ kickParticipantCheckBox.isEnabled = false
+ kickParticipantCheckBox.isChecked = false
+ } else {
+ raiseHandCheckBox.isEnabled = true
+ kickParticipantCheckBox.isEnabled = true
+ }
+ }
+
val registerPhoneAccountButton = findViewById<Button>(R.id.registerButton)
registerPhoneAccountButton.setOnClickListener { mScope.launch { registerPhoneAccount() } }
@@ -75,12 +96,26 @@
val addOutgoingCallButton = findViewById<Button>(R.id.addOutgoingCall)
addOutgoingCallButton.setOnClickListener {
- mScope.launch { addCallWithAttributes(Utilities.OUTGOING_CALL_ATTRIBUTES) }
+ mScope.launch {
+ addCallWithAttributes(
+ Utilities.OUTGOING_CALL_ATTRIBUTES,
+ participantCheckBox.isChecked,
+ raiseHandCheckBox.isChecked,
+ kickParticipantCheckBox.isChecked
+ )
+ }
}
val addIncomingCallButton = findViewById<Button>(R.id.addIncomingCall)
addIncomingCallButton.setOnClickListener {
- mScope.launch { addCallWithAttributes(Utilities.INCOMING_CALL_ATTRIBUTES) }
+ mScope.launch {
+ addCallWithAttributes(
+ Utilities.INCOMING_CALL_ATTRIBUTES,
+ participantCheckBox.isChecked,
+ raiseHandCheckBox.isChecked,
+ kickParticipantCheckBox.isChecked
+ )
+ }
}
// setup the adapters which hold the endpoint and call rows
@@ -124,7 +159,12 @@
mCallsManager?.registerAppWithTelecom(capabilities)
}
- private suspend fun addCallWithAttributes(attributes: CallAttributesCompat) {
+ private suspend fun addCallWithAttributes(
+ attributes: CallAttributesCompat,
+ isParticipantsEnabled: Boolean,
+ isRaiseHandEnabled: Boolean,
+ isKickParticipantEnabled: Boolean
+ ) {
Log.i(TAG, "addCallWithAttributes: attributes=$attributes")
val callObject = VoipCall()
@@ -132,36 +172,17 @@
val handler = CoroutineExceptionHandler { _, exception ->
Log.i(TAG, "CoroutineExceptionHandler: handling e=$exception")
}
-
CoroutineScope(Dispatchers.Default).launch(handler) {
try {
- attributes.preferredStartingCallEndpoint =
- mPreCallEndpointAdapter.mSelectedCallEndpoint
- mCallsManager!!.addCall(
- attributes,
- callObject.mOnAnswerLambda,
- callObject.mOnDisconnectLambda,
- callObject.mOnSetActiveLambda,
- callObject.mOnSetInActiveLambda,
- ) {
- mPreCallEndpointAdapter.mSelectedCallEndpoint = null
- // inject client control interface into the VoIP call object
- callObject.setCallId(getCallId().toString())
- callObject.setCallControl(this)
-
- // Collect updates
- launch {
- currentCallEndpoint.collect { callObject.onCallEndpointChanged(it) }
- }
-
- launch {
- availableEndpoints.collect {
- callObject.onAvailableCallEndpointsChanged(it)
- }
- }
-
- launch { isMuted.collect { callObject.onMuteStateChanged(it) } }
- addCallRow(callObject)
+ if (isParticipantsEnabled) {
+ addCallWithExtensions(
+ attributes,
+ callObject,
+ isRaiseHandEnabled,
+ isKickParticipantEnabled
+ )
+ } else {
+ addCall(attributes, callObject)
}
} catch (e: Exception) {
logException(e, "addCallWithAttributes: catch inner")
@@ -174,6 +195,108 @@
}
}
+ private suspend fun addCall(attributes: CallAttributesCompat, callObject: VoipCall) {
+ mCallsManager!!.addCall(
+ attributes,
+ callObject.mOnAnswerLambda,
+ callObject.mOnDisconnectLambda,
+ callObject.mOnSetActiveLambda,
+ callObject.mOnSetInActiveLambda,
+ ) {
+ mPreCallEndpointAdapter.mSelectedCallEndpoint = null
+ // inject client control interface into the VoIP call object
+ callObject.setCallId(getCallId().toString())
+ callObject.setCallControl(this)
+
+ // Collect updates
+ launch { currentCallEndpoint.collect { callObject.onCallEndpointChanged(it) } }
+
+ launch { availableEndpoints.collect { callObject.onAvailableCallEndpointsChanged(it) } }
+
+ launch { isMuted.collect { callObject.onMuteStateChanged(it) } }
+ addCallRow(callObject)
+ }
+ }
+
+ @OptIn(ExperimentalAppActions::class)
+ private suspend fun addCallWithExtensions(
+ attributes: CallAttributesCompat,
+ callObject: VoipCall,
+ isRaiseHandEnabled: Boolean = false,
+ isKickParticipantEnabled: Boolean = false
+ ) {
+ mCallsManager!!.addCallWithExtensions(
+ attributes,
+ callObject.mOnAnswerLambda,
+ callObject.mOnDisconnectLambda,
+ callObject.mOnSetActiveLambda,
+ callObject.mOnSetInActiveLambda,
+ ) {
+ val participants = ParticipantsExtensionManager()
+ val participantExtension =
+ addParticipantExtension(
+ initialParticipants =
+ participants.participants.value.map { it.toParticipant() }.toSet()
+ )
+ var raiseHandState: RaiseHandState? = null
+ if (isRaiseHandEnabled) {
+ raiseHandState =
+ participantExtension.addRaiseHandSupport {
+ participants.onRaisedHandStateChanged(it)
+ }
+ }
+ if (isKickParticipantEnabled) {
+ participantExtension.addKickParticipantSupport {
+ participants.onKickParticipant(it)
+ }
+ }
+ onCall {
+ mPreCallEndpointAdapter.mSelectedCallEndpoint = null
+ // inject client control interface into the VoIP call object
+ callObject.setCallId(getCallId().toString())
+ callObject.setCallControl(this)
+ callObject.setParticipantControl(
+ ParticipantControl(
+ onParticipantAdded = participants::addParticipant,
+ onParticipantRemoved = participants::removeParticipant
+ )
+ )
+ addCallRow(callObject)
+
+ // Collect updates
+ participants.participants
+ .onEach {
+ participantExtension.updateParticipants(
+ it.map { p -> p.toParticipant() }.toSet()
+ )
+ participantExtension.updateActiveParticipant(
+ it.firstOrNull { p -> p.isActive }?.toParticipant()
+ )
+ raiseHandState?.updateRaisedHands(
+ it.filter { p -> p.isHandRaised }.map { p -> p.toParticipant() }
+ )
+ callObject.onParticipantsChanged(it)
+ }
+ .launchIn(this)
+
+ launch {
+ while (true) {
+ delay(1000)
+ participants.changeParticipantStates()
+ }
+ }
+
+ launch { currentCallEndpoint.collect { callObject.onCallEndpointChanged(it) } }
+
+ launch {
+ availableEndpoints.collect { callObject.onAvailableCallEndpointsChanged(it) }
+ }
+
+ launch { isMuted.collect { callObject.onMuteStateChanged(it) } }
+ }
+ }
+ }
+
private fun fetchPreCallEndpoints(cancelFlowButton: Button) {
val endpointsFlow = mCallsManager!!.getAvailableStartingCallEndpoints()
CoroutineScope(Dispatchers.Default).launch {
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantState.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantState.kt
new file mode 100644
index 0000000..ddfccb2
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantState.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/** The state of one participant in a call */
+data class ParticipantState(
+ val id: String,
+ val name: String,
+ val isActive: Boolean,
+ val isHandRaised: Boolean,
+ val isSelf: Boolean
+)
+
+/** Control callback handler for adding/removing new participants in the Call via UI */
+data class ParticipantControl(
+ val onParticipantAdded: () -> Unit,
+ val onParticipantRemoved: () -> Unit
+)
+
+@OptIn(ExperimentalAppActions::class)
+fun ParticipantState.toParticipant(): Participant {
+ return Participant(id, name)
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantsExtensionManager.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantsExtensionManager.kt
new file mode 100644
index 0000000..0f16fb8
--- /dev/null
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/ParticipantsExtensionManager.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.util.ExperimentalAppActions
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.random.Random
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** Manages the extension in a Call */
+@OptIn(ExperimentalAppActions::class)
+class ParticipantsExtensionManager {
+ companion object {
+ // Represents "self" in participants window, which allows raise hand state modification and
+ // no kicking
+ internal val SELF_PARTICIPANT =
+ ParticipantState(
+ "0",
+ "Participant 0",
+ isHandRaised = false,
+ isActive = false,
+ isSelf = true
+ )
+ }
+
+ private val nextId = AtomicInteger(1)
+ private val mParticipants: MutableStateFlow<List<ParticipantState>> =
+ MutableStateFlow(listOf(SELF_PARTICIPANT))
+ /** The current state of participants for the given call */
+ val participants = mParticipants.asStateFlow()
+
+ /** Adds a new Participant to the call. */
+ fun addParticipant() {
+ val id = nextId.getAndAdd(1)
+ mParticipants.update {
+ ArrayList(it).apply {
+ add(
+ ParticipantState(
+ id = "$id",
+ name = "Participant $id",
+ isHandRaised = false,
+ isActive = false,
+ isSelf = false
+ )
+ )
+ }
+ }
+ }
+
+ /** Removes the last participant in the List */
+ fun removeParticipant() {
+ mParticipants.update { participants ->
+ if (participants.isEmpty()) return
+ if (participants.last().isSelf) return
+ ArrayList(participants).apply { remove(last()) }
+ }
+ }
+
+ /** randomly change all Participant raise hand/active states one time */
+ fun changeParticipantStates() {
+ mParticipants.update { participants ->
+ // Randomly choose a participant to make active & get hand raised
+ val nextActive = Random.nextInt(0, participants.size + 1) - 1
+ var raisedHandParticipant: ParticipantState? = null
+ if (participants.size > 1) {
+ val nextRaisedHand = Random.nextInt(0, participants.size)
+ if (nextRaisedHand > 0) {
+ // self controls their own raised hand
+ raisedHandParticipant = participants.getOrNull(nextRaisedHand)
+ }
+ }
+ val activeParticipant = participants.getOrNull(nextActive)
+
+ participants.map { p ->
+ ParticipantState(
+ id = p.id,
+ name = p.name,
+ isActive = activeParticipant?.id == p.id,
+ isHandRaised =
+ if (SELF_PARTICIPANT.id != p.id) {
+ raisedHandParticipant?.id == p.id
+ } else {
+ p.isHandRaised
+ },
+ isSelf = p.isSelf
+ )
+ }
+ }
+ }
+
+ /** Change the raised hand state of the participant representing this user */
+ fun onRaisedHandStateChanged(isHandRaised: Boolean) {
+ mParticipants.update { state ->
+ val newState = ArrayList<ParticipantState>()
+ for (p in state) {
+ if (p.id == SELF_PARTICIPANT.id) {
+ newState.add(
+ ParticipantState(
+ id = p.id,
+ name = p.name,
+ isActive = p.isActive,
+ isHandRaised = isHandRaised,
+ isSelf = p.isSelf
+ )
+ )
+ } else {
+ newState.add(p)
+ }
+ }
+ newState
+ }
+ }
+
+ /** Kick a participant as long as it is not this user */
+ fun onKickParticipant(participant: Participant) {
+ mParticipants.update { state ->
+ if (participant.id == SELF_PARTICIPANT.id) return
+ val candidate = state.firstOrNull { it.id == participant.id }
+ if (candidate == null) return
+ ArrayList(state).apply { remove(candidate) }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
index 1b18a37..912610e 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/VoipCall.kt
@@ -28,6 +28,7 @@
var mAdapter: CallListAdapter? = null
var mCallControl: CallControlScope? = null
+ var mParticipantControl: ParticipantControl? = null
var mCurrentEndpoint: CallEndpointCompat? = null
var mAvailableEndpoints: List<CallEndpointCompat>? = ArrayList()
var mIsMuted = false
@@ -57,6 +58,10 @@
mCallControl = callControl
}
+ fun setParticipantControl(participantControl: ParticipantControl) {
+ mParticipantControl = participantControl
+ }
+
fun setCallAdapter(adapter: CallListAdapter?) {
mAdapter = adapter
}
@@ -65,6 +70,10 @@
mTelecomCallId = callId
}
+ fun onParticipantsChanged(participants: List<ParticipantState>) {
+ mAdapter?.updateParticipants(mTelecomCallId, participants)
+ }
+
fun onCallEndpointChanged(endpoint: CallEndpointCompat) {
Log.i(TAG, "onCallEndpointChanged: endpoint=$endpoint")
mCurrentEndpoint = endpoint
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 68efdc9..f7dd3d3 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -53,10 +53,37 @@
<CheckBox
android:id="@+id/streamingCheckBox"
android:layout_width="match_parent"
- android:layout_height="61dp"
+ android:layout_height="wrap_content"
android:padding="16dp"
android:text="CAPABILITY_SUPPORTS_CALL_STREAMING" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/ParticipantsCheckbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:padding="16dp"
+ android:text="Participants" />
+
+ <CheckBox
+ android:id="@+id/RaiseHandCheckbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:text="Raise Hand" />
+ <CheckBox
+ android:id="@+id/KickPartCheckbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:text="Kick" />
+ </LinearLayout>
+
<Button
android:id="@+id/registerButton"
android:layout_width="wrap_content"
diff --git a/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
index ae53266..b12fe73 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
+++ b/core/core-telecom/integration-tests/testapp/src/main/res/layout/call_row.xml
@@ -61,6 +61,18 @@
android:text="currentEndpoint=[null]" />
</LinearLayout>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/participantsTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="participants=[null]" />
+
+ </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@@ -118,5 +130,25 @@
android:text="bluetooth" />
</LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/addParticipantButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text= "+ Participant" />
+
+ <Button
+ android:id="@+id/removeParticipantButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="- Participant" />
+ </LinearLayout>
+
</LinearLayout>
</LinearLayout>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testicsapp/build.gradle b/core/core-telecom/integration-tests/testicsapp/build.gradle
new file mode 100644
index 0000000..b8b9fdc
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace 'androidx.core.telecom.test'
+
+ defaultConfig {
+ applicationId "androidx.core.telecom.icstest"
+ minSdk 29 // Move down to 23 if we support CallingApp:checkDialerRole for < Q
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+ }
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ //@Serialize
+ implementation(libs.kotlinSerializationCore)
+ // Test package
+ implementation project(":core:core-telecom")
+ // Compose
+ implementation("androidx.activity:activity-compose:1.9.1")
+ // Themes and Dynamic coloring
+ implementation("androidx.compose.material3:material3:1.2.1")
+ // Icons
+ implementation("androidx.compose.material:material-icons-core:1.6.8")
+ // @Preview
+ implementation("androidx.compose.ui:ui-tooling-preview:1.6.8")
+ debugImplementation("androidx.compose.ui:ui-tooling:1.6.8")
+ // Navigation
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ // collectAsStateWithLifecycle
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.4")
+}
+
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/AndroidManifest.xml b/core/core-telecom/integration-tests/testicsapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..578dcca
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/AndroidManifest.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:tools="http://schemas.android.com/tools"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
+ <application
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.IcsTest">
+
+ <activity
+ android:name=".ui.CallingActivity"
+ android:exported="true"
+ android:label="@string/main_activity_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <service android:name=".services.InCallServiceImpl"
+ android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:exported="true"
+ tools:ignore="MissingServiceExportedEqualsTrue">
+ <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
+ <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS" android:value="true" />
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
+ <activity android:name=".DialerActivity"
+ android:label="DialerActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="tel" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/Compatibility.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/Compatibility.kt
new file mode 100644
index 0000000..a623e2a
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/Compatibility.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test
+
+import android.bluetooth.BluetoothDevice
+import android.net.Uri
+import android.os.Build
+import android.os.OutcomeReceiver
+import android.telecom.Call
+import android.telecom.Call.Details
+import android.telecom.CallEndpoint
+import android.telecom.CallEndpointException
+import android.telecom.InCallService
+import androidx.annotation.RequiresApi
+import java.util.concurrent.Executor
+
+/** Ensure compatibility for APIs back to API level 29 */
+object Compatibility {
+ @JvmStatic
+ fun getContactDisplayName(details: Details): String? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Api30Impl.getContactDisplayName(details)
+ } else {
+ details.callerDisplayName
+ }
+ }
+
+ @JvmStatic
+ fun getContactPhotoUri(details: Details): Uri? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Impl.getContactDisplayUri(details)
+ } else {
+ null
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ @JvmStatic
+ fun getCallState(call: Call): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Api31Impl.getCallState(call)
+ } else {
+ call.state
+ }
+ }
+
+ @JvmStatic
+ fun getBluetoothDeviceAlias(device: BluetoothDevice): Result<String?> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Api30Impl.getBluetoothDeviceAlias(device)
+ } else {
+ Result.success(null)
+ }
+ }
+
+ @JvmStatic
+ fun getEndpointIdentifier(endpoint: CallEndpoint): String? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Impl.getEndpointIdentifier(endpoint)
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun getEndpointName(endpoint: CallEndpoint): String? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Impl.getEndpointName(endpoint).toString()
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun getEndpointType(endpoint: CallEndpoint): Int? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Impl.getEndpointType(endpoint)
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun requestCallEndpointChange(
+ service: InCallService,
+ endpoint: CallEndpoint,
+ executor: Executor,
+ callback: OutcomeReceiver<Void, CallEndpointException>
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Api34Impl.requestCallEndpointChange(service, endpoint, executor, callback)
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+object Api30Impl {
+ @JvmStatic
+ fun getContactDisplayName(details: Details): String? {
+ return details.contactDisplayName
+ }
+
+ @JvmStatic
+ fun getBluetoothDeviceAlias(device: BluetoothDevice): Result<String?> {
+ return try {
+ Result.success(device.alias)
+ } catch (e: SecurityException) {
+ Result.failure(e)
+ }
+ }
+}
+
+/** Ensure compatibility for [Call] APIs for API level 31+ */
+@RequiresApi(Build.VERSION_CODES.S)
+object Api31Impl {
+ @JvmStatic
+ fun getCallState(call: Call): Int {
+ return call.details.state
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+object Api34Impl {
+
+ @JvmStatic
+ fun getContactDisplayUri(details: Details): Uri? {
+ return details.contactPhotoUri
+ }
+
+ @JvmStatic
+ fun getEndpointIdentifier(endpoint: CallEndpoint): String {
+ return endpoint.identifier.toString()
+ }
+
+ @JvmStatic
+ fun getEndpointName(endpoint: CallEndpoint): CharSequence {
+ return endpoint.endpointName
+ }
+
+ @JvmStatic
+ fun getEndpointType(endpoint: CallEndpoint): Int {
+ return endpoint.endpointType
+ }
+
+ @JvmStatic
+ fun requestCallEndpointChange(
+ service: InCallService,
+ endpoint: CallEndpoint,
+ executor: Executor,
+ callback: OutcomeReceiver<Void, CallEndpointException>
+ ) {
+ service.requestCallEndpointChange(endpoint, executor, callback)
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/DialerActivity.kt
similarity index 65%
copy from compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
copy to core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/DialerActivity.kt
index c273ad3..faab36a 100644
--- a/compose/foundation/foundation-layout/src/jvmMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.jvm.kt
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/DialerActivity.kt
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.core.telecom.test
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable {
- return exception.initCause(cause)
-}
+import androidx.activity.ComponentActivity
+
+/**
+ * Not used yet - mainly here to fulfill the role requirements for this test application.
+ *
+ * This activity will become useful if dialing numbers becomes a requirement for this application.
+ */
+class DialerActivity : ComponentActivity()
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallAudioRouteResolver.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallAudioRouteResolver.kt
new file mode 100644
index 0000000..7459b7b
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallAudioRouteResolver.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.bluetooth.BluetoothDevice
+import android.os.Build
+import android.os.OutcomeReceiver
+import android.telecom.CallAudioState
+import android.telecom.CallEndpoint
+import android.telecom.CallEndpointException
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.test.Compatibility
+import java.util.UUID
+import java.util.concurrent.Executor
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Tracks the current state of the available and current audio route on the device while in call,
+ * taking into account the device's android API version.
+ *
+ * @param coroutineScope The scope attached to the lifecycle of the Service
+ * @param callData The stream of calls that are active on this device
+ * @param onChangeAudioRoute The callback used when user has requested to change the audio route on
+ * the device for devices running an API version < UDC
+ * @param onRequestBluetoothAudio The callback used when the user has requested to change the audio
+ * route for devices running on API version < UDC
+ * @param onRequestEndpointChange The callback used when the user has requested to change the
+ * endpoint for devices running API version UDC+
+ */
+class CallAudioRouteResolver(
+ private val coroutineScope: CoroutineScope,
+ callData: StateFlow<List<CallData>>,
+ private val onChangeAudioRoute: (Int) -> Unit,
+ private val onRequestBluetoothAudio: (BluetoothDevice) -> Unit,
+ private val onRequestEndpointChange:
+ (CallEndpoint, Executor, OutcomeReceiver<Void, CallEndpointException>) -> Unit
+) {
+ private val mIsCallAudioStateDeprecated =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+
+ // Maps the CallAudioEndpoint to the associated BluetoothDevice (if applicable) for bkwds
+ // compatibility with devices running on API version < UDC
+ data class EndpointEntry(val endpoint: CallAudioEndpoint, val device: BluetoothDevice? = null)
+
+ private val mCurrentEndpoint: MutableStateFlow<CallAudioEndpoint?> = MutableStateFlow(null)
+ private val mAvailableEndpoints: MutableStateFlow<List<CallAudioEndpoint>> =
+ MutableStateFlow(emptyList())
+ val currentEndpoint: StateFlow<CallAudioEndpoint?> = mCurrentEndpoint.asStateFlow()
+ val availableEndpoints = mAvailableEndpoints.asStateFlow()
+
+ private val mCallAudioState: MutableStateFlow<CallAudioState?> = MutableStateFlow(null)
+ private val mEndpoints: MutableStateFlow<List<EndpointEntry>> = MutableStateFlow(emptyList())
+ private val mCurrentCallEndpoint: MutableStateFlow<CallEndpoint?> = MutableStateFlow(null)
+ private val mAvailableCallEndpoints: MutableStateFlow<List<CallEndpoint>> =
+ MutableStateFlow(emptyList())
+
+ init {
+ if (!mIsCallAudioStateDeprecated) {
+ // bkwds compat functionality
+ mCallAudioState
+ .filterNotNull()
+ .combine(callData) { state, data ->
+ if (data.isNotEmpty()) {
+ mCurrentEndpoint.value = getCurrentEndpoint(state)
+ mEndpoints.value = createEndpointEntries(state)
+ mAvailableEndpoints.value = mEndpoints.value.map { it.endpoint }
+ } else {
+ mCurrentEndpoint.value = null
+ mEndpoints.value = emptyList()
+ mAvailableEndpoints.value = emptyList()
+ }
+ }
+ .launchIn(coroutineScope)
+ } else {
+ // UDC+ functionality
+ mAvailableCallEndpoints
+ .combine(callData) { endpoints, data ->
+ val availableEndpoints =
+ if (data.isNotEmpty()) {
+ endpoints.mapNotNull(::createCallAudioEndpoint)
+ } else {
+ emptyList()
+ }
+ mAvailableEndpoints.value = availableEndpoints
+ availableEndpoints
+ }
+ .combine(mCurrentCallEndpoint) { available, current ->
+ if (available.isEmpty()) {
+ mCurrentEndpoint.value = null
+ }
+ val audioEndpoint = current?.let { createCallAudioEndpoint(it) }
+ mCurrentEndpoint.value = available.firstOrNull { it.id == audioEndpoint?.id }
+ }
+ .launchIn(coroutineScope)
+ }
+ }
+
+ /** The audio state reported from the ICS has changed. */
+ fun onCallAudioStateChanged(audioState: CallAudioState?) {
+ if (mIsCallAudioStateDeprecated) return
+ mCallAudioState.value = audioState
+ }
+
+ /** The call endpoint reported from the ICS has changed. */
+ fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
+ if (!mIsCallAudioStateDeprecated) return
+ mCurrentCallEndpoint.value = callEndpoint
+ }
+
+ /** The available endpoints reported from the ICS have changed. */
+ fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) {
+ if (!mIsCallAudioStateDeprecated) return
+ mAvailableCallEndpoints.value = availableEndpoints
+ }
+
+ /**
+ * Request to change the audio route using the provided [CallAudioEndpoint.id].
+ *
+ * @return true if the operation succeeded, false if it did not because the endpoint doesn't
+ * exist.
+ */
+ suspend fun onChangeAudioRoute(id: String): Boolean {
+ if (mIsCallAudioStateDeprecated) {
+ val endpoint =
+ mAvailableCallEndpoints.value.firstOrNull { it.identifier.toString() == id }
+ if (endpoint == null) return false
+ return coroutineScope.async { onRequestEndpointChange(endpoint) }.await()
+ } else {
+ val endpoint = mEndpoints.value.firstOrNull { it.endpoint.id == id }
+ if (endpoint == null) return false
+ if (endpoint.endpoint.audioRoute != AudioRoute.BLUETOOTH) {
+ onChangeAudioRoute(getAudioState(endpoint.endpoint.audioRoute))
+ return true
+ } else {
+ if (endpoint.device == null) return false
+ onRequestBluetoothAudio(endpoint.device)
+ return true
+ }
+ }
+ }
+
+ /** Send a request to the InCallService to change the current endpoint. */
+ private suspend fun onRequestEndpointChange(endpoint: CallEndpoint): Boolean =
+ suspendCancellableCoroutine { continuation ->
+ onRequestEndpointChange(
+ endpoint,
+ Runnable::run,
+ @RequiresApi(Build.VERSION_CODES.S)
+ object : OutcomeReceiver<Void, CallEndpointException> {
+ override fun onResult(result: Void?) {
+ continuation.resume(true)
+ }
+
+ override fun onError(error: CallEndpointException) {
+ continuation.resume(false)
+ }
+ }
+ )
+ }
+
+ /** Maps from the Telecom [CallAudioState] to the app's [CallAudioEndpoint] */
+ private fun getCurrentEndpoint(callAudioState: CallAudioState): CallAudioEndpoint {
+ if (CallAudioState.ROUTE_BLUETOOTH != callAudioState.route) {
+ return CallAudioEndpoint(
+ id = getAudioEndpointId(callAudioState.route),
+ audioRoute = getAudioEndpointRoute(callAudioState.route)
+ )
+ }
+ val device: BluetoothDevice? = callAudioState.activeBluetoothDevice
+ if (device?.address != null) {
+ return CallAudioEndpoint(
+ id = device.address,
+ audioRoute = AudioRoute.BLUETOOTH,
+ frameworkName = getName(device)
+ )
+ }
+ val exactMatch = mEndpoints.value.firstOrNull { it.device == device }
+ if (exactMatch != null) return exactMatch.endpoint
+ return CallAudioEndpoint(
+ id = "",
+ audioRoute = AudioRoute.BLUETOOTH,
+ frameworkName = device?.let { getName(it) }
+ )
+ }
+
+ /** Create the [CallAudioEndpoint] from the telecom [CallEndpoint] for API UDC+ */
+ private fun createCallAudioEndpoint(endpoint: CallEndpoint): CallAudioEndpoint? {
+ val id = Compatibility.getEndpointIdentifier(endpoint) ?: return null
+ val type = Compatibility.getEndpointType(endpoint) ?: return null
+ val name = Compatibility.getEndpointName(endpoint) ?: return null
+ return CallAudioEndpoint(id, getAudioRouteFromEndpointType(type), name)
+ }
+
+ /** Reconstruct the available audio routes from telecom state and construct [EndpointEntry]s */
+ private fun createEndpointEntries(callAudioState: CallAudioState): List<EndpointEntry> {
+ return buildList {
+ if (CallAudioState.ROUTE_EARPIECE and callAudioState.supportedRouteMask > 0) {
+ add(
+ EndpointEntry(
+ CallAudioEndpoint(
+ id = getAudioEndpointId(CallAudioState.ROUTE_EARPIECE),
+ audioRoute = AudioRoute.EARPIECE
+ )
+ )
+ )
+ }
+ if (CallAudioState.ROUTE_SPEAKER and callAudioState.supportedRouteMask > 0) {
+ add(
+ EndpointEntry(
+ CallAudioEndpoint(
+ id = getAudioEndpointId(CallAudioState.ROUTE_SPEAKER),
+ audioRoute = AudioRoute.SPEAKER
+ )
+ )
+ )
+ }
+ if (CallAudioState.ROUTE_WIRED_HEADSET and callAudioState.supportedRouteMask > 0) {
+ add(
+ EndpointEntry(
+ CallAudioEndpoint(
+ id = getAudioEndpointId(CallAudioState.ROUTE_WIRED_HEADSET),
+ audioRoute = AudioRoute.HEADSET
+ )
+ )
+ )
+ }
+ if (CallAudioState.ROUTE_STREAMING and callAudioState.supportedRouteMask > 0) {
+ add(
+ EndpointEntry(
+ CallAudioEndpoint(
+ id = getAudioEndpointId(CallAudioState.ROUTE_STREAMING),
+ audioRoute = AudioRoute.STREAMING
+ )
+ )
+ )
+ }
+ // For Bluetooth, cache the BluetoothDevices associated with the route so we can choose
+ // them later
+ if (CallAudioState.ROUTE_BLUETOOTH and callAudioState.supportedRouteMask > 0) {
+ addAll(
+ callAudioState.supportedBluetoothDevices.map { device ->
+ EndpointEntry(
+ CallAudioEndpoint(
+ id = device.address?.toString() ?: UUID.randomUUID().toString(),
+ audioRoute = AudioRoute.BLUETOOTH,
+ frameworkName = getName(device)
+ ),
+ device
+ )
+ }
+ )
+ }
+ }
+ }
+
+ private fun getName(device: BluetoothDevice): String? {
+ var name = Compatibility.getBluetoothDeviceAlias(device)
+ if (name.isFailure) {
+ name = getBluetoothDeviceName(device)
+ }
+ return name.getOrDefault(null)
+ }
+
+ private fun getBluetoothDeviceName(device: BluetoothDevice): Result<String> {
+ return try {
+ Result.success(device.name ?: "")
+ } catch (e: SecurityException) {
+ Result.failure(e)
+ }
+ }
+
+ private fun getAudioEndpointId(audioState: Int): String {
+ return when (audioState) {
+ CallAudioState.ROUTE_EARPIECE -> "Earpiece"
+ CallAudioState.ROUTE_SPEAKER -> "Speaker"
+ CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
+ CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
+ CallAudioState.ROUTE_STREAMING -> "Streaming"
+ else -> "Unknown"
+ }
+ }
+
+ private fun getAudioRouteFromEndpointType(endpointType: Int): AudioRoute {
+ return when (endpointType) {
+ CallEndpoint.TYPE_EARPIECE -> AudioRoute.EARPIECE
+ CallEndpoint.TYPE_SPEAKER -> AudioRoute.SPEAKER
+ CallEndpoint.TYPE_WIRED_HEADSET -> AudioRoute.HEADSET
+ CallEndpoint.TYPE_BLUETOOTH -> AudioRoute.BLUETOOTH
+ CallEndpoint.TYPE_STREAMING -> AudioRoute.STREAMING
+ else -> {
+ AudioRoute.UNKNOWN
+ }
+ }
+ }
+
+ private fun getAudioEndpointRoute(audioState: Int): AudioRoute {
+ return when (audioState) {
+ CallAudioState.ROUTE_EARPIECE -> AudioRoute.EARPIECE
+ CallAudioState.ROUTE_SPEAKER -> AudioRoute.SPEAKER
+ CallAudioState.ROUTE_WIRED_HEADSET -> AudioRoute.HEADSET
+ CallAudioState.ROUTE_BLUETOOTH -> AudioRoute.BLUETOOTH
+ CallAudioState.ROUTE_STREAMING -> AudioRoute.STREAMING
+ else -> AudioRoute.UNKNOWN
+ }
+ }
+
+ private fun getAudioState(audioRoute: AudioRoute): Int {
+ return when (audioRoute) {
+ AudioRoute.EARPIECE -> CallAudioState.ROUTE_EARPIECE
+ AudioRoute.SPEAKER -> CallAudioState.ROUTE_SPEAKER
+ AudioRoute.HEADSET -> CallAudioState.ROUTE_WIRED_HEADSET
+ AudioRoute.BLUETOOTH -> CallAudioState.ROUTE_BLUETOOTH
+ AudioRoute.STREAMING -> CallAudioState.ROUTE_STREAMING
+ else -> CallAudioState.ROUTE_EARPIECE
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallData.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallData.kt
new file mode 100644
index 0000000..77ac06b
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallData.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.net.Uri
+import android.telecom.PhoneAccountHandle
+import androidx.core.telecom.extensions.KickParticipantAction
+import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.extensions.RaiseHandAction
+import androidx.core.telecom.test.ui.calling.CallStateTransition
+import androidx.core.telecom.util.ExperimentalAppActions
+
+enum class CallState {
+ INCOMING,
+ DIALING,
+ ACTIVE,
+ HELD,
+ DISCONNECTING,
+ DISCONNECTED,
+ UNKNOWN
+}
+
+enum class Direction {
+ INCOMING,
+ OUTGOING
+}
+
+enum class AudioRoute {
+ UNKNOWN,
+ EARPIECE,
+ SPEAKER,
+ BLUETOOTH,
+ HEADSET,
+ STREAMING
+}
+
+enum class CallType {
+ AUDIO,
+ VIDEO
+}
+
+enum class Capability {
+ SUPPORTS_HOLD
+}
+
+/** Base relevant call data */
+data class BaseCallData(
+ val id: Int,
+ val phoneAccountHandle: PhoneAccountHandle,
+ val name: String,
+ val contactName: String?,
+ val contactUri: Uri?,
+ val number: Uri,
+ val state: CallState,
+ val direction: Direction,
+ val callType: CallType,
+ val capabilities: List<Capability>,
+ val onStateChanged: (transition: CallStateTransition) -> Unit
+)
+
+/** Represents a call endpoint from the application's perspective */
+data class CallAudioEndpoint(
+ val id: String,
+ val audioRoute: AudioRoute,
+ val frameworkName: String? = null
+)
+
+/** data related to the extensions to the call */
+@OptIn(ExperimentalAppActions::class)
+data class ParticipantExtensionData(
+ val isSupported: Boolean,
+ val activeParticipant: Participant?,
+ val selfParticipant: Participant?,
+ val participants: Set<Participant>,
+ val raiseHandData: RaiseHandData? = null,
+ val kickParticipantData: KickParticipantData? = null
+)
+
+@OptIn(ExperimentalAppActions::class)
+data class RaiseHandData(val raisedHands: List<Participant>, val raiseHandAction: RaiseHandAction)
+
+@OptIn(ExperimentalAppActions::class)
+data class KickParticipantData(val kickParticipantAction: KickParticipantAction)
+
+/** Combined call data including extensions. */
+data class CallData(
+ val callData: BaseCallData,
+ val participantExtensionData: ParticipantExtensionData?
+)
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataAggregator.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataAggregator.kt
new file mode 100644
index 0000000..5d7318f
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataAggregator.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+
+/**
+ * Watches for [CallData] flow changes for each active call.
+ *
+ * Each call should call [watch] so this aggregator can track and listen to changes in its
+ * [CallData]. When there is a change in the [CallData] state for any call, regenerate all of the
+ * data in [callDataState]
+ */
+class CallDataAggregator {
+ private val mCallDataProducers: MutableStateFlow<List<StateFlow<CallData>>> =
+ MutableStateFlow(emptyList())
+ private val mCallDataState: MutableStateFlow<List<CallData>> = MutableStateFlow(emptyList())
+ /** Contains the current state of all active calls */
+ val callDataState: StateFlow<List<CallData>> = mCallDataState.asStateFlow()
+
+ /**
+ * Watch the [CallData] flow for changes related to a new call until the [scope] completes when
+ * the call ends.
+ */
+ suspend fun watch(scope: CoroutineScope, dataFlow: Flow<CallData>) {
+ val dataStateFlow = dataFlow.stateIn(scope)
+ mCallDataProducers.update { oldList -> ArrayList(oldList).apply { add(dataStateFlow) } }
+ dataStateFlow
+ .onEach { onCallDataUpdated() }
+ .onCompletion {
+ mCallDataProducers.update { oldList ->
+ ArrayList(oldList).apply { remove(dataStateFlow) }
+ }
+ onCallDataUpdated()
+ }
+ .launchIn(scope)
+ }
+
+ private fun onCallDataUpdated() {
+ mCallDataState.value = mCallDataProducers.value.map { it.value }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataEmitters.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataEmitters.kt
new file mode 100644
index 0000000..cc65745
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/CallDataEmitters.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.telecom.Call
+import android.telecom.TelecomManager
+import android.telecom.VideoProfile
+import android.util.Log
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.CallException.Companion.ERROR_CALL_IS_NOT_BEING_TRACKED
+import androidx.core.telecom.extensions.KickParticipantAction
+import androidx.core.telecom.extensions.Participant
+import androidx.core.telecom.extensions.RaiseHandAction
+import androidx.core.telecom.test.Compatibility
+import androidx.core.telecom.test.ui.calling.CallStateTransition
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Track the kick participant support for this application */
+@OptIn(ExperimentalAppActions::class)
+class KickParticipantDataEmitter {
+ companion object {
+ private val unsupportedAction =
+ object : KickParticipantAction {
+ override var isSupported: Boolean = false
+
+ override suspend fun requestKickParticipant(
+ participant: Participant
+ ): CallControlResult {
+ return CallControlResult.Error(ERROR_CALL_IS_NOT_BEING_TRACKED)
+ }
+ }
+ /** Implementation used when kicking participants is unsupported */
+ val UNSUPPORTED = KickParticipantDataEmitter().collect(unsupportedAction)
+ }
+
+ /** Collect updates to [KickParticipantData] related to the call */
+ fun collect(action: KickParticipantAction): Flow<KickParticipantData> {
+ return flowOf(createKickParticipantData(action))
+ }
+
+ private fun createKickParticipantData(action: KickParticipantAction): KickParticipantData {
+ return KickParticipantData(action)
+ }
+}
+
+/** Track the raised hands state of participants in the call */
+@OptIn(ExperimentalAppActions::class)
+class RaiseHandDataEmitter {
+ companion object {
+ private val unsupportedAction =
+ object : RaiseHandAction {
+ override var isSupported: Boolean = false
+
+ override suspend fun requestRaisedHandStateChange(
+ isRaised: Boolean
+ ): CallControlResult {
+ return CallControlResult.Error(ERROR_CALL_IS_NOT_BEING_TRACKED)
+ }
+ }
+ /** The implementation used when not supported */
+ val UNSUPPORTED = RaiseHandDataEmitter().collect(unsupportedAction)
+ }
+
+ private val raisedHands: MutableStateFlow<List<Participant>> = MutableStateFlow(emptyList())
+
+ /** The raised hands state of the participants has changed */
+ fun onRaisedHandsChanged(newRaisedHands: List<Participant>) {
+ raisedHands.value = newRaisedHands
+ }
+
+ /** Collect updates to the [RaiseHandData] related to this call */
+ fun collect(action: RaiseHandAction): Flow<RaiseHandData> {
+ return raisedHands.map { raisedHands -> createRaiseHandData(action, raisedHands) }
+ }
+
+ private fun createRaiseHandData(
+ action: RaiseHandAction,
+ raisedHands: List<Participant>
+ ): RaiseHandData {
+ return RaiseHandData(raisedHands, action)
+ }
+}
+
+/**
+ * Track and update listeners when the [ParticipantExtensionData] related to a call changes,
+ * including the optional raise hand and kick participant extensions.
+ */
+@OptIn(ExperimentalAppActions::class)
+class ParticipantExtensionDataEmitter {
+ private val activeParticipant: MutableStateFlow<Participant?> = MutableStateFlow(null)
+ private val participants: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet())
+
+ /** The participants in the call have changed */
+ fun onParticipantsChanged(newParticipants: Set<Participant>) {
+ participants.value = newParticipants
+ }
+
+ /** The active participant in the call has changed */
+ fun onActiveParticipantChanged(participant: Participant?) {
+ activeParticipant.value = participant
+ }
+
+ /**
+ * Collect updates to the [ParticipantExtensionData] related to this call based on the support
+ * state of this extension + actions
+ */
+ fun collect(
+ isSupported: Boolean,
+ raiseHandDataEmitter: Flow<RaiseHandData> = RaiseHandDataEmitter.UNSUPPORTED,
+ kickParticipantDataEmitter: Flow<KickParticipantData> =
+ KickParticipantDataEmitter.UNSUPPORTED
+ ): Flow<ParticipantExtensionData> {
+ return participants
+ .combine(activeParticipant) { newParticipants, newActiveParticipant ->
+ createExtensionData(isSupported, newActiveParticipant, newParticipants)
+ }
+ .combine(raiseHandDataEmitter) { data, rhData ->
+ ParticipantExtensionData(
+ isSupported = data.isSupported,
+ activeParticipant = data.activeParticipant,
+ selfParticipant = data.selfParticipant,
+ participants = data.participants,
+ raiseHandData = rhData,
+ kickParticipantData = data.kickParticipantData
+ )
+ }
+ .combine(kickParticipantDataEmitter) { data, kpData ->
+ ParticipantExtensionData(
+ isSupported = data.isSupported,
+ activeParticipant = data.activeParticipant,
+ selfParticipant = data.selfParticipant,
+ participants = data.participants,
+ raiseHandData = data.raiseHandData,
+ kickParticipantData = kpData
+ )
+ }
+ }
+
+ private fun createExtensionData(
+ isSupported: Boolean,
+ activeParticipant: Participant? = null,
+ participants: Set<Participant> = emptySet()
+ ): ParticipantExtensionData {
+ // For now, the first element is considered ourself
+ val self = participants.firstOrNull()
+ return ParticipantExtensionData(isSupported, activeParticipant, self, participants)
+ }
+}
+
+/**
+ * Track a [Call] and begin to stream [BaseCallData] using [collect] whenever the call data changes.
+ */
+class CallDataEmitter(val trackedCall: IcsCall) {
+ private companion object {
+ const val LOG_TAG = "CallDataProducer"
+ }
+
+ /** Collect on changes to the [BaseCallData] related to the [trackedCall] */
+ fun collect(): Flow<BaseCallData> {
+ return createCallDataFlow()
+ }
+
+ private fun createCallDataFlow(): Flow<BaseCallData> = callbackFlow {
+ val callback =
+ object : Call.Callback() {
+ override fun onStateChanged(call: Call?, state: Int) {
+ if (call != trackedCall.call) return
+ val callData = createCallData(trackedCall)
+ Log.v(LOG_TAG, "onStateChanged: call ${trackedCall.id}: $callData")
+ trySendBlocking(callData)
+ }
+
+ override fun onDetailsChanged(call: Call?, details: Call.Details?) {
+ if (call != trackedCall.call) return
+ val callData = createCallData(trackedCall)
+ Log.v(LOG_TAG, "onDetailsChanged: call ${trackedCall.id}: $callData")
+ trySendBlocking(callData)
+ }
+
+ override fun onCallDestroyed(call: Call?) {
+ if (call != trackedCall.call) return
+ Log.v(LOG_TAG, "call ${trackedCall.id}: destroyed")
+ channel.close()
+ }
+ }
+ if (trackedCall.call.details != null) {
+ val callData = createCallData(trackedCall)
+ Log.v(LOG_TAG, "call ${trackedCall.id}: $callData")
+ trySendBlocking(callData)
+ }
+ trackedCall.call.registerCallback(callback)
+ awaitClose { trackedCall.call.unregisterCallback(callback) }
+ }
+
+ private fun createCallData(icsCall: IcsCall): BaseCallData {
+ return BaseCallData(
+ id = icsCall.id,
+ phoneAccountHandle = icsCall.call.details.accountHandle,
+ name =
+ when (icsCall.call.details.callerDisplayNamePresentation) {
+ TelecomManager.PRESENTATION_ALLOWED ->
+ icsCall.call.details.callerDisplayName ?: ""
+ TelecomManager.PRESENTATION_RESTRICTED -> "Restricted"
+ TelecomManager.PRESENTATION_UNKNOWN -> "Unknown"
+ else -> icsCall.call.details.callerDisplayName ?: ""
+ },
+ contactName = Compatibility.getContactDisplayName(icsCall.call.details),
+ contactUri = Compatibility.getContactPhotoUri(icsCall.call.details),
+ number = icsCall.call.details.handle,
+ state = getState(Compatibility.getCallState(icsCall.call)),
+ direction =
+ when (icsCall.call.details.callDirection) {
+ Call.Details.DIRECTION_INCOMING -> Direction.INCOMING
+ else -> Direction.OUTGOING
+ },
+ callType =
+ when (VideoProfile.isVideo(icsCall.call.details.videoState)) {
+ true -> CallType.VIDEO
+ false -> CallType.AUDIO
+ },
+ capabilities = getCapabilities(icsCall.call.details.callCapabilities),
+ onStateChanged = ::onChangeCallState
+ )
+ }
+
+ private fun onChangeCallState(transition: CallStateTransition) {
+ when (transition) {
+ CallStateTransition.HOLD -> trackedCall.call.hold()
+ CallStateTransition.UNHOLD -> trackedCall.call.unhold()
+ CallStateTransition.ANSWER -> trackedCall.call.answer(VideoProfile.STATE_AUDIO_ONLY)
+ CallStateTransition.DISCONNECT -> trackedCall.call.disconnect()
+ CallStateTransition.NONE -> {}
+ }
+ }
+
+ private fun getState(telecomState: Int): CallState {
+ return when (telecomState) {
+ Call.STATE_RINGING -> CallState.INCOMING
+ Call.STATE_DIALING -> CallState.DIALING
+ Call.STATE_ACTIVE -> CallState.ACTIVE
+ Call.STATE_HOLDING -> CallState.HELD
+ Call.STATE_DISCONNECTING -> CallState.DISCONNECTING
+ Call.STATE_DISCONNECTED -> CallState.DISCONNECTED
+ else -> CallState.UNKNOWN
+ }
+ }
+
+ private fun getCapabilities(capabilities: Int): List<Capability> {
+ val capabilitiesList = ArrayList<Capability>()
+ if (canHold(capabilities)) {
+ capabilitiesList.add(Capability.SUPPORTS_HOLD)
+ }
+ return capabilitiesList
+ }
+
+ private fun canHold(capabilities: Int): Boolean {
+ return (Call.Details.CAPABILITY_HOLD and capabilities) > 0
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/IcsCall.kt
similarity index 73%
rename from compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt
rename to core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/IcsCall.kt
index 263e22e..915dc7d 100644
--- a/compose/foundation/foundation-layout/src/commonStubsMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.commonStubs.kt
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/IcsCall.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation.layout
+package androidx.core.telecom.test.services
-@Suppress("NOTHING_TO_INLINE")
-internal actual inline fun initCause(
- exception: IllegalArgumentException,
- cause: Exception
-): Throwable = implementedInJetBrainsFork()
+import android.telecom.Call
+
+/** Contains the [Call] and an app specific ID that relates to this call. */
+class IcsCall(val id: Int, val call: Call)
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/InCallServiceImpl.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/InCallServiceImpl.kt
new file mode 100644
index 0000000..430b8b8
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/InCallServiceImpl.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import android.telecom.Call
+import android.telecom.CallAudioState
+import android.telecom.CallEndpoint
+import android.util.Log
+import androidx.core.telecom.InCallServiceCompat
+import androidx.core.telecom.test.Compatibility
+import androidx.core.telecom.util.ExperimentalAppActions
+import androidx.lifecycle.lifecycleScope
+import java.util.concurrent.atomic.AtomicInteger
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/**
+ * Implements the InCallService for this application as well as a local ICS binder for activities to
+ * bind to this service locally and receive state changes.
+ */
+class InCallServiceImpl : LocalIcsBinder, InCallServiceCompat() {
+ private companion object {
+ const val LOG_TAG = "InCallServiceImpl"
+ }
+
+ private val localBinder =
+ object : LocalIcsBinder.Connector, Binder() {
+ override fun getService(): LocalIcsBinder {
+ return this@InCallServiceImpl
+ }
+ }
+
+ private val currId = AtomicInteger(1)
+ private val mCallDataAggregator = CallDataAggregator()
+ override val callData: StateFlow<List<CallData>> = mCallDataAggregator.callDataState
+ private val mMuteStateResolver = MuteStateResolver()
+
+ @Suppress("DEPRECATION")
+ private val mCallAudioRouteResolver =
+ CallAudioRouteResolver(
+ lifecycleScope,
+ callData,
+ ::setAudioRoute,
+ ::requestBluetoothAudio,
+ onRequestEndpointChange = { ep, e, or ->
+ Compatibility.requestCallEndpointChange(this@InCallServiceImpl, ep, e, or)
+ }
+ )
+ override val isMuted: StateFlow<Boolean> = mMuteStateResolver.muteState
+ override val currentAudioEndpoint: StateFlow<CallAudioEndpoint?> =
+ mCallAudioRouteResolver.currentEndpoint
+ override val availableAudioEndpoints: StateFlow<List<CallAudioEndpoint>> =
+ mCallAudioRouteResolver.availableEndpoints
+
+ override fun onBind(intent: Intent?): IBinder? {
+ if (intent == null) {
+ Log.w(LOG_TAG, "onBind: null intent, returning")
+ return null
+ }
+ if (SERVICE_INTERFACE == intent.action) {
+ Log.d(LOG_TAG, "onBind: Received telecom interface.")
+ return super.onBind(intent)
+ }
+ Log.d(LOG_TAG, "onBind: Received bind request from ${intent.`package`}")
+ return localBinder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ Log.d(LOG_TAG, "onUnbind: Received unbind request from $intent")
+ // work around a stupid bug where InCallService assumes that the unbind request can only
+ // come from telecom
+ if (intent?.action != null) {
+ return super.onUnbind(intent)
+ }
+ return false
+ }
+
+ override fun onChangeMuteState(isMuted: Boolean) {
+ setMuted(isMuted)
+ }
+
+ override suspend fun onChangeAudioRoute(id: String) {
+ mCallAudioRouteResolver.onChangeAudioRoute(id)
+ }
+
+ @OptIn(ExperimentalAppActions::class)
+ override fun onCallAdded(call: Call?) {
+ if (call == null) return
+ var callJob: Job? = null
+ callJob =
+ lifecycleScope.launch {
+ connectExtensions(call) {
+ val participantsEmitter = ParticipantExtensionDataEmitter()
+ val participantExtension =
+ addParticipantExtension(
+ onActiveParticipantChanged =
+ participantsEmitter::onActiveParticipantChanged,
+ onParticipantsUpdated = participantsEmitter::onParticipantsChanged
+ )
+
+ val kickParticipantDataEmitter = KickParticipantDataEmitter()
+ val kickParticipantAction = participantExtension.addKickParticipantAction()
+
+ val raiseHandDataEmitter = RaiseHandDataEmitter()
+ val raiseHandAction =
+ participantExtension.addRaiseHandAction(
+ raiseHandDataEmitter::onRaisedHandsChanged
+ )
+ onConnected {
+ val callDataEmitter = CallDataEmitter(IcsCall(currId.getAndAdd(1), call))
+ val participantData =
+ participantsEmitter.collect(
+ participantExtension.isSupported,
+ raiseHandDataEmitter.collect(raiseHandAction),
+ kickParticipantDataEmitter.collect(kickParticipantAction)
+ )
+ val fullData =
+ callDataEmitter.collect().combine(participantData) { callData, partData
+ ->
+ CallData(callData, partData)
+ }
+ mCallDataAggregator.watch(this@launch, fullData)
+ }
+ }
+ callJob?.cancel("Call Disconnected")
+ Log.d(LOG_TAG, "onCallAdded: connectedExtensions complete")
+ }
+ }
+
+ @Deprecated("Deprecated in API 34")
+ override fun onCallAudioStateChanged(audioState: CallAudioState?) {
+ mMuteStateResolver.onCallAudioStateChanged(audioState)
+ mCallAudioRouteResolver.onCallAudioStateChanged(audioState)
+ }
+
+ override fun onMuteStateChanged(isMuted: Boolean) {
+ mMuteStateResolver.onMuteStateChanged(isMuted)
+ }
+
+ override fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
+ mCallAudioRouteResolver.onCallEndpointChanged(callEndpoint)
+ }
+
+ override fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) {
+ mCallAudioRouteResolver.onAvailableCallEndpointsChanged(availableEndpoints)
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/LocalIcsBinder.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/LocalIcsBinder.kt
new file mode 100644
index 0000000..89b66d7
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/LocalIcsBinder.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import kotlinx.coroutines.flow.StateFlow
+
+/** Local interface used to define the local connection between a component and this Service. */
+interface LocalIcsBinder {
+ /** Connector used during Service binding to capture the instance of this class */
+ interface Connector {
+ fun getService(): LocalIcsBinder
+ }
+
+ /** the state of active calls on this device */
+ val callData: StateFlow<List<CallData>>
+ /** The state of global mute on this device */
+ val isMuted: StateFlow<Boolean>
+ /** The current audio route that the active call is using */
+ val currentAudioEndpoint: StateFlow<CallAudioEndpoint?>
+ /** The available audio routes for the active call */
+ val availableAudioEndpoints: StateFlow<List<CallAudioEndpoint>>
+
+ /** Request to change the mute state of the device */
+ fun onChangeMuteState(isMuted: Boolean)
+
+ /** Request to change the current audio route on the device */
+ suspend fun onChangeAudioRoute(id: String)
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/MuteStateResolver.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/MuteStateResolver.kt
new file mode 100644
index 0000000..b08e7141
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/MuteStateResolver.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.os.Build
+import android.telecom.CallAudioState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Tracks the current global mute state of the device */
+class MuteStateResolver {
+ private val isCallAudioStateDeprecated =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ private val mMuteState = MutableStateFlow(false)
+ val muteState = mMuteState.asStateFlow()
+
+ /** The audio state of the device has changed for devices using API version < UDC */
+ fun onCallAudioStateChanged(audioState: CallAudioState?) {
+ if (audioState == null || isCallAudioStateDeprecated) return
+ mMuteState.value = audioState.isMuted
+ }
+
+ /** The audio state of the device has changed for devices using API version UDC+ */
+ fun onMuteStateChanged(isMuted: Boolean) {
+ if (!isCallAudioStateDeprecated) return
+ mMuteState.value = isMuted
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/RemoteCallProvider.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/RemoteCallProvider.kt
new file mode 100644
index 0000000..2b59c02
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/services/RemoteCallProvider.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.services
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Context.BIND_AUTO_CREATE
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.getAndUpdate
+
+data class LocalServiceConnection(
+ val isConnected: Boolean,
+ val context: Context? = null,
+ val serviceConnection: ServiceConnection? = null,
+ val connection: LocalIcsBinder? = null
+)
+
+/**
+ * Manages the connection for the Provider of "remote" calls, which are calls from the app's
+ * [InCallServiceImpl].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class RemoteCallProvider {
+ private companion object {
+ const val LOG_TAG = "RemoteCallProvider"
+ }
+
+ private val connectedService: MutableStateFlow<LocalServiceConnection> =
+ MutableStateFlow(LocalServiceConnection(false))
+
+ /** Bind to the app's [LocalIcsBinder.Connector] Service implementation */
+ fun connectService(context: Context) {
+ if (connectedService.value.isConnected) return
+ val intent = Intent(context, InCallServiceImpl::class.java)
+ val serviceConnection =
+ object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ if (service == null) return
+ val localService = service as LocalIcsBinder.Connector
+ connectedService.value =
+ LocalServiceConnection(true, context, this, localService.getService())
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ // Unlikely since the Service is in the same process. Re-evaluate if the service
+ // is moved to another process.
+ Log.w(LOG_TAG, "onServiceDisconnected: Unexpected call")
+ }
+ }
+ Log.i(LOG_TAG, "connectToIcs: Binding to ICS locally")
+ context.bindService(intent, serviceConnection, BIND_AUTO_CREATE)
+ }
+
+ /** Disconnect from the app;s [LocalIcsBinder.Connector] Service implementation */
+ fun disconnectService() {
+ val localConnection = connectedService.getAndUpdate { LocalServiceConnection(false) }
+ localConnection.serviceConnection?.let { conn ->
+ Log.i(LOG_TAG, "connectToIcs: Unbinding to ICS locally")
+ localConnection.context?.unbindService(conn)
+ }
+ }
+
+ /**
+ * Stream the [CallData] representing each active Call on the device. The Flow will be empty
+ * until the remote Service connects.
+ */
+ fun streamCallData(): Flow<List<CallData>> {
+ return connectedService.flatMapConcat { conn ->
+ if (!conn.isConnected) {
+ emptyFlow()
+ } else {
+ conn.connection?.callData ?: emptyFlow()
+ }
+ }
+ }
+
+ /**
+ * Stream the global mute state of the device. The Flow will be empty until the remote Service
+ * connects.
+ */
+ fun streamMuteData(): Flow<Boolean> {
+ return connectedService.flatMapConcat { conn ->
+ if (!conn.isConnected) {
+ emptyFlow()
+ } else {
+ conn.connection?.isMuted ?: emptyFlow()
+ }
+ }
+ }
+
+ /**
+ * Stream the [CallAudioEndpoint] representing the current endpoint of the active call. The Flow
+ * will be empty until the remote Service connects.
+ */
+ fun streamCurrentEndpointData(): Flow<CallAudioEndpoint?> {
+ return connectedService.flatMapConcat { conn ->
+ if (!conn.isConnected) {
+ emptyFlow()
+ } else {
+ conn.connection?.currentAudioEndpoint ?: emptyFlow()
+ }
+ }
+ }
+
+ /**
+ * Stream the List of [CallAudioEndpoint]s representing the available endpoints of the active
+ * call. The Flow will be empty until the remote Service connects.
+ */
+ fun streamAvailableEndpointData(): Flow<List<CallAudioEndpoint>> {
+ return connectedService.flatMapConcat { conn ->
+ if (!conn.isConnected) {
+ emptyFlow()
+ } else {
+ conn.connection?.availableAudioEndpoints ?: emptyFlow()
+ }
+ }
+ }
+
+ /** Request to change the global mute state of the device. */
+ fun onChangeMuteState(isMuted: Boolean) {
+ val service = connectedService.value
+ if (!service.isConnected) return
+ service.connection?.onChangeMuteState(isMuted)
+ }
+
+ /** Request to change the current audio route of the active call. */
+ suspend fun onChangeAudioRoute(id: String) {
+ val service = connectedService.value
+ if (!service.isConnected) return
+ service.connection?.onChangeAudioRoute(id)
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingActivity.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingActivity.kt
new file mode 100644
index 0000000..addc405
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingActivity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import android.app.role.RoleManager
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.core.telecom.test.ui.theme.AppTheme
+
+/** Main activity for this application, which sets the compose UI content. */
+class CallingActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val roleManager = applicationContext.getSystemService(RoleManager::class.java)
+ val isSupported = roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)
+ setContent { AppTheme { CallingApp(isSupported) } }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingApp.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingApp.kt
new file mode 100644
index 0000000..54629e3
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/CallingApp.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import android.Manifest
+import android.app.role.RoleManager
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.telecom.test.ui.calling.OngoingCallsViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
+
+/** Compose UI for the application, which handles navigation between screens */
+@Composable
+fun CallingApp(isSupported: Boolean) {
+ val navController = rememberNavController()
+ val context = LocalContext.current
+ val roleManager = context.getSystemService(RoleManager::class.java)
+ var isGranted by remember { mutableStateOf(roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) }
+ val roleIntent by remember {
+ mutableStateOf(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER))
+ }
+ var isBtPermRequestSuggested by remember {
+ mutableStateOf(
+ // Telecom handles getting the name for UDC+
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
+ context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) !=
+ PackageManager.PERMISSION_GRANTED
+ )
+ }
+ val btPermlauncher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
+ isBtGranted ->
+ isBtPermRequestSuggested = !isBtGranted
+ navController.launchAudioRouteDialog()
+ }
+
+ val ongoingCallsViewModel: OngoingCallsViewModel = viewModel()
+
+ val startRoute =
+ when (isSupported) {
+ true -> {
+ when (isGranted) {
+ true -> NavRoute.CALLS
+ false -> NavRoute.ROLE_REQUESTS
+ }
+ }
+ false -> NavRoute.NOT_SUPPORTED
+ }
+ // Following encapsulation guidelines from
+ // https://developer.android.com/guide/navigation/design/encapsulate
+ NavHost(navController, startDestination = startRoute) {
+ notSupportedDestination()
+ roleRequestsDestination(roleIntent) { isGranted = it }
+ callsDestination(
+ ongoingCallsViewModel,
+ onShowAudioRouting = {
+ if (isBtPermRequestSuggested) {
+ btPermlauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
+ } else {
+ navController.launchAudioRouteDialog()
+ }
+ },
+ onMoveToSettings = { navController.moveToSettingsDestination() }
+ )
+ audioRouteDialog(
+ ongoingCallsViewModel,
+ onDismissDialog = { navController.popBackStack() },
+ onChangeAudioRoute = { ongoingCallsViewModel.onChangeAudioRoute(it) }
+ )
+ settingsDestination()
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/Navigation.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/Navigation.kt
new file mode 100644
index 0000000..1c06915
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/Navigation.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import android.content.Intent
+import androidx.core.telecom.test.ui.calling.AudioRoutePickerDialog
+import androidx.core.telecom.test.ui.calling.CallsScreen
+import androidx.core.telecom.test.ui.calling.OngoingCallsViewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.dialog
+
+/* Routes defined by the navgraph */
+object NavRoute {
+ const val ROLE_REQUESTS = "RoleRequests"
+ const val CALLS = "Calls"
+ const val NOT_SUPPORTED = "NotSupported"
+ const val AUDIO_ROUTE_PICKER = "AudioRoutePicker"
+ const val SETTINGS = "Settings"
+}
+
+/** The screen used for devices that do not support this application */
+fun NavGraphBuilder.notSupportedDestination() {
+ composable(NavRoute.NOT_SUPPORTED) { UnsupportedDeviceScreen() }
+}
+
+/** The screen used for devices that have not set this application to the default dialer yet. */
+fun NavGraphBuilder.roleRequestsDestination(
+ roleIntent: Intent,
+ onGrantedStateChanged: (Boolean) -> Unit
+) {
+ composable(NavRoute.ROLE_REQUESTS) { RoleRequestScreen(roleIntent, onGrantedStateChanged) }
+}
+
+/** The main calling screen, which manages new and ongoing calls. */
+fun NavGraphBuilder.callsDestination(
+ ongoingCallsViewModel: OngoingCallsViewModel,
+ onShowAudioRouting: () -> Unit,
+ onMoveToSettings: () -> Unit
+) {
+ composable(NavRoute.CALLS) {
+ CallsScreen(
+ ongoingCallsViewModel = ongoingCallsViewModel,
+ onShowAudioRouting = onShowAudioRouting,
+ onMoveToSettings = onMoveToSettings
+ )
+ }
+}
+
+/**
+ * The audio routing dialog, which sits on top of the active screen and allows users to change the
+ * active audio route of the active call.
+ */
+fun NavGraphBuilder.audioRouteDialog(
+ ongoingCallsViewModel: OngoingCallsViewModel,
+ onDismissDialog: () -> Unit,
+ onChangeAudioRoute: suspend (String) -> Unit
+) {
+ dialog(NavRoute.AUDIO_ROUTE_PICKER) {
+ AudioRoutePickerDialog(ongoingCallsViewModel, onDismissDialog, onChangeAudioRoute)
+ }
+}
+
+/** Defines the screen used to control app settings. */
+fun NavGraphBuilder.settingsDestination() {
+ composable(NavRoute.SETTINGS) { SettingsScreen() }
+}
+
+/** Launch the audio routing dialog for the user. */
+fun NavController.launchAudioRouteDialog() {
+ navigate(route = NavRoute.AUDIO_ROUTE_PICKER)
+}
+
+/** Launch the settings screen. */
+fun NavController.moveToSettingsDestination() {
+ navigate(route = NavRoute.SETTINGS)
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/RoleRequestScreen.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/RoleRequestScreen.kt
new file mode 100644
index 0000000..4a2aa52
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/RoleRequestScreen.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+
+/**
+ * Screen that allows the user to request the dialer role for this application, which grants the
+ * permissions required for this application to run.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RoleRequestScreen(roleIntent: Intent, onGrantedStateChanged: (Boolean) -> Unit) {
+ val scope = rememberCoroutineScope()
+ val snackbarHostState = remember { SnackbarHostState() }
+ // Handles launching activities for result
+ val launcher =
+ rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) {
+ when (it.resultCode) {
+ RESULT_OK -> onGrantedStateChanged(true)
+ else -> {
+ scope.launch { snackbarHostState.showSnackbar("Role denied: Try again?") }
+ onGrantedStateChanged(false)
+ }
+ }
+ }
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ colors =
+ topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary
+ ),
+ title = { Text("Required Permissions Request") }
+ )
+ },
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+ ) { contentPadding ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(contentPadding).padding(12.dp).fillMaxHeight()
+ ) {
+ Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
+ Text(
+ text =
+ "This test app is required to be the default dialer to work." +
+ "Tap \"Request Permissions\" below and set this app as the default dialer" +
+ " to continue."
+ )
+ }
+ Column {
+ ElevatedButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { launcher.launch(roleIntent) }
+ ) {
+ Text("Request Permissions")
+ }
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/SettingsScreen.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/SettingsScreen.kt
new file mode 100644
index 0000000..0ce9066
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/SettingsScreen.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+/** The screen used to show the application settings. */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScreen() {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ colors =
+ topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary
+ ),
+ title = { Text("Settings") }
+ )
+ }
+ ) { scaffoldPadding ->
+ Column(modifier = Modifier.padding(scaffoldPadding)) { Text("<Nothing yet...>") }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/UnsupportedDeviceScreen.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/UnsupportedDeviceScreen.kt
new file mode 100644
index 0000000..e4119c1
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/UnsupportedDeviceScreen.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+/** Screen used to communicate to the user that the device does not support this application. */
+@Composable
+fun UnsupportedDeviceScreen() {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier.padding(12.dp).fillMaxSize()
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ text = "Unsupported Device"
+ )
+ Text(
+ modifier = Modifier.padding(10.dp).fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text =
+ "Unfortunately, this device doesn't support the dialer role so this app will " +
+ "not work."
+ )
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioEndpointUiState.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioEndpointUiState.kt
new file mode 100644
index 0000000..c13d318
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioEndpointUiState.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.core.telecom.test.services.AudioRoute
+
+/** Sets up some previews for UI components using the [AudioEndpointUiState]. */
+class UserPreviewEndpointProvider : PreviewParameterProvider<AudioEndpointUiState> {
+ override val values =
+ sequenceOf(
+ AudioEndpointUiState(id = "A", name = "Earpiece", audioRoute = AudioRoute.EARPIECE),
+ AudioEndpointUiState(id = "B", name = "Speaker", audioRoute = AudioRoute.SPEAKER),
+ AudioEndpointUiState(id = "C", name = "Bluetooth", audioRoute = AudioRoute.BLUETOOTH),
+ )
+}
+
+data class AudioEndpointUiState(val id: String, val name: String, val audioRoute: AudioRoute)
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioRouteDialog.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioRouteDialog.kt
new file mode 100644
index 0000000..947a732
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/AudioRouteDialog.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.Wallpapers
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.core.telecom.test.R
+import androidx.core.telecom.test.services.AudioRoute
+import androidx.core.telecom.test.ui.calling.OngoingCallsViewModel.Companion.UnknownAudioUiState
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.launch
+
+/**
+ * The dialog that pops up on the screen when the user tries to change the audio route of the
+ * device.
+ */
+@Composable
+fun AudioRoutePickerDialog(
+ ongoingCallsViewModel: OngoingCallsViewModel,
+ onDismissDialog: () -> Unit,
+ onChangeAudioRoute: suspend (String) -> Unit
+) {
+ val currentAudioRoute: AudioEndpointUiState by
+ ongoingCallsViewModel
+ .streamCurrentEndpointAudioData()
+ .collectAsStateWithLifecycle(UnknownAudioUiState)
+ val availableAudioRoutes: List<AudioEndpointUiState> by
+ ongoingCallsViewModel
+ .streamAvailableEndpointAudioData()
+ .collectAsStateWithLifecycle(emptyList())
+ Dialog(onDismissRequest = onDismissDialog) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors =
+ CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
+ ) {
+ Column(modifier = Modifier.padding(6.dp)) {
+ Text("Current Audio Route")
+ Spacer(modifier = Modifier.padding(vertical = 3.dp))
+ OutlinedCard { AudioRouteContent(currentAudioRoute) }
+ HorizontalDivider(modifier = Modifier.padding(vertical = 6.dp))
+ Text("Available Audio Routes")
+ val available = availableAudioRoutes.filter { it.id != currentAudioRoute.id }
+ if (available.isEmpty()) {
+ Text(modifier = Modifier.padding(6.dp), text = "<None Available>")
+ } else {
+ available.forEach { route ->
+ ClickableAudioRouteContent(route, onChangeAudioRoute)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true, wallpaper = Wallpapers.BLUE_DOMINATED_EXAMPLE)
+@Composable
+fun ClickableAudioRouteContent(
+ @PreviewParameter(UserPreviewEndpointProvider::class) audioRoute: AudioEndpointUiState,
+ onChangeAudioRoute: suspend (String) -> Unit = {}
+) {
+ val coroutineScope = rememberCoroutineScope()
+ var isLoading: Boolean by remember { mutableStateOf(false) }
+ ElevatedCard(
+ enabled = !isLoading,
+ onClick = {
+ coroutineScope.launch {
+ isLoading = true
+ onChangeAudioRoute(audioRoute.id)
+ isLoading = false
+ }
+ }
+ ) {
+ AudioRouteContent(audioRoute)
+ }
+}
+
+@Composable
+fun AudioRouteContent(audioRoute: AudioEndpointUiState) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(getResourceForAudioRoute(audioRoute.audioRoute)),
+ contentDescription = "audio route details"
+ )
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp))
+ Text(audioRoute.name)
+ }
+}
+
+fun getResourceForAudioRoute(audioRoute: AudioRoute): Int {
+ return when (audioRoute) {
+ AudioRoute.UNKNOWN -> R.drawable.phone_in_talk_24px
+ AudioRoute.EARPIECE -> R.drawable.phone_in_talk_24px
+ AudioRoute.SPEAKER -> R.drawable.speaker_phone_24px
+ AudioRoute.BLUETOOTH -> R.drawable.bluetooth_24px
+ AudioRoute.HEADSET -> R.drawable.headset_mic_24px
+ AudioRoute.STREAMING -> R.drawable.cast_24px
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallUiState.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallUiState.kt
new file mode 100644
index 0000000..23c3da2
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallUiState.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import android.net.Uri
+import androidx.core.telecom.test.services.CallState
+import androidx.core.telecom.test.services.CallType
+import androidx.core.telecom.test.services.Direction
+
+/** Defines valid call state transitions */
+enum class CallStateTransition {
+ ANSWER,
+ HOLD,
+ UNHOLD,
+ NONE,
+ DISCONNECT
+}
+
+/** UI state and callback container for a Call */
+data class CallUiState(
+ val id: Int,
+ val name: String,
+ val photo: Uri?,
+ val number: String,
+ val state: CallState,
+ val validTransition: CallStateTransition,
+ val direction: Direction,
+ val callType: CallType,
+ val onStateChanged: (transition: CallStateTransition) -> Unit,
+ val participantUiState: ParticipantExtensionUiState?
+)
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallsScreen.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallsScreen.kt
new file mode 100644
index 0000000..c316c6a
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/CallsScreen.kt
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.net.Uri
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Face
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedIconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.Wallpapers
+import androidx.compose.ui.unit.dp
+import androidx.core.telecom.test.R
+import androidx.core.telecom.test.services.AudioRoute
+import androidx.core.telecom.test.services.CallState
+import androidx.core.telecom.test.services.CallType
+import androidx.core.telecom.test.services.Direction
+import androidx.core.telecom.test.ui.calling.OngoingCallsViewModel.Companion.UnknownAudioUiState
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+/**
+ * The main screen of the application, which allows the user to view and manage ongoing calls on the
+ * device.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CallsScreen(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ context: Context = LocalContext.current,
+ ongoingCallsViewModel: OngoingCallsViewModel,
+ onShowAudioRouting: () -> Unit,
+ onMoveToSettings: () -> Unit
+) {
+ DisposableEffect(lifecycleOwner, context) {
+ ongoingCallsViewModel.connectService(context)
+ // When the effect leaves the Composition, teardown
+ onDispose { ongoingCallsViewModel.disconnectService() }
+ }
+ val callDataState: List<CallUiState> by
+ ongoingCallsViewModel
+ .streamCallData(LocalContext.current)
+ .collectAsStateWithLifecycle(emptyList())
+ val isMuted: Boolean by
+ ongoingCallsViewModel.streamMuteData().collectAsStateWithLifecycle(false)
+ val currentAudioRoute: AudioEndpointUiState by
+ ongoingCallsViewModel
+ .streamCurrentEndpointAudioData()
+ .collectAsStateWithLifecycle(UnknownAudioUiState)
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ colors =
+ topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary
+ ),
+ title = { Text("Ongoing Calls") },
+ actions = {
+ IconButton(onClick = onMoveToSettings) {
+ Icon(imageVector = Icons.Rounded.Settings, contentDescription = "Settings ")
+ }
+ }
+ )
+ }
+ ) { scaffoldPadding ->
+ ServiceConnectedCallContent(
+ modifier = Modifier.padding(scaffoldPadding),
+ callDataState,
+ isMuted,
+ currentAudioRoute,
+ onChangeMuteState = ongoingCallsViewModel::onChangeMuteState,
+ onShowAudioRouteDialog = onShowAudioRouting
+ )
+ }
+}
+
+@Composable
+fun ServiceConnectedCallContent(
+ modifier: Modifier = Modifier,
+ calls: List<CallUiState>,
+ isMuted: Boolean,
+ currentAudioRoute: AudioEndpointUiState,
+ onChangeMuteState: (Boolean) -> Unit,
+ onShowAudioRouteDialog: () -> Unit,
+) {
+ Column(modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
+ if (calls.isNotEmpty()) {
+ DeviceStatusCard(
+ isMuted,
+ currentAudioRoute,
+ onShowAudioRouteDialog = onShowAudioRouteDialog,
+ onMuteStateChange = onChangeMuteState
+ )
+ }
+ calls.forEach { caller -> CallCard(caller = caller) }
+ }
+}
+
+@Preview(showBackground = true, wallpaper = Wallpapers.BLUE_DOMINATED_EXAMPLE)
+@Composable
+fun CallerCard(
+ modifier: Modifier = Modifier,
+ isExpanded: Boolean = false,
+ name: String = "Abraham Lincoln",
+ number: String = "555-1212",
+ photo: Uri? = null,
+ direction: Direction = Direction.INCOMING,
+ callType: CallType = CallType.AUDIO,
+ callState: CallState = CallState.UNKNOWN
+) {
+ Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
+ if (photo == null) {
+ Icon(
+ Icons.Rounded.Face,
+ modifier = Modifier.size(48.dp),
+ contentDescription = "Caller Icon"
+ )
+ } else {
+ val context = LocalContext.current
+ val bitmap: Bitmap by remember {
+ mutableStateOf(
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(context.contentResolver, photo)
+ )
+ )
+ }
+ Image(
+ modifier = Modifier.size(48.dp).clip(CircleShape),
+ painter = BitmapPainter(bitmap.asImageBitmap()),
+ contentDescription = "Caller Icon"
+ )
+ }
+ Column(modifier = Modifier.padding(6.dp)) {
+ if (name.isNotEmpty()) {
+ Text(text = name)
+ }
+ Text(text = number)
+ if (isExpanded) {
+ Row(modifier = Modifier.height(IntrinsicSize.Min)) {
+ Text(
+ text =
+ when (callType) {
+ CallType.AUDIO -> "Audio"
+ CallType.VIDEO -> "Video"
+ }
+ )
+ VerticalDivider(modifier = Modifier.padding(horizontal = 6.dp))
+ Text(
+ text =
+ when (direction) {
+ Direction.INCOMING -> "Incoming"
+ Direction.OUTGOING -> "Outgoing"
+ }
+ )
+ VerticalDivider(modifier = Modifier.padding(horizontal = 6.dp))
+ Text(
+ text =
+ when (callState) {
+ CallState.UNKNOWN -> "Unknown"
+ CallState.INCOMING -> "Incoming"
+ CallState.DIALING -> "Dialing"
+ CallState.ACTIVE -> "Active"
+ CallState.HELD -> "Held"
+ CallState.DISCONNECTING -> "Disconnecting"
+ CallState.DISCONNECTED -> "Disconnected"
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DeviceStatusCard(
+ isMuted: Boolean = false,
+ currentAudioRoute: AudioEndpointUiState,
+ onMuteStateChange: (Boolean) -> Unit,
+ onShowAudioRouteDialog: () -> Unit,
+) {
+ ElevatedCard(
+ colors =
+ CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ modifier = Modifier.fillMaxWidth().padding(6.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(6.dp),
+ ) {
+ Text("Device State")
+ HorizontalDivider(modifier = Modifier.padding(vertical = 6.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ OutlinedIconButton(onClick = { onMuteStateChange(!isMuted) }) {
+ if (!isMuted) {
+ Icon(
+ painter = painterResource(R.drawable.mic),
+ contentDescription = "device unmuted"
+ )
+ } else {
+ Icon(
+ painter = painterResource(R.drawable.mic_off_24px),
+ contentDescription = "device muted"
+ )
+ }
+ }
+ OutlinedIconButton(
+ enabled = currentAudioRoute.audioRoute != AudioRoute.UNKNOWN,
+ onClick = onShowAudioRouteDialog
+ ) {
+ Icon(
+ painter =
+ painterResource(getResourceForAudioRoute(currentAudioRoute.audioRoute)),
+ contentDescription = "current audio route"
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun CallCard(caller: CallUiState, defaultExpandedState: Boolean = false) {
+ var isExpanded by remember { mutableStateOf(defaultExpandedState) }
+ val expandedColor =
+ when (isExpanded) {
+ true -> MaterialTheme.colorScheme.surfaceContainerHigh
+ false -> MaterialTheme.colorScheme.surfaceContainerLow
+ }
+ val padding =
+ when (isExpanded) {
+ true -> 6.dp
+ false -> 12.dp
+ }
+ ElevatedCard(
+ colors = CardDefaults.cardColors(containerColor = expandedColor),
+ modifier =
+ Modifier.animateContentSize()
+ .height(IntrinsicSize.Min)
+ .fillMaxWidth()
+ .padding(padding)
+ .clickable { isExpanded = !isExpanded }
+ ) {
+ Column {
+ Column(modifier = Modifier.padding(6.dp)) {
+ CallerCard(
+ isExpanded = isExpanded,
+ name = caller.name,
+ number = caller.number,
+ photo = caller.photo,
+ direction = caller.direction,
+ callType = caller.callType,
+ callState = caller.state
+ )
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
+ val isCallTransitionPossible =
+ caller.validTransition != CallStateTransition.NONE &&
+ caller.validTransition != CallStateTransition.DISCONNECT
+ if (isCallTransitionPossible) {
+ OutlinedButton(
+ onClick = { caller.onStateChanged(caller.validTransition) },
+ ) {
+ val stateTransitionText =
+ when (caller.validTransition) {
+ CallStateTransition.UNHOLD -> "Unhold"
+ CallStateTransition.HOLD -> "Hold"
+ CallStateTransition.ANSWER -> "Answer"
+ CallStateTransition.NONE -> "None"
+ CallStateTransition.DISCONNECT -> "Disconnect"
+ }
+ Text(text = stateTransitionText)
+ }
+ }
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp))
+ OutlinedButton(
+ onClick = { caller.onStateChanged(CallStateTransition.DISCONNECT) },
+ ) {
+ val disconnectText =
+ if (caller.state == CallState.INCOMING) {
+ "Reject"
+ } else {
+ "Hangup"
+ }
+ Text(disconnectText)
+ }
+ }
+ }
+ AnimatedVisibility(isExpanded) {
+ Column {
+ HorizontalDivider(modifier = Modifier.padding(vertical = 6.dp))
+ if (caller.participantUiState == null) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(6.dp),
+ text = "<No Extensions supported>"
+ )
+ } else {
+ ExtensionsContent(participantUiState = caller.participantUiState)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ExtensionsContent.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ExtensionsContent.kt
new file mode 100644
index 0000000..fbc12bf
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ExtensionsContent.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Face
+import androidx.compose.material3.Button
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.Wallpapers
+import androidx.compose.ui.unit.dp
+import androidx.core.telecom.test.R
+import kotlinx.coroutines.launch
+
+@Preview(showBackground = true, wallpaper = Wallpapers.BLUE_DOMINATED_EXAMPLE)
+@Composable
+fun ExtensionsContent(
+ @PreviewParameter(ParticipantExtensionProvider::class)
+ participantUiState: ParticipantExtensionUiState
+) {
+ Column(modifier = Modifier.fillMaxWidth().padding(6.dp)) {
+ Text("Participants")
+ if (participantUiState.participants.isEmpty()) {
+ Text(modifier = Modifier.padding(horizontal = 6.dp), text = "<No Participants>")
+ } else {
+ Column(
+ modifier =
+ Modifier.height(150.dp)
+ .fillMaxWidth()
+ .padding(6.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ participantUiState.participants.forEach {
+ if (it.isActive) {
+ ActiveParticipantContent(
+ participantUiState.isKickParticipantSupported,
+ participantUiState.isRaiseHandSupported,
+ onRaiseHandStateChanged = participantUiState.onRaiseHandStateChanged,
+ it
+ )
+ } else {
+ NonActiveParticipantContent(
+ participantUiState.isKickParticipantSupported,
+ participantUiState.isRaiseHandSupported,
+ onRaiseHandStateChanged = participantUiState.onRaiseHandStateChanged,
+ it
+ )
+ }
+ Spacer(Modifier.padding(vertical = 6.dp))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun NonActiveParticipantContent(
+ isKickSupported: Boolean,
+ isRaiseHandSupported: Boolean,
+ onRaiseHandStateChanged: suspend (Boolean) -> Unit,
+ participant: ParticipantUiState
+) {
+ ElevatedCard {
+ ParticipantContent(
+ isKickSupported,
+ isRaiseHandSupported,
+ onRaiseHandStateChanged,
+ participant
+ )
+ }
+}
+
+@Composable
+fun ActiveParticipantContent(
+ isKickSupported: Boolean,
+ isRaiseHandSupported: Boolean,
+ onRaiseHandStateChanged: suspend (Boolean) -> Unit,
+ participant: ParticipantUiState
+) {
+ OutlinedCard(
+ border = BorderStroke(3.dp, Color.Black),
+ ) {
+ ParticipantContent(
+ isKickSupported,
+ isRaiseHandSupported,
+ onRaiseHandStateChanged,
+ participant
+ )
+ }
+}
+
+@Composable
+fun ParticipantContent(
+ isKickSupported: Boolean,
+ isRaiseHandSupported: Boolean,
+ onRaiseHandStateChanged: suspend (Boolean) -> Unit,
+ participant: ParticipantUiState
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val scope = rememberCoroutineScope()
+ Icon(
+ Icons.Rounded.Face,
+ modifier = Modifier.size(48.dp),
+ contentDescription = "Caller Icon"
+ )
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp))
+ Text(participant.name)
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp))
+ if (isRaiseHandSupported) {
+ if (participant.isHandRaised) {
+ Icon(
+ painter = painterResource(R.drawable.waving_hand_24px),
+ contentDescription = "hand raised"
+ )
+ }
+ }
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp).weight(1f))
+ if (participant.isSelf && isRaiseHandSupported) {
+ var isRaiseHandEnabled by remember { mutableStateOf(true) }
+ if (participant.isHandRaised) {
+ Button(
+ enabled = isRaiseHandEnabled,
+ onClick = {
+ scope.launch {
+ isRaiseHandEnabled = false
+ onRaiseHandStateChanged(false)
+ isRaiseHandEnabled = true
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.waving_hand_24px),
+ contentDescription = "lower hand request"
+ )
+ }
+ } else {
+ ElevatedButton(
+ enabled = isRaiseHandEnabled,
+ onClick = {
+ scope.launch {
+ isRaiseHandEnabled = false
+ onRaiseHandStateChanged(true)
+ isRaiseHandEnabled = true
+ }
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.waving_hand_24px),
+ contentDescription = "raise hand request"
+ )
+ }
+ }
+ }
+
+ if (!participant.isSelf && isKickSupported) {
+ Spacer(modifier = Modifier.padding(horizontal = 6.dp))
+ var isKickEnabled by remember { mutableStateOf(true) }
+ ElevatedButton(
+ enabled = isKickEnabled,
+ onClick = {
+ scope.launch {
+ isKickEnabled = false
+ participant.onKickParticipant()
+ isKickEnabled = true
+ }
+ }
+ ) {
+ Text("Kick")
+ }
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/OngoingCallsViewModel.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/OngoingCallsViewModel.kt
new file mode 100644
index 0000000..32e7c29
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/OngoingCallsViewModel.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import android.content.Context
+import android.net.Uri
+import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
+import android.telephony.PhoneNumberUtils
+import android.telephony.TelephonyManager
+import androidx.core.content.getSystemService
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.CallException
+import androidx.core.telecom.test.services.AudioRoute
+import androidx.core.telecom.test.services.CallAudioEndpoint
+import androidx.core.telecom.test.services.CallData
+import androidx.core.telecom.test.services.CallState
+import androidx.core.telecom.test.services.Capability
+import androidx.core.telecom.test.services.ParticipantExtensionData
+import androidx.core.telecom.test.services.RemoteCallProvider
+import androidx.core.telecom.util.ExperimentalAppActions
+import androidx.lifecycle.ViewModel
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel responsible for maintaining the connection to the [RemoteCallProvider] as well as
+ * converting call/extension state to the associated UI specific state.
+ */
+class OngoingCallsViewModel(private val callProvider: RemoteCallProvider = RemoteCallProvider()) :
+ ViewModel() {
+ companion object {
+ val UnknownAudioEndpoint =
+ CallAudioEndpoint(id = "UNKNOWN", audioRoute = AudioRoute.UNKNOWN)
+ val UnknownAudioUiState =
+ AudioEndpointUiState(id = "UNKNOWN", name = "UNKNOWN", audioRoute = AudioRoute.UNKNOWN)
+ }
+
+ /** connect to the remote call provider with the given context */
+ fun connectService(context: Context) {
+ callProvider.connectService(context)
+ }
+
+ /** disconnect from the remote call provider */
+ fun disconnectService() {
+ callProvider.disconnectService()
+ }
+
+ /**
+ * stream the [CallData] from the [RemoteCallProvider] when the service is connected and map
+ * that data to associated [CallUiState].
+ */
+ fun streamCallData(context: Context): Flow<List<CallUiState>> {
+ return callProvider.streamCallData().map { dataState ->
+ dataState.map { mapToUiState(context, it) }
+ }
+ }
+
+ /** Stream the global mute state of the device as long as the service is connected. */
+ fun streamMuteData(): Flow<Boolean> {
+ return callProvider.streamMuteData()
+ }
+
+ /**
+ * Stream the current audio endpoint of the device as long as the service is connected and we
+ * are in call.
+ */
+ fun streamCurrentEndpointAudioData(): Flow<AudioEndpointUiState> {
+ return callProvider
+ .streamCurrentEndpointData()
+ .map { it ?: UnknownAudioEndpoint }
+ .map(::mapToUiAudioState)
+ }
+
+ /**
+ * Stream the available endpoints of the device as long as the service is connected and we are
+ * in call.
+ */
+ fun streamAvailableEndpointAudioData(): Flow<List<AudioEndpointUiState>> {
+ return callProvider
+ .streamAvailableEndpointData()
+ .map { it.map(::mapToUiAudioState) }
+ .map { endpoints -> endpoints.sortedWith(compareBy({ it.audioRoute }, { it.name })) }
+ }
+
+ /**
+ * Change the global mute state of the device
+ *
+ * @param isMuted true if the device should be muted, false otherwise
+ */
+ fun onChangeMuteState(isMuted: Boolean) {
+ callProvider.onChangeMuteState(isMuted)
+ }
+
+ /**
+ * Change the audio route of the active call
+ *
+ * @param id The ID of the endpoint from [AudioEndpointUiState.id]
+ */
+ suspend fun onChangeAudioRoute(id: String) {
+ callProvider.onChangeAudioRoute(id)
+ }
+
+ /** Perform a map operation from [CallData] to [CallUiState] */
+ private fun mapToUiState(context: Context, fullCallData: CallData): CallUiState {
+ return CallUiState(
+ id = fullCallData.callData.id,
+ name = fullCallData.callData.contactName ?: fullCallData.callData.name,
+ photo = fullCallData.callData.contactUri,
+ number =
+ formatPhoneNumber(
+ context,
+ fullCallData.callData.phoneAccountHandle,
+ fullCallData.callData.number
+ ),
+ state = fullCallData.callData.state,
+ validTransition =
+ getValidTransition(fullCallData.callData.state, fullCallData.callData.capabilities),
+ direction = fullCallData.callData.direction,
+ callType = fullCallData.callData.callType,
+ onStateChanged = { fullCallData.callData.onStateChanged(it) },
+ participantUiState = mapToUiParticipantExtension(fullCallData.participantExtensionData)
+ )
+ }
+
+ /** Perform a map ooperation from [ParticipantExtensionData] to [ParticipantExtensionUiState] */
+ @OptIn(ExperimentalAppActions::class)
+ private fun mapToUiParticipantExtension(
+ participantExtensionData: ParticipantExtensionData?
+ ): ParticipantExtensionUiState? {
+ if (participantExtensionData == null || !participantExtensionData.isSupported) return null
+ return ParticipantExtensionUiState(
+ isRaiseHandSupported =
+ participantExtensionData.raiseHandData?.raiseHandAction?.isSupported ?: false,
+ isKickParticipantSupported =
+ participantExtensionData.kickParticipantData?.kickParticipantAction?.isSupported
+ ?: false,
+ onRaiseHandStateChanged = {
+ participantExtensionData.raiseHandData
+ ?.raiseHandAction
+ ?.requestRaisedHandStateChange(it)
+ },
+ participants = mapUiParticipants(participantExtensionData)
+ )
+ }
+
+ /** map [ParticipantExtensionData] to [ParticipantExtensionUiState] */
+ @OptIn(ExperimentalAppActions::class)
+ private fun mapUiParticipants(
+ participantExtensionData: ParticipantExtensionData
+ ): List<ParticipantUiState> {
+ return participantExtensionData.participants.map { p ->
+ ParticipantUiState(
+ name = p.name.toString(),
+ isActive = participantExtensionData.activeParticipant == p,
+ isSelf = participantExtensionData.selfParticipant?.id == p.id,
+ isHandRaised =
+ participantExtensionData.raiseHandData?.raisedHands?.contains(p) ?: false,
+ onKickParticipant = {
+ participantExtensionData.kickParticipantData
+ ?.kickParticipantAction
+ ?.requestKickParticipant(p)
+ ?: CallControlResult.Error(CallException.ERROR_CALL_IS_NOT_BEING_TRACKED)
+ }
+ )
+ }
+ }
+
+ /** format the phone number to a user friendly form */
+ private fun formatPhoneNumber(
+ context: Context,
+ phoneAccountHandle: PhoneAccountHandle,
+ number: Uri
+ ): String {
+ val isTel = PhoneAccount.SCHEME_TEL == number.scheme
+ if (!isTel) return number.schemeSpecificPart
+ val tm: TelephonyManager? =
+ context
+ .getSystemService<TelephonyManager>()
+ ?.createForPhoneAccountHandle(phoneAccountHandle)
+ val iso = tm?.networkCountryIso ?: Locale.getDefault().country
+ return PhoneNumberUtils.formatNumber(number.schemeSpecificPart, iso)
+ }
+
+ /** Determine the valid [CallStateTransition] based on [CallState] and call [Capability] */
+ private fun getValidTransition(
+ state: CallState,
+ capabilities: List<Capability>
+ ): CallStateTransition {
+ return when (state) {
+ CallState.INCOMING -> CallStateTransition.ANSWER
+ CallState.DIALING -> CallStateTransition.NONE
+ CallState.ACTIVE -> {
+ if (capabilities.contains(Capability.SUPPORTS_HOLD)) {
+ CallStateTransition.HOLD
+ } else {
+ CallStateTransition.NONE
+ }
+ }
+ CallState.HELD -> CallStateTransition.UNHOLD
+ CallState.DISCONNECTING -> CallStateTransition.NONE
+ CallState.DISCONNECTED -> CallStateTransition.NONE
+ CallState.UNKNOWN -> CallStateTransition.NONE
+ }
+ }
+
+ /** Map from [CallAudioEndpoint] to [AudioEndpointUiState] */
+ private fun mapToUiAudioState(endpoint: CallAudioEndpoint): AudioEndpointUiState {
+ return AudioEndpointUiState(
+ id = endpoint.id,
+ name = endpoint.frameworkName ?: getAudioEndpointRouteName(endpoint.audioRoute),
+ audioRoute = endpoint.audioRoute
+ )
+ }
+
+ /** Get the user friendly endpoint route name */
+ private fun getAudioEndpointRouteName(audioState: AudioRoute): String {
+ return when (audioState) {
+ AudioRoute.EARPIECE -> "Earpiece"
+ AudioRoute.SPEAKER -> "Speaker"
+ AudioRoute.HEADSET -> "Headset"
+ AudioRoute.BLUETOOTH -> "Bluetooth"
+ AudioRoute.STREAMING -> "Streaming"
+ AudioRoute.UNKNOWN -> "Unknown"
+ }
+ }
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ParticipantExtensionUiState.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ParticipantExtensionUiState.kt
new file mode 100644
index 0000000..7ca42ae
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/calling/ParticipantExtensionUiState.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.calling
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.core.telecom.CallControlResult
+
+/** Provide a preview of [ParticipantExtensionUiState] for helping with UI rendering of state. */
+class ParticipantExtensionProvider : PreviewParameterProvider<ParticipantExtensionUiState> {
+ override val values =
+ sequenceOf(
+ ParticipantExtensionUiState(
+ isRaiseHandSupported = true,
+ isKickParticipantSupported = true,
+ onRaiseHandStateChanged = { CallControlResult.Success() },
+ listOf(
+ ParticipantUiState(
+ "Abraham Lincoln",
+ false,
+ isHandRaised = false,
+ isSelf = true,
+ onKickParticipant = { CallControlResult.Success() }
+ ),
+ ParticipantUiState(
+ "Betty Lapone",
+ true,
+ isHandRaised = true,
+ isSelf = false,
+ onKickParticipant = { CallControlResult.Success() }
+ )
+ )
+ )
+ )
+}
+
+/** UI state and actions related to the extensions on a call */
+data class ParticipantExtensionUiState(
+ val isRaiseHandSupported: Boolean,
+ val isKickParticipantSupported: Boolean,
+ val onRaiseHandStateChanged: suspend (Boolean) -> Unit,
+ val participants: List<ParticipantUiState>
+)
+
+/** UI state and actions associated with a participant */
+data class ParticipantUiState(
+ val name: String,
+ val isActive: Boolean,
+ val isSelf: Boolean,
+ val isHandRaised: Boolean,
+ val onKickParticipant: suspend () -> CallControlResult,
+)
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/theme/Theme.kt b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/theme/Theme.kt
new file mode 100644
index 0000000..8c233af
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/java/androidx/core/telecom/test/ui/theme/Theme.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.test.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+/**
+ * VERY basic theme for this device, which just pulls default colors from the system UI in light and
+ * dark mode.
+ */
+@Composable
+fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+ val context = LocalContext.current
+ val colors =
+ when {
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
+ if (useDarkTheme) dynamicDarkColorScheme(context)
+ else dynamicLightColorScheme(context)
+ }
+ useDarkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+ // Add primary status bar color from chosen color scheme.
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colors.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
+ useDarkTheme
+ }
+ }
+
+ MaterialTheme(colorScheme = colors, content = content)
+}
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/android.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/android.xml
new file mode 100644
index 0000000..dfa932e
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/android.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="160dp"
+ android:height="160dp"
+ android:viewportHeight="432"
+ android:viewportWidth="432">
+
+ <!-- Safe zone = 66dp => 432 * (66 / 108) = 432 * 0.61 -->
+ <group
+ android:translateX="84"
+ android:translateY="84"
+ android:scaleX="0.61"
+ android:scaleY="0.61">
+
+ <path
+ android:fillColor="#3ddc84"
+ android:pathData="m322.02,167.89c12.141,-21.437 25.117,-42.497 36.765,-64.158 2.2993,-7.7566 -9.5332,-12.802 -13.555,-5.7796 -12.206,21.045 -24.375,42.112 -36.567,63.166 -57.901,-26.337 -127.00,-26.337 -184.90,0.0 -12.685,-21.446 -24.606,-43.441 -37.743,-64.562 -5.6074,-5.8390 -15.861,1.9202 -11.747,8.8889 12.030,20.823 24.092,41.629 36.134,62.446C47.866,200.90 5.0987,267.15 0.0,337.5c144.00,0.0 288.00,0.0 432.0,0.0C426.74,267.06 384.46,201.32 322.02,167.89ZM116.66,276.03c-13.076,0.58968 -22.531,-15.277 -15.773,-26.469 5.7191,-11.755 24.196,-12.482 30.824,-1.2128 7.8705,11.451 -1.1102,28.027 -15.051,27.682zM315.55,276.03c-13.076,0.58968 -22.531,-15.277 -15.773,-26.469 5.7191,-11.755 24.196,-12.482 30.824,-1.2128 7.8705,11.451 -1.1097,28.027 -15.051,27.682z"
+ android:strokeWidth="2" />
+ </group>
+</vector>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/bluetooth_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/bluetooth_24px.xml
new file mode 100644
index 0000000..c4a98b4
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/bluetooth_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M440,880L440,576L256,760L200,704L424,480L200,256L256,200L440,384L440,80L480,80L708,308L536,480L708,652L480,880L440,880ZM520,384L596,308L520,234L520,384ZM520,726L596,652L520,576L520,726Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/call_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/call_24px.xml
new file mode 100644
index 0000000..09fff5d
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/call_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/cast_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/cast_24px.xml
new file mode 100644
index 0000000..7e99841
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/cast_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM800,800L600,800Q600,780 598.5,760Q597,740 594,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,286Q140,283 120,281.5Q100,280 80,280L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800ZM80,800L80,680Q130,680 165,715Q200,750 200,800L80,800ZM280,800Q280,717 221.5,658.5Q163,600 80,600L80,520Q197,520 278.5,601.5Q360,683 360,800L280,800ZM440,800Q440,725 411.5,659.5Q383,594 334.5,545.5Q286,497 220.5,468.5Q155,440 80,440L80,360Q171,360 251,394.5Q331,429 391,489Q451,549 485.5,629Q520,709 520,800L440,800Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/dialpad_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/dialpad_24px.xml
new file mode 100644
index 0000000..ce1f8b1
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/dialpad_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,920Q447,920 423.5,896.5Q400,873 400,840Q400,807 423.5,783.5Q447,760 480,760Q513,760 536.5,783.5Q560,807 560,840Q560,873 536.5,896.5Q513,920 480,920ZM240,200Q207,200 183.5,176.5Q160,153 160,120Q160,87 183.5,63.5Q207,40 240,40Q273,40 296.5,63.5Q320,87 320,120Q320,153 296.5,176.5Q273,200 240,200ZM240,440Q207,440 183.5,416.5Q160,393 160,360Q160,327 183.5,303.5Q207,280 240,280Q273,280 296.5,303.5Q320,327 320,360Q320,393 296.5,416.5Q273,440 240,440ZM240,680Q207,680 183.5,656.5Q160,633 160,600Q160,567 183.5,543.5Q207,520 240,520Q273,520 296.5,543.5Q320,567 320,600Q320,633 296.5,656.5Q273,680 240,680ZM720,200Q687,200 663.5,176.5Q640,153 640,120Q640,87 663.5,63.5Q687,40 720,40Q753,40 776.5,63.5Q800,87 800,120Q800,153 776.5,176.5Q753,200 720,200ZM480,680Q447,680 423.5,656.5Q400,633 400,600Q400,567 423.5,543.5Q447,520 480,520Q513,520 536.5,543.5Q560,567 560,600Q560,633 536.5,656.5Q513,680 480,680ZM720,680Q687,680 663.5,656.5Q640,633 640,600Q640,567 663.5,543.5Q687,520 720,520Q753,520 776.5,543.5Q800,567 800,600Q800,633 776.5,656.5Q753,680 720,680ZM720,440Q687,440 663.5,416.5Q640,393 640,360Q640,327 663.5,303.5Q687,280 720,280Q753,280 776.5,303.5Q800,327 800,360Q800,393 776.5,416.5Q753,440 720,440ZM480,440Q447,440 423.5,416.5Q400,393 400,360Q400,327 423.5,303.5Q447,280 480,280Q513,280 536.5,303.5Q560,327 560,360Q560,393 536.5,416.5Q513,440 480,440ZM480,200Q447,200 423.5,176.5Q400,153 400,120Q400,87 423.5,63.5Q447,40 480,40Q513,40 536.5,63.5Q560,87 560,120Q560,153 536.5,176.5Q513,200 480,200Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/headset_mic_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/headset_mic_24px.xml
new file mode 100644
index 0000000..ac3d6e5
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/headset_mic_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,920L480,840L760,840Q760,840 760,840Q760,840 760,840L760,800L600,800L600,480L760,480L760,440Q760,324 678,242Q596,160 480,160Q364,160 282,242Q200,324 200,440L200,480L360,480L360,800L200,800Q167,800 143.5,776.5Q120,753 120,720L120,440Q120,366 148.5,300.5Q177,235 226,186Q275,137 340.5,108.5Q406,80 480,80Q554,80 619.5,108.5Q685,137 734,186Q783,235 811.5,300.5Q840,366 840,440L840,840Q840,873 816.5,896.5Q793,920 760,920L480,920ZM200,720L280,720L280,560L200,560L200,720Q200,720 200,720Q200,720 200,720ZM680,720L760,720L760,560L680,560L680,720ZM200,560Q200,560 200,560Q200,560 200,560L200,560L280,560L280,560L200,560ZM680,560L680,560L760,560L760,560L680,560Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/ic_launcher.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000..481bbd7
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/android" />
+</layer-list>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic.xml
new file mode 100644
index 0000000..b239700
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:pathData="M480,560q-50,0 -85,-35t-35,-85v-240q0,-50 35,-85t85,-35q50,0 85,35t35,85v240q0,50 -35,85t-85,35ZM480,320ZM440,840v-123q-104,-14 -172,-93t-68,-184h80q0,83 58.5,141.5T480,640q83,0 141.5,-58.5T680,440h80q0,105 -68,184t-172,93v123h-80ZM480,480q17,0 28.5,-11.5T520,440v-240q0,-17 -11.5,-28.5T480,160q-17,0 -28.5,11.5T440,200v240q0,17 11.5,28.5T480,480Z"
+ android:fillColor="@android:color/white"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic_off_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic_off_24px.xml
new file mode 100644
index 0000000..716a40c
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/mic_off_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M710,598L652,540Q666,517 673,492Q680,467 680,440L760,440Q760,484 747,523.5Q734,563 710,598ZM480,366Q480,366 480,366Q480,366 480,366L480,366L480,366L480,366Q480,366 480,366Q480,366 480,366ZM592,478L520,406L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,326L360,246L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,451 597.5,460Q595,469 592,478ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 337.5,581.5Q395,640 480,640Q514,640 544.5,629.5Q575,619 600,600L657,657Q628,680 593.5,696Q559,712 520,717L520,840L440,840ZM792,904L56,168L112,112L848,848L792,904Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/phone_in_talk_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/phone_in_talk_24px.xml
new file mode 100644
index 0000000..d35ae53
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/phone_in_talk_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M760,480Q760,363 678.5,281.5Q597,200 480,200L480,120Q555,120 620.5,148.5Q686,177 734.5,225.5Q783,274 811.5,339.5Q840,405 840,480L760,480ZM600,480Q600,430 565,395Q530,360 480,360L480,280Q563,280 621.5,338.5Q680,397 680,480L600,480ZM798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/speaker_phone_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/speaker_phone_24px.xml
new file mode 100644
index 0000000..6e40548
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/speaker_phone_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M338,340L280,282Q321,243 372.5,221.5Q424,200 480,200Q536,200 587.5,221.5Q639,243 680,282L622,340Q593,311 557,295.5Q521,280 480,280Q439,280 403,295.5Q367,311 338,340ZM226,224L170,168Q233,106 312.5,73Q392,40 480,40Q568,40 647.5,73Q727,106 790,168L734,224Q683,174 617,147Q551,120 480,120Q409,120 343,147Q277,174 226,224ZM400,880Q367,880 343.5,856.5Q320,833 320,800L320,480Q320,447 343.5,423.5Q367,400 400,400L560,400Q593,400 616.5,423.5Q640,447 640,480L640,800Q640,833 616.5,856.5Q593,880 560,880L400,880ZM560,800Q560,800 560,800Q560,800 560,800L560,480Q560,480 560,480Q560,480 560,480L400,480Q400,480 400,480Q400,480 400,480L400,800Q400,800 400,800Q400,800 400,800L560,800ZM560,800L400,800Q400,800 400,800Q400,800 400,800L400,800Q400,800 400,800Q400,800 400,800L560,800Q560,800 560,800Q560,800 560,800L560,800Q560,800 560,800Q560,800 560,800Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/waving_hand_24px.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/waving_hand_24px.xml
new file mode 100644
index 0000000..a5589ac
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/drawable/waving_hand_24px.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M430,460L713,177Q725,165 741,165Q757,165 769,177Q781,189 781,205Q781,221 769,233L487,516L430,460ZM529,559L783,304Q795,292 811.5,292Q828,292 840,304Q852,316 852,332.5Q852,349 840,361L586,615L529,559ZM211,749Q120,658 120,530Q120,402 211,311L331,191L390,250Q397,257 402,264.5Q407,272 412,280L560,131Q572,119 588.5,119Q605,119 617,131Q629,143 629,159.5Q629,176 617,188L444,361L444,361L359,445L378,464Q424,510 422,574Q420,638 373,685L373,685L316,629L316,629Q339,606 341.5,574.5Q344,543 321,520L274,474Q262,462 262,445.5Q262,429 274,417L331,361Q343,349 343,332.5Q343,316 331,304L331,304L267,368Q199,436 199,530.5Q199,625 267,693Q335,761 430,761Q525,761 593,693L832,453Q844,441 860.5,441Q877,441 889,453Q901,465 901,481.5Q901,498 889,510L649,749Q558,840 430,840Q302,840 211,749ZM430,530Q430,530 430,530Q430,530 430,530L430,530L430,530L430,530Q430,530 430,530Q430,530 430,530L430,530L430,530L430,530Q430,530 430,530Q430,530 430,530L430,530Q430,530 430,530Q430,530 430,530ZM680,921L680,840Q746,840 793,793Q840,746 840,680L921,680Q921,780 850.5,850.5Q780,921 680,921ZM39,280Q39,180 109.5,109.5Q180,39 280,39L280,120Q214,120 167,167Q120,214 120,280L39,280Z"/>
+</vector>
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/values/strings.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..53186db
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_name">Telecom Test ICS</string>
+ <string name="main_activity_name">Jetpack ICS</string>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testicsapp/src/main/res/values/themes.xml b/core/core-telecom/integration-tests/testicsapp/src/main/res/values/themes.xml
new file mode 100644
index 0000000..eb98ba9
--- /dev/null
+++ b/core/core-telecom/integration-tests/testicsapp/src/main/res/values/themes.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="Theme.IcsTest" parent="@android:style/Theme.Material.Light.NoActionBar"/>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
index b46b7ce..630a3e9 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2EExtensionTests.kt
@@ -26,6 +26,7 @@
import androidx.core.telecom.extensions.CallExtensionScope
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.Extensions
+import androidx.core.telecom.extensions.LocalCallSilenceExtensionImpl
import androidx.core.telecom.extensions.Participant
import androidx.core.telecom.extensions.ParticipantExtensionImpl
import androidx.core.telecom.extensions.ParticipantExtensionRemote
@@ -131,6 +132,22 @@
}
}
+ // TODO:: b/364316364 should assert on a per call basis
+ internal class CachedLocalSilence(scope: CallExtensionScope) {
+ private val isLocallySilenced = MutableStateFlow(false)
+
+ val extension =
+ scope.addLocalCallSilenceExtension(onIsLocallySilencedUpdated = isLocallySilenced::emit)
+
+ suspend fun waitForLocalCallSilenceState(expected: Boolean) {
+ val result =
+ withTimeoutOrNull(ICS_EXTENSION_UPDATE_TIMEOUT_MS) {
+ isLocallySilenced.first { it == expected }
+ }
+ assertEquals("Never received local call silence state", expected, result)
+ }
+ }
+
internal class CachedRaisedHands(extension: ParticipantExtensionRemote) {
private val raisedHands = MutableStateFlow<List<Participant>>(emptyList())
val action = extension.addRaiseHandAction(raisedHands::emit)
@@ -235,6 +252,10 @@
val participants = CachedParticipants(this)
onConnected {
hasConnected = true
+ assertTrue(
+ "Participants are not supported",
+ participants.extension.isSupported
+ )
// Wait for initial state
participants.waitForParticipants(emptySet())
participants.waitForActiveParticipant(null)
@@ -319,6 +340,50 @@
}
/**
+ * This is an end to end test that verifies a VoIP application and InCallService can add the
+ * LocalCallSilenceExtension and toggle the value.
+ */
+ @LargeTest
+ @Test(timeout = 10000)
+ fun testVoipAndIcsTogglingTheLocalCallSilenceExtension(): Unit = runBlocking {
+ usingIcs { ics ->
+ val voipAppControl = bindToVoipAppWithExtensions()
+ val callback = TestCallCallbackListener(this)
+ voipAppControl.setCallback(callback)
+ val voipCallId =
+ createAndVerifyVoipCall(
+ voipAppControl,
+ listOf(getLocalSilenceCapability(setOf())),
+ parameters.direction
+ )
+
+ val call = TestUtils.waitOnInCallServiceToReachXCalls(ics, 1)!!
+ var hasConnected = false
+ with(ics) {
+ connectExtensions(call) {
+ val localSilenceExtension = CachedLocalSilence(this)
+ onConnected {
+ hasConnected = true
+ // VoIP --> ICS
+ voipAppControl.updateIsLocallySilenced(false)
+ localSilenceExtension.waitForLocalCallSilenceState(false)
+
+ voipAppControl.updateIsLocallySilenced(true)
+ localSilenceExtension.waitForLocalCallSilenceState(true)
+
+ // ICS -> VOIP
+ localSilenceExtension.extension.requestLocalCallSilenceUpdate(false)
+ callback.waitForIsLocalSilenced(voipCallId, false)
+
+ call.disconnect()
+ }
+ }
+ }
+ assertTrue("onConnected never received", hasConnected)
+ }
+ }
+
+ /**
* Create a VOIP call with a participants extension and attach participant Call extensions.
* Verify kick participant functionality works as expected
*/
@@ -432,4 +497,12 @@
actions = actions
)
}
+
+ private fun getLocalSilenceCapability(actions: Set<Int>): Capability {
+ return createCapability(
+ id = Extensions.LOCAL_CALL_SILENCE,
+ version = LocalCallSilenceExtensionImpl.VERSION,
+ actions = actions
+ )
+ }
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt
index 0690c4d..7ec150a 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/ExtensionAidlTest.kt
@@ -20,6 +20,7 @@
import androidx.core.telecom.extensions.ICallDetailsListener
import androidx.core.telecom.extensions.ICapabilityExchange
import androidx.core.telecom.extensions.ICapabilityExchangeListener
+import androidx.core.telecom.extensions.ILocalSilenceStateListener
import androidx.core.telecom.extensions.IParticipantStateListener
import androidx.core.telecom.util.ExperimentalAppActions
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -79,6 +80,14 @@
createCallDetailsExtension(version, actions, l, packageName)
}
+ override fun onCreateLocalCallSilenceExtension(
+ version: Int,
+ actions: IntArray?,
+ l: ILocalSilenceStateListener?
+ ) {
+ TODO("Not yet implemented")
+ }
+
override fun onRemoveExtensions() {
unsubscribeFromParticipantExtensionUpdatse()
unsubscribeFromCallDetailsExtensionUpdates()
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
index 18b590d..aeea2a7 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipAppWithExtensionsControl.kt
@@ -43,6 +43,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -57,6 +58,8 @@
private var participantsFlow: MutableStateFlow<Set<Participant>> = MutableStateFlow(emptySet())
private var activeParticipantFlow: MutableStateFlow<Participant?> = MutableStateFlow(null)
private var raisedHandsFlow: MutableStateFlow<List<Participant>> = MutableStateFlow(emptyList())
+ // TODO:: b/364316364 should be Pair(callId:String, value: Boolean)
+ private var isLocallySilencedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
companion object {
val TAG = VoipAppWithExtensionsControl::class.java.simpleName
@@ -85,7 +88,6 @@
}
override fun addCall(capabilities: List<Capability>, isOutgoing: Boolean): String {
- Log.i(TAG, "VoipAppWithExtensionsControl: addCall: in function")
var id = ""
runBlocking {
val deferredId = CompletableDeferred<String>()
@@ -106,7 +108,7 @@
participantsFlow
.onEach {
TestUtils.printParticipants(it, "VoIP participants")
- participantStateUpdater!!.updateParticipants(it)
+ participantStateUpdater?.updateParticipants(it)
}
.launchIn(this)
raisedHandsFlow
@@ -118,7 +120,16 @@
activeParticipantFlow
.onEach {
Log.i(TAG, "VOIP active participant: $it")
- participantStateUpdater!!.updateActiveParticipant(it)
+ participantStateUpdater?.updateActiveParticipant(it)
+ }
+ .launchIn(this)
+ isLocallySilencedFlow
+ .drop(1) // ignore the first value from the voip app
+ // since only values from the test should be sent!
+ .onEach {
+ Log.i(TAG, "VoIP isLocallySilenced=[$it]")
+ // TODO:: b/364316364 gate on callId
+ localCallSilenceUpdater?.updateIsLocallySilenced(it)
}
.launchIn(this)
deferredId.complete(this.getCallId().toString())
@@ -142,6 +153,11 @@
override fun updateRaisedHands(raisedHandsParticipants: List<ParticipantParcelable>) {
raisedHandsFlow.value = raisedHandsParticipants.map { it.toParticipant() }
}
+
+ // TODO:: b/364316364 add CallId arg. Should be changing on a per call basis
+ override fun updateIsLocallySilenced(isLocallySilenced: Boolean) {
+ isLocallySilencedFlow.value = isLocallySilenced
+ }
}
override fun onBind(intent: Intent?): IBinder? {
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
index fc9d6f5..e97f365 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/VoipAppWithExtensions/VoipCall.kt
@@ -26,6 +26,7 @@
import androidx.core.telecom.extensions.Capability
import androidx.core.telecom.extensions.ExtensionInitializationScope
import androidx.core.telecom.extensions.Extensions
+import androidx.core.telecom.extensions.LocalCallSilenceExtension
import androidx.core.telecom.extensions.ParticipantExtension
import androidx.core.telecom.extensions.ParticipantExtensionImpl
import androidx.core.telecom.extensions.RaiseHandState
@@ -47,6 +48,8 @@
// Participant state updaters
internal var participantStateUpdater: ParticipantExtension? = null
internal var raiseHandStateUpdater: RaiseHandState? = null
+ // Local Call Silence
+ internal var localCallSilenceUpdater: LocalCallSilenceExtension? = null
suspend fun addCall(
callAttributes: CallAttributesCompat,
@@ -79,6 +82,13 @@
participantStateUpdater = addParticipantExtension()
participantStateUpdater!!.initializeActions(capability)
}
+ Extensions.LOCAL_CALL_SILENCE -> {
+ localCallSilenceUpdater =
+ addLocalSilenceExtension(false) {
+ Log.i(TAG, "addLocalSilenceExtension: callId=[$callId], it=[$it]")
+ callback?.setLocalCallSilenceState(callId, it)
+ }
+ }
}
}
}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
index 79b9d8f..214a196 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
@@ -44,6 +44,7 @@
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
@@ -418,6 +419,8 @@
MutableSharedFlow(replay = 1)
private val kickParticipantFlow: MutableSharedFlow<Pair<String, Participant?>> =
MutableSharedFlow(replay = 1)
+ private val isLocallySilencedFlow: MutableSharedFlow<Pair<String, Boolean>> =
+ MutableStateFlow(Pair("", false))
override fun raiseHandStateAction(callId: String?, isHandRaised: Boolean) {
if (callId == null) return
@@ -429,6 +432,11 @@
scope.launch { kickParticipantFlow.emit(Pair(callId, participant?.toParticipant())) }
}
+ override fun setLocalCallSilenceState(callId: String?, isLocallySilenced: Boolean) {
+ if (callId == null) return
+ scope.launch { isLocallySilencedFlow.emit(Pair(callId, isLocallySilenced)) }
+ }
+
suspend fun waitForRaiseHandState(callId: String, expectedState: Boolean) {
val result =
withTimeoutOrNull(5000) {
@@ -437,6 +445,16 @@
assertEquals("raised hands action never received", expectedState, result?.second)
}
+ suspend fun waitForIsLocalSilenced(callId: String, expectedState: Boolean) {
+ val result =
+ withTimeoutOrNull(5000) {
+ isLocallySilencedFlow
+ .filter { it.first == callId && it.second == expectedState }
+ .first()
+ }
+ assertEquals("<LOCAL CALL SILENCE> never received", expectedState, result?.second)
+ }
+
suspend fun waitForKickParticipant(callId: String, expectedParticipant: Participant?) {
val result =
withTimeoutOrNull(5000) {
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl
index 011f1d9..9f70c47 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ICapabilityExchangeListener.aidl
@@ -20,6 +20,7 @@
import androidx.core.telecom.extensions.Capability;
import androidx.core.telecom.extensions.IParticipantStateListener;
import androidx.core.telecom.extensions.ICallDetailsListener;
+import androidx.core.telecom.extensions.ILocalSilenceStateListener;
// ICS Client -> VOIP app
@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
@@ -32,4 +33,6 @@
void onCreateCallDetailsExtension(in int version, in int[] actions, in ICallDetailsListener l, in String packageName) = 1;
// V1 - Remove extensions and release resources related to this InCallService connection
void onRemoveExtensions() = 2;
+ // V1 - no actions, only the ability to toggle the isLocallySilenced value
+ void onCreateLocalCallSilenceExtension(in int version, in int[] actions, in ILocalSilenceStateListener l) = 3;
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceActions.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceActions.aidl
new file mode 100644
index 0000000..782396b
--- /dev/null
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceActions.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.core.telecom.extensions;
+
+import androidx.core.telecom.extensions.IActionsResultCallback;
+
+// ICS Client -> VOIP App
+@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+oneway interface ILocalSilenceActions {
+ void setIsLocallySilenced(boolean isLocallySilenced, in IActionsResultCallback cb) = 0;
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceStateListener.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceStateListener.aidl
new file mode 100644
index 0000000..ebb07e1
--- /dev/null
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/extensions/ILocalSilenceStateListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.core.telecom.extensions;
+
+import java.util.List;
+import androidx.core.telecom.extensions.ILocalSilenceActions;
+
+// VOIP app -> ICS Client
+@JavaPassthrough(annotation="@androidx.core.telecom.util.ExperimentalAppActions")
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+oneway interface ILocalSilenceStateListener {
+void updateIsLocallySilenced(boolean isLocallySilenced)= 0;
+void finishSync(in ILocalSilenceActions cb) = 1;
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
index 999fa0f..455ec11 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControl.aidl
@@ -13,4 +13,5 @@
void updateParticipants(in List<ParticipantParcelable> participants);
void updateActiveParticipant(in ParticipantParcelable participant);
void updateRaisedHands(in List<ParticipantParcelable> raisedHandsParticipants);
+ void updateIsLocallySilenced(boolean isLocallySilenced);
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
index d9bc951..8e88d62 100644
--- a/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
+++ b/core/core-telecom/src/main/aidl/androidx/core/telecom/test/ITestAppControlCallback.aidl
@@ -6,4 +6,5 @@
oneway interface ITestAppControlCallback {
void raiseHandStateAction(in String callId, boolean isHandRaised);
void kickParticipantAction(in String callId, in ParticipantParcelable participant);
+ void setLocalCallSilenceState(in String callId, boolean isLocallySilenced);
}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt
index 349ab3f..b3f6a0f 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScope.kt
@@ -83,4 +83,27 @@
onActiveParticipantChanged: suspend (Participant?) -> Unit,
onParticipantsUpdated: suspend (Set<Participant>) -> Unit
): ParticipantExtensionRemote
+
+ /**
+ * Add support for this remote surface to display information related to the local call silence
+ * state for this call.
+ *
+ * ```
+ * connectExtensions(call) {
+ * val localCallSilenceExtension = addLocalCallSilenceExtension(
+ * // consume local call silence state changes
+ * )
+ * onConnected {
+ * // At this point, support for the local call silence extension will be known
+ * }
+ * }
+ * ```
+ *
+ * @param onIsLocallySilencedUpdated Called when the local call silence state has changed and
+ * the UI should be updated.
+ * @return The interface that is used to interact with the local call silence extension methods.
+ */
+ public fun addLocalCallSilenceExtension(
+ onIsLocallySilencedUpdated: suspend (Boolean) -> Unit
+ ): LocalCallSilenceExtensionRemote
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
index 7b44167..39674ef 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/CallExtensionScopeImpl.kt
@@ -16,9 +16,7 @@
package androidx.core.telecom.extensions
-import android.Manifest
import android.content.Context
-import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -28,24 +26,26 @@
import android.telecom.Call.Callback
import android.telecom.InCallService
import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
-import androidx.core.content.ContextCompat
import androidx.core.telecom.CallsManager
import androidx.core.telecom.internal.CapabilityExchangeListenerRemote
import androidx.core.telecom.internal.utils.Utils
import androidx.core.telecom.util.ExperimentalAppActions
import java.util.Collections
import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
@@ -152,6 +152,24 @@
return extension
}
+ override fun addLocalCallSilenceExtension(
+ onIsLocallySilencedUpdated: suspend (Boolean) -> Unit
+ ): LocalCallSilenceExtensionRemoteImpl {
+ val extension = LocalCallSilenceExtensionRemoteImpl(callScope, onIsLocallySilencedUpdated)
+ registerExtension {
+ CallExtensionCreator(
+ extensionCapability =
+ Capability().apply {
+ featureId = Extensions.LOCAL_CALL_SILENCE
+ featureVersion = LocalCallSilenceExtensionImpl.VERSION
+ supportedActions = extension.actions
+ },
+ onExchangeComplete = extension::onExchangeComplete
+ )
+ }
+ return extension
+ }
+
/**
* Register an extension with this call, whose capability will be negotiated with the VOIP
* application.
@@ -210,30 +228,19 @@
if (Utils.hasPlatformV2Apis()) {
// Android CallsManager V+ check
if (details.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL)) {
+ Log.d(TAG, "resolveCallExtensionsType: PROPERTY_IS_TRANSACTIONAL present")
return CAPABILITY_EXCHANGE
}
// Android CallsManager U check
- // Verify read phone numbers permission to see if phone account supports transactional
- // ops.
- if (
- ContextCompat.checkSelfPermission(
- applicationContext,
- Manifest.permission.READ_PHONE_NUMBERS
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- val telecomManager =
- applicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
- val phoneAccount = telecomManager.getPhoneAccount(details.accountHandle)
- if (
- phoneAccount?.hasCapabilities(
- PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS
- ) == true
- ) {
- return CAPABILITY_EXCHANGE
- }
- } else {
- Log.i(TAG, "Unable to resolve call extension type due to lack of permission.")
+ val acct = getPhoneAccountIfAllowed(details.accountHandle)
+ if (acct == null) {
+ Log.d(TAG, "resolveCallExtensionsType: Unable to resolve PA")
type = UNKNOWN
+ } else if (
+ acct.hasCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ ) {
+ Log.d(TAG, "resolveCallExtensionsType: PA supports transactional API")
+ return CAPABILITY_EXCHANGE
}
}
// The extras may come in after the call is first signalled to InCallService - wait for the
@@ -256,21 +263,55 @@
if (callExtras.containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)) {
return CAPABILITY_EXCHANGE
}
- Log.i(TAG, "Unable to resolve call extension type. Returning $type.")
+ Log.i(
+ TAG,
+ "resolveCallExtensionsType: Unable to resolve call extension type. " +
+ "Returning $type."
+ )
return type
}
+ private suspend fun getPhoneAccountIfAllowed(handle: PhoneAccountHandle): PhoneAccount? =
+ coroutineScope {
+ val telecomManager =
+ applicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
+ async(Dispatchers.IO) {
+ try {
+ telecomManager.getPhoneAccount(handle)
+ } catch (e: SecurityException) {
+ Log.i(
+ TAG,
+ "getPhoneAccountIfAllowed: Unable to resolve call extension " +
+ "type due to lack of permission."
+ )
+ null
+ }
+ }
+ .await()
+ }
+
/** Perform the operation to connect the extensions to the call. */
internal suspend fun connectExtensionSession() {
val type = resolveCallExtensionsType()
Log.d(TAG, "connectExtensionsSession: type=$type")
- // When we support EXTRAs, extensions should wrap this detail into a generic interface
- val extensions = performExchangeWithRemote()
+ var extensions: CapabilityExchangeResult? = null
try {
when (type) {
- CAPABILITY_EXCHANGE -> initializeExtensions(extensions)
- else -> Log.w(TAG, "connectExtensions: unexpected type: $type")
+ CAPABILITY_EXCHANGE,
+ UNKNOWN -> {
+ // When we support EXTRAs, extensions should wrap this detail into a generic
+ // interface
+ extensions = performExchangeWithRemote()
+ }
+ else -> {
+ Log.w(
+ TAG,
+ "connectExtensions: unexpected type: $type. Proceeding with " +
+ "no extension support"
+ )
+ }
}
+ initializeExtensions(extensions)
invokeDelegate()
waitForDestroy()
} finally {
@@ -287,11 +328,11 @@
* does not support extensions at all.
*/
private suspend fun performExchangeWithRemote(): CapabilityExchangeResult? {
- Log.d(TAG, "requestExtensions: requesting extensions from remote")
+ Log.d(TAG, "performExchangeWithRemote: requesting extensions from remote")
val extensions =
withTimeoutOrNull(CAPABILITY_EXCHANGE_TIMEOUT_MS) { registerWithRemoteService() }
if (extensions == null) {
- Log.w(TAG, "startCapabilityExchange: never received response")
+ Log.w(TAG, "performExchangeWithRemote: never received response")
}
return extensions
}
@@ -338,7 +379,7 @@
* @return the remote capabilities and Binder interface used to communicate with the remote
*/
private suspend fun registerWithRemoteService(): CapabilityExchangeResult? =
- suspendCoroutine { continuation ->
+ suspendCancellableCoroutine { continuation ->
val binder =
object : ICapabilityExchange.Stub() {
override fun beginExchange(
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
index 63c9e5b..e21ad2d 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScope.kt
@@ -97,4 +97,28 @@
initialParticipants: Set<Participant> = emptySet(),
initialActiveParticipant: Participant? = null
): ParticipantExtension
+
+ /**
+ * Adds the local call silence extension to a call, which provides the ability for this
+ * application to signal to the local call silence state to other surfaces (e.g. Android Auto)
+ *
+ * Local Call Silence means that the call should be silenced at the application layer (local
+ * silence) instead of the hardware layer (global silence). Using a local call silence over
+ * global silence is advantageous when the application wants to still receive the audio input
+ * data while not transmitting audio input data to remote users.
+ *
+ * @param initialCallSilenceState The initial call silence value at the start of the call. True,
+ * signals silence the user and do not transmit audio data to the remote users. False signals
+ * the mic is transmitting audio data at the application layer.
+ * @param onLocalSilenceUpdate This is called when the user has requested to change their
+ * silence state on a remote surface. If true, this user has requested to silence the
+ * microphone. If false, this user has unsilenced the microphone. This operation should not
+ * return until the request has been processed.
+ * @return The interface used by this application to further update the local call silence
+ * extension state to remote surfaces
+ */
+ public fun addLocalSilenceExtension(
+ initialCallSilenceState: Boolean,
+ onLocalSilenceUpdate: (suspend (Boolean) -> Unit),
+ ): LocalCallSilenceExtension
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
index ed3156e4..20b900f 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ExtensionInitializationScopeImpl.kt
@@ -64,6 +64,16 @@
return participant
}
+ override fun addLocalSilenceExtension(
+ initialCallSilenceState: Boolean,
+ onLocalSilenceUpdate: (suspend (Boolean) -> Unit)
+ ): LocalCallSilenceExtension {
+ val localSilenceExtension =
+ LocalCallSilenceExtensionImpl(initialCallSilenceState, onLocalSilenceUpdate)
+ registerExtension(onExchangeStarted = localSilenceExtension::onExchangeStarted)
+ return localSilenceExtension
+ }
+
/**
* Register an extension to be created once capability exchange begins.
*
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
index f58ce43..9eab523 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/Extensions.kt
@@ -40,4 +40,6 @@
/** Represents the [ParticipantExtension] extension */
internal const val PARTICIPANT = 1
+ /** Represents the [LocalCallSilenceExtension] extension */
+ internal const val LOCAL_CALL_SILENCE = 2
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt
new file mode 100644
index 0000000..96e8b75
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtension.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/**
+ * Add support for this remote surface to display information related to the local call silence
+ * state for this call.
+ *
+ * Local Call Silence means that the call should be silenced at the application layer (local
+ * silence) instead of the hardware layer (global silence). Using a local call silence over global
+ * silence is advantageous when the application wants to still receive the audio input data while
+ * not transmitting audio input data to remote users. This allows applications to do stuff like
+ * nudge the user when they are silenced but talking into the microphone.
+ *
+ * @see ExtensionInitializationScope.addLocalSilenceExtension
+ */
+@ExperimentalAppActions
+public interface LocalCallSilenceExtension {
+ /**
+ * Update all of the remote surfaces that the local call silence state of this call has changed.
+ *
+ * @param isSilenced The new local call silence state associated with this call.
+ */
+ public suspend fun updateIsLocallySilenced(isSilenced: Boolean)
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionImpl.kt
new file mode 100644
index 0000000..53f0a5d
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionImpl.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.util.Log
+import androidx.core.telecom.internal.CapabilityExchangeRepository
+import androidx.core.telecom.internal.LocalCallSilenceCallbackRepository
+import androidx.core.telecom.internal.LocalCallSilenceStateListenerRemote
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+@OptIn(ExperimentalAppActions::class)
+internal class LocalCallSilenceExtensionImpl(
+ private val initialSilenceState: Boolean,
+ private val onLocalSilenceUpdate: suspend (Boolean) -> Unit
+) : LocalCallSilenceExtension {
+ companion object {
+ internal const val VERSION = 1
+ val TAG: String = LocalCallSilenceExtensionImpl::class.java.simpleName
+ }
+
+ internal val isLocallySilenced: MutableStateFlow<Boolean> =
+ MutableStateFlow(initialSilenceState)
+
+ /**
+ * This method is called by the VoIP application whenever the VoIP application wants to update
+ * all the remote surfaces
+ */
+ override suspend fun updateIsLocallySilenced(isSilenced: Boolean) {
+ Log.i(TAG, "updateIsLocallySilenced: isSilenced=[$isSilenced]")
+ isLocallySilenced.emit(isSilenced)
+ }
+
+ internal fun onExchangeStarted(callbacks: CapabilityExchangeRepository): Capability {
+ callbacks.onCreateLocalCallSilenceExtension = ::onCreateLocalSilenceExtension
+ return Capability().apply {
+ featureId = Extensions.LOCAL_CALL_SILENCE
+ featureVersion = VERSION
+ supportedActions = IntArray(0)
+ }
+ }
+
+ private fun onCreateLocalSilenceExtension(
+ coroutineScope: CoroutineScope,
+ remoteActions: Set<Int>,
+ binder: LocalCallSilenceStateListenerRemote
+ ) {
+ Log.d(TAG, "onCreateLocalSilenceExtension: actions=$remoteActions")
+ // Synchronize initial state with remote
+ binder.updateIsLocallySilenced(initialSilenceState)
+ // Setup listeners for changes to state
+ isLocallySilenced
+ .drop(1) // drop the first value since the sync was already sent out
+ .onEach {
+ // send all updates to the remote surfaces
+ // VoIP --> ICS
+ binder.updateIsLocallySilenced(it)
+ }
+ .launchIn(coroutineScope)
+ // hook up the callbacks so the remote ICS can update this impl
+ val callbackRepository = LocalCallSilenceCallbackRepository(coroutineScope)
+ callbackRepository.localCallSilenceCallback = ::localCallSilenceStateChanged
+ binder.finishSync(callbackRepository.eventListener)
+ }
+
+ /**
+ * This method is the entry point when the remote surface wants to update this impl. This
+ * updates the block in the VoIP app where the extension was added.
+ */
+ private suspend fun localCallSilenceStateChanged(isSilenced: Boolean) {
+ Log.i(TAG, "localCallSilenceStateChanged: isSilenced=[$isSilenced]")
+ onLocalSilenceUpdate(isSilenced)
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemote.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemote.kt
new file mode 100644
index 0000000..4de460f
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemote.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.util.ExperimentalAppActions
+
+/**
+ * Interface used to allow the remote surface (automotive, watch, etc...) to know if the connected
+ * calling application supports the local call silence extension.
+ *
+ * Local Call Silence means that the call should be silenced at the application layer (local
+ * silence) instead of the hardware layer (global silence). Using a local call silence over global
+ * silence is advantageous when the application wants to still receive the audio input data while
+ * not transmitting audio input data to remote users.
+ */
+@ExperimentalAppActions
+public interface LocalCallSilenceExtensionRemote {
+
+ /**
+ * Whether or not the local call silence extension is supported by the calling application.
+ *
+ * If `true`, then updates about the local call silence state will be notified. If `false`, then
+ * the remote doesn't support this extension and the global silence
+ * [android.telecom.InCallService.setMuted] should be used instead.
+ *
+ * Note: Must not be queried until after [CallExtensionScope.onConnected] is called.
+ */
+ public val isSupported: Boolean
+
+ /**
+ * Request the calling application to change the local call silence state.
+ *
+ * Note: A [CallControlResult.Success] result does not mean that the local call silence state of
+ * the user has changed. It only means that the request was received by the remote application
+ * and processed.
+ *
+ * @param isSilenced `true` signals the user wants to locally silence the call.
+ * @return Whether or not the remote application received this event. This does not mean that
+ * the operation succeeded, but rather the remote received and processed the event
+ * successfully.
+ */
+ public suspend fun requestLocalCallSilenceUpdate(isSilenced: Boolean): CallControlResult
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemoteImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemoteImpl.kt
new file mode 100644
index 0000000..5e5908b
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/LocalCallSilenceExtensionRemoteImpl.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.telecom.extensions
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallControlResult
+import androidx.core.telecom.CallException
+import androidx.core.telecom.internal.CapabilityExchangeListenerRemote
+import androidx.core.telecom.internal.LocalCallSilenceActionsRemote
+import androidx.core.telecom.internal.LocalCallSilenceStateListener
+import androidx.core.telecom.util.ExperimentalAppActions
+import kotlin.coroutines.resume
+import kotlin.properties.Delegates
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalAppActions::class)
+internal class LocalCallSilenceExtensionRemoteImpl(
+ private val callScope: CoroutineScope,
+ private val onLocalSilenceStateUpdated: suspend (Boolean) -> Unit
+) : LocalCallSilenceExtensionRemote {
+
+ companion object {
+ val TAG: String = LocalCallSilenceExtensionRemoteImpl::class.java.simpleName
+ }
+
+ override var isSupported: Boolean by Delegates.notNull()
+ private val isLocallySilenced = MutableStateFlow(false)
+ private var remoteActions: ILocalSilenceActions? = null
+
+ /**
+ * This method is used by the InCallService to update the VoIP applications local call silence
+ * state.
+ */
+ override suspend fun requestLocalCallSilenceUpdate(isSilenced: Boolean): CallControlResult {
+ if (remoteActions == null) {
+ Log.i(TAG, "requestLocalCallSilenceState: remoteActions are null")
+ return CallControlResult.Error(CallException.ERROR_UNKNOWN)
+ }
+ val cb = ActionsResultCallback()
+ // this remote impl --> VoIP / Callback
+ remoteActions?.setIsLocallySilenced(isSilenced, cb)
+ val result = cb.waitForResponse()
+ Log.i(TAG, "requestLocalCallSilenceState: isSilenced= $isSilenced, result=$result")
+ return result
+ }
+
+ // NOTE: There are NO actions! Therefore there is no need to add action support OR register!
+ internal val actions
+ get() = IntArray(0)
+
+ internal suspend fun onExchangeComplete(
+ negotiatedCapability: Capability?,
+ remote: CapabilityExchangeListenerRemote?
+ ) {
+ if (negotiatedCapability == null || remote == null) {
+ Log.i(TAG, "onNegotiated: remote is not capable")
+ isSupported = false
+ return
+ }
+ isSupported = true
+
+ isLocallySilenced
+ .drop(1) // ignore the first default value
+ .onEach {
+ // This updates external extension block that the InCallService implements.
+ // see [CallExtensionScopeImpl#addLocalCallSilenceExtension] for more.
+ onLocalSilenceStateUpdated(it)
+ }
+ .launchIn(callScope)
+
+ remoteActions = connectToRemote(negotiatedCapability, remote)
+ }
+
+ private suspend fun connectToRemote(
+ negotiatedCapability: Capability,
+ remote: CapabilityExchangeListenerRemote
+ ): LocalCallSilenceActionsRemote? = suspendCancellableCoroutine { continuation ->
+ val stateListener =
+ LocalCallSilenceStateListener(
+ updateLocalCallSilence = {
+ callScope.launch {
+ // This is the first entry point when the VoIP app updates this
+ // remote impl. It is called when:
+ // - the initial sync is started
+ // - any update the VoIP app sends to this remote impl.
+
+ // This updates external extension block that the InCallService implements.
+ // see [CallExtensionScopeImpl#addLocalCallSilenceExtension] for more.
+ Log.i(TAG, "LCS_SL: updateLocalCallSilence: isSilenced=[$it]")
+ isLocallySilenced.emit(it)
+ }
+ },
+ finishSync = { remoteBinder ->
+ callScope.launch { continuation.resume(remoteBinder) }
+ }
+ )
+ remote.onCreateLocalCallSilenceExtension(
+ negotiatedCapability.featureVersion,
+ negotiatedCapability.supportedActions,
+ stateListener
+ )
+ }
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt
index 012e7bc..e264736 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/extensions/ParticipantExtensionRemoteImpl.kt
@@ -83,6 +83,7 @@
// Maps a Capability to a receiver that allows the action to register itself with a listener
// and then return a Receiver that gets called when Cap exchange completes.
private val actionInitializers = HashMap<Int, ActionExchangeResult>()
+
// Manages callbacks that are applicable to sub-actions of the Participants
private val callbacks = ParticipantStateCallbackRepository()
@@ -152,6 +153,7 @@
initializeNotSupportedActions()
return
}
+ isSupported = true
Log.d(TAG, "onNegotiated: setup updates")
initializeParticipantUpdates()
initializeActionsLocally(negotiatedCapability)
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
index a08a7a0..759e467 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/AidlExtensions.kt
@@ -17,11 +17,14 @@
package androidx.core.telecom.internal
import android.util.Log
+import androidx.core.telecom.CallException
import androidx.core.telecom.extensions.Extensions
import androidx.core.telecom.extensions.IActionsResultCallback
import androidx.core.telecom.extensions.ICallDetailsListener
import androidx.core.telecom.extensions.ICapabilityExchange
import androidx.core.telecom.extensions.ICapabilityExchangeListener
+import androidx.core.telecom.extensions.ILocalSilenceActions
+import androidx.core.telecom.extensions.ILocalSilenceStateListener
import androidx.core.telecom.extensions.IParticipantActions
import androidx.core.telecom.extensions.IParticipantStateListener
import androidx.core.telecom.extensions.Participant
@@ -118,6 +121,63 @@
}
}
+@ExperimentalAppActions
+internal class LocalCallSilenceActionsRemote(binder: ILocalSilenceActions) :
+ ILocalSilenceActions by binder
+
+@ExperimentalAppActions
+internal class LocalCallSilenceStateListenerRemote(val binder: ILocalSilenceStateListener) {
+ fun updateIsLocallySilenced(isLocallySilenced: Boolean) {
+ binder.updateIsLocallySilenced(isLocallySilenced)
+ }
+
+ fun finishSync(actions: ILocalSilenceActions) {
+ binder.finishSync(actions)
+ }
+}
+
+@ExperimentalAppActions
+internal class LocalCallSilenceCallbackRepository(coroutineScope: CoroutineScope) {
+ var localCallSilenceCallback: (suspend (Boolean) -> Unit)? = null
+
+ val eventListener =
+ object : ILocalSilenceActions.Stub() {
+ override fun setIsLocallySilenced(
+ isLocallySilenced: Boolean,
+ cb: IActionsResultCallback?
+ ) {
+ cb?.let {
+ coroutineScope.launch {
+ if (localCallSilenceCallback == null) {
+ ActionsResultCallbackRemote(cb)
+ .onFailure(
+ CallException.ERROR_UNKNOWN,
+ "localCallSilenceCallback is NULL"
+ )
+ } else {
+ localCallSilenceCallback?.invoke(isLocallySilenced)
+ ActionsResultCallbackRemote(cb).onSuccess()
+ }
+ }
+ }
+ }
+ }
+}
+
+@ExperimentalAppActions
+internal class LocalCallSilenceStateListener(
+ private val updateLocalCallSilence: (Boolean) -> Unit,
+ private val finishSync: (LocalCallSilenceActionsRemote?) -> Unit
+) : ILocalSilenceStateListener.Stub() {
+ override fun updateIsLocallySilenced(isLocallySilenced: Boolean) {
+ updateLocalCallSilence.invoke(isLocallySilenced)
+ }
+
+ override fun finishSync(cb: ILocalSilenceActions?) {
+ finishSync.invoke(cb?.let { LocalCallSilenceActionsRemote(it) })
+ }
+}
+
/**
* The remote interface used to begin capability exchange with the InCallService.
*
@@ -183,6 +243,12 @@
((CoroutineScope, Set<Int>, ParticipantStateListenerRemote) -> Unit)? =
null
+ // This is set in LocalSilenceExtensionImpl (VoIP side) in onExchangeStarted(...)
+ // callbacks.onCreateLocalCallSilenceExtension = // current impl
+ var onCreateLocalCallSilenceExtension:
+ ((CoroutineScope, Set<Int>, LocalCallSilenceStateListenerRemote) -> Unit)? =
+ null
+
val listener =
object : ICapabilityExchangeListener.Stub() {
override fun onCreateParticipantExtension(
@@ -199,6 +265,21 @@
}
}
+ override fun onCreateLocalCallSilenceExtension(
+ version: Int,
+ actions: IntArray?,
+ l: ILocalSilenceStateListener?
+ ) {
+ l?.let {
+ // called by the LocalSilenceExtensionImpl (VoIP side)
+ onCreateLocalCallSilenceExtension?.invoke(
+ connectionScope,
+ actions?.toSet() ?: emptySet(),
+ LocalCallSilenceStateListenerRemote(l)
+ )
+ }
+ }
+
override fun onCreateCallDetailsExtension(
version: Int,
actions: IntArray?,
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 2c49d7e..387307d 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -3805,6 +3805,8 @@
public static class AccessibilityNodeInfoCompat.CollectionInfoCompat {
method public int getColumnCount();
+ method public int getImportantForAccessibilityItemCount();
+ method public int getItemCount();
method public int getRowCount();
method public int getSelectionMode();
method public boolean isHierarchical();
@@ -3813,6 +3815,18 @@
field public static final int SELECTION_MODE_MULTIPLE = 2; // 0x2
field public static final int SELECTION_MODE_NONE = 0; // 0x0
field public static final int SELECTION_MODE_SINGLE = 1; // 0x1
+ field public static final int UNDEFINED = -1; // 0xffffffff
+ }
+
+ public static final class AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder {
+ ctor public AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder();
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat build();
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setColumnCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setHierarchical(boolean);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setImportantForAccessibilityItemCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setItemCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setRowCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setSelectionMode(int);
}
public static class AccessibilityNodeInfoCompat.CollectionItemInfoCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 4f16f2d..a42afa7 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -4349,6 +4349,8 @@
public static class AccessibilityNodeInfoCompat.CollectionInfoCompat {
method public int getColumnCount();
+ method public int getImportantForAccessibilityItemCount();
+ method public int getItemCount();
method public int getRowCount();
method public int getSelectionMode();
method public boolean isHierarchical();
@@ -4357,6 +4359,18 @@
field public static final int SELECTION_MODE_MULTIPLE = 2; // 0x2
field public static final int SELECTION_MODE_NONE = 0; // 0x0
field public static final int SELECTION_MODE_SINGLE = 1; // 0x1
+ field public static final int UNDEFINED = -1; // 0xffffffff
+ }
+
+ public static final class AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder {
+ ctor public AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder();
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat build();
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setColumnCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setHierarchical(boolean);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setImportantForAccessibilityItemCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setItemCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setRowCount(int);
+ method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder setSelectionMode(int);
}
public static class AccessibilityNodeInfoCompat.CollectionItemInfoCompat {
diff --git a/core/core/build.gradle b/core/core/build.gradle
index bae3ce7..d4d7840 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -33,7 +33,7 @@
api(libs.kotlinStdlib)
// We don't ship this as a public artifact, so it must remain a project-type dependency.
- annotationProcessor(projectOrArtifact(":versionedparcelable:versionedparcelable-compiler"))
+ annotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
@@ -43,7 +43,7 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.testUiautomator)
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.2")
@@ -51,8 +51,8 @@
// Including both dexmakers allows support for all API levels plus final mocking support on
// API 28+. The implementation is swapped based on the finality of the mock type. This
// delegation is handled manually inside androidx.core.util.mockito.CustomMockMaker.
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0") {
exclude group: "androidx.core", module: "core"
}
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
import androidx.test.filters.MediumTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -365,6 +366,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueOne() throws Throwable {
initStatics();
@@ -386,6 +388,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueMultiple() throws Throwable {
initStatics();
@@ -410,6 +413,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueSubWork() throws Throwable {
initStatics();
@@ -439,6 +443,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
@RequiresApi(26)
public void testStopWhileWorking() throws Throwable {
if (Build.VERSION.SDK_INT < 26) {
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index 2305523..034e5a9 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -16,6 +16,8 @@
package androidx.core.view.accessibility;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.UNDEFINED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
@@ -53,6 +55,44 @@
}
@Test
+ public void testCollectionInfoBuilder_withDefaultValues() {
+ AccessibilityNodeInfoCompat.CollectionInfoCompat info =
+ new AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder()
+ .setRowCount(4)
+ .setColumnCount(1)
+ .setHierarchical(false)
+ .setSelectionMode(1)
+ .build();
+ assertThat(info.getRowCount()).isEqualTo(4);
+ assertThat(info.getColumnCount()).isEqualTo(1);
+ assertThat(info.isHierarchical()).isFalse();
+ assertThat(info.getSelectionMode()).isEqualTo(1);
+ assertThat(info.getItemCount()).isEqualTo(UNDEFINED);
+ assertThat(info.getImportantForAccessibilityItemCount()).isEqualTo(UNDEFINED);
+ }
+
+ @Test
+ public void testCollectionInfoBuilder_withRealValues() {
+ AccessibilityNodeInfoCompat.CollectionInfoCompat info =
+ new AccessibilityNodeInfoCompat.CollectionInfoCompat.Builder()
+ .setRowCount(4)
+ .setColumnCount(1)
+ .setHierarchical(false)
+ .setSelectionMode(1)
+ .setItemCount(4)
+ .setImportantForAccessibilityItemCount(3)
+ .build();
+ assertThat(info.getRowCount()).isEqualTo(4);
+ assertThat(info.getColumnCount()).isEqualTo(1);
+ assertThat(info.isHierarchical()).isFalse();
+ assertThat(info.getSelectionMode()).isEqualTo(1);
+ if (Build.VERSION.SDK_INT >= 35) {
+ assertThat(info.getItemCount()).isEqualTo(4);
+ assertThat(info.getImportantForAccessibilityItemCount()).isEqualTo(3);
+ }
+ }
+
+ @Test
public void testSetCollectionItemInfoIsNullable() {
AccessibilityNodeInfoCompat accessibilityNodeInfoCompat = obtainedWrappedNodeCompat();
accessibilityNodeInfoCompat.setCollectionItemInfo(null);
@@ -75,7 +115,7 @@
}
@Test
- public void testSetCollectionInfoCompatBuilder_withRealValues() {
+ public void testSetCollectionItemInfoCompatBuilder_withRealValues() {
AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfoCompat =
new AccessibilityNodeInfoCompat.CollectionItemInfoCompat.Builder()
.setColumnIndex(2)
@@ -93,9 +133,7 @@
assertThat(collectionItemInfoCompat.getRowTitle()).isEqualTo("Row title");
}
- if (Build.VERSION.SDK_INT >= 21) {
- assertThat(collectionItemInfoCompat.isSelected()).isTrue();
- }
+ assertThat(collectionItemInfoCompat.isSelected()).isTrue();
assertThat(collectionItemInfoCompat.getColumnIndex()).isEqualTo(2);
assertThat(collectionItemInfoCompat.getColumnSpan()).isEqualTo(1);
@@ -276,44 +314,35 @@
try {
AccessibilityActionCompat actionCompat;
actionCompat = AccessibilityActionCompat.ACTION_SHOW_ON_SCREEN;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionShowOnScreen));
+ assertThat(actionCompat.getId()).isEqualTo(
+ android.R.id.accessibilityActionShowOnScreen);
actionCompat = AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION;
assertThat(actionCompat.getId()).isEqualTo(
- getExpectedActionId(android.R.id.accessibilityActionScrollToPosition));
+ android.R.id.accessibilityActionScrollToPosition);
actionCompat = AccessibilityActionCompat.ACTION_SCROLL_UP;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionScrollUp));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionScrollUp);
actionCompat = AccessibilityActionCompat.ACTION_SCROLL_LEFT;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionScrollLeft));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionScrollLeft);
actionCompat = AccessibilityActionCompat.ACTION_SCROLL_DOWN;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionScrollDown));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionScrollDown);
actionCompat = AccessibilityActionCompat.ACTION_SCROLL_RIGHT;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionScrollRight));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionScrollRight);
actionCompat = AccessibilityActionCompat.ACTION_CONTEXT_CLICK;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionContextClick));
+ assertThat(actionCompat.getId()).isEqualTo(
+ android.R.id.accessibilityActionContextClick);
actionCompat = AccessibilityActionCompat.ACTION_SET_PROGRESS;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionSetProgress));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionSetProgress);
actionCompat = AccessibilityActionCompat.ACTION_MOVE_WINDOW;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionMoveWindow));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionMoveWindow);
actionCompat = AccessibilityActionCompat.ACTION_SHOW_TOOLTIP;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionShowTooltip));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionShowTooltip);
actionCompat = AccessibilityActionCompat.ACTION_HIDE_TOOLTIP;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionHideTooltip));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionHideTooltip);
actionCompat = AccessibilityActionCompat.ACTION_PRESS_AND_HOLD;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionPressAndHold));
+ assertThat(actionCompat.getId()).isEqualTo(
+ android.R.id.accessibilityActionPressAndHold);
actionCompat = AccessibilityActionCompat.ACTION_IME_ENTER;
- assertThat(actionCompat.getId())
- .isEqualTo(getExpectedActionId(android.R.id.accessibilityActionImeEnter));
+ assertThat(actionCompat.getId()).isEqualTo(android.R.id.accessibilityActionImeEnter);
} catch (NullPointerException e) {
Assert.fail("Expected no NullPointerException, but got: " + e.getMessage());
}
@@ -355,7 +384,6 @@
}
}
- @SdkSuppress(minSdkVersion = 21)
@Test
public void testWrappedActionEqualsStaticAction() {
// Static AccessibilityActionCompat
@@ -368,7 +396,6 @@
assertThat(staticAction.hashCode() == wrappedAction.hashCode()).isTrue();
}
- @SdkSuppress(minSdkVersion = 21)
@Test
public void testActionIdAndLabelEqualsStaticAction() {
AccessibilityActionCompat staticAction =
@@ -380,7 +407,6 @@
assertThat(staticAction.hashCode() == wrappedIdAndLabelAction.hashCode()).isTrue();
}
- @SdkSuppress(minSdkVersion = 21)
@Test
public void testDifferentActionIdsNotEquals() {
AccessibilityActionCompat staticLongClickAction =
@@ -411,10 +437,6 @@
return AccessibilityNodeInfoCompat.wrap(accessibilityNodeInfo);
}
- private int getExpectedActionId(int id) {
- return Build.VERSION.SDK_INT >= 21 ? id : 0;
- }
-
@SdkSuppress(minSdkVersion = 26)
@SmallTest
@Test
@@ -451,8 +473,8 @@
public void testActionScrollInDirection() {
AccessibilityActionCompat actionCompat =
AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION;
- assertThat(actionCompat.getId()).isEqualTo(getExpectedActionId(
- android.R.id.accessibilityActionScrollInDirection));
+ assertThat(actionCompat.getId()).isEqualTo(
+ android.R.id.accessibilityActionScrollInDirection);
assertThat(actionCompat.toString()).isEqualTo("AccessibilityActionCompat: "
+ "ACTION_SCROLL_IN_DIRECTION");
}
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 2dfb577..d28602c 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -95,12 +95,11 @@
* <p>
* <b>Defining a FileProvider</b>
* <p>
- * Extend FileProvider with a default constructor, and call super with an XML resource file that
- * specifies the available files (see below for the structure of the XML file):
+ * Extend FileProvider with a default constructor:
* <pre class="prettyprint">
* public class MyFileProvider extends FileProvider {
* public MyFileProvider() {
- * super(R.xml.file_paths)
+ * ...
* }
* }
* </pre>
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 7fed70d..31c46a8 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
@@ -63,7 +63,6 @@
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -793,7 +792,7 @@
Class<? extends CommandArguments> viewCommandArgumentClass) {
mId = id;
mCommand = command;
- if (Build.VERSION.SDK_INT >= 21 && action == null) {
+ if (action == null) {
mAction = new AccessibilityNodeInfo.AccessibilityAction(id, label);
} else {
mAction = action;
@@ -807,11 +806,7 @@
* @return The action id.
*/
public int getId() {
- if (Build.VERSION.SDK_INT >= 21) {
- return ((AccessibilityNodeInfo.AccessibilityAction) mAction).getId();
- } else {
- return 0;
- }
+ return ((AccessibilityNodeInfo.AccessibilityAction) mAction).getId();
}
/**
@@ -821,11 +816,7 @@
* @return The label.
*/
public CharSequence getLabel() {
- if (Build.VERSION.SDK_INT >= 21) {
- return ((AccessibilityNodeInfo.AccessibilityAction) mAction).getLabel();
- } else {
- return null;
- }
+ return ((AccessibilityNodeInfo.AccessibilityAction) mAction).getLabel();
}
/**
@@ -971,6 +962,14 @@
/** Selection mode where multiple items may be selected. */
public static final int SELECTION_MODE_MULTIPLE = 2;
+ /**
+ * Constant to denote a missing collection count.
+ *
+ * This should be used for {@code mItemCount} and
+ * {@code mImportantForAccessibilityItemCount} when values for those fields are not known.
+ */
+ public static final int UNDEFINED = AccessibilityNodeInfo.CollectionInfo.UNDEFINED;
+
final Object mInfo;
/**
@@ -990,13 +989,8 @@
*/
public static CollectionInfoCompat obtain(int rowCount, int columnCount,
boolean hierarchical, int selectionMode) {
- if (Build.VERSION.SDK_INT >= 21) {
- return new CollectionInfoCompat(AccessibilityNodeInfo.CollectionInfo.obtain(
- rowCount, columnCount, hierarchical, selectionMode));
- } else {
- return new CollectionInfoCompat(AccessibilityNodeInfo.CollectionInfo.obtain(
- rowCount, columnCount, hierarchical));
- }
+ return new CollectionInfoCompat(AccessibilityNodeInfo.CollectionInfo.obtain(
+ rowCount, columnCount, hierarchical, selectionMode));
}
/**
@@ -1056,10 +1050,135 @@
* </ul>
*/
public int getSelectionMode() {
- if (Build.VERSION.SDK_INT >= 21) {
- return ((AccessibilityNodeInfo.CollectionInfo) mInfo).getSelectionMode();
- } else {
- return 0;
+ return ((AccessibilityNodeInfo.CollectionInfo) mInfo).getSelectionMode();
+ }
+
+ /**
+ * Gets the number of items in the collection.
+ *
+ * @return The count of items, which may be {@code UNDEFINED} if the count is not known.
+ */
+ public int getItemCount() {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.getItemCount(mInfo);
+ }
+ return UNDEFINED;
+ }
+
+ /**
+ * Gets the number of items in the collection considered important for accessibility.
+ *
+ * @return The count of items important for accessibility, which may be {@code UNDEFINED}
+ * if the count is not known.
+ */
+ public int getImportantForAccessibilityItemCount() {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.getImportantForAccessibilityItemCount(mInfo);
+ }
+ return UNDEFINED;
+ }
+
+ /**
+ * Class for building {@link CollectionInfoCompat} objects.
+ */
+ public static final class Builder {
+ private int mRowCount = 0;
+ private int mColumnCount = 0;
+ private boolean mHierarchical = false;
+ private int mSelectionMode;
+ private int mItemCount = AccessibilityNodeInfo.CollectionInfo.UNDEFINED;
+ private int mImportantForAccessibilityItemCount =
+ AccessibilityNodeInfo.CollectionInfo.UNDEFINED;
+
+ /**
+ * Creates a new Builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Sets the row count.
+ * @param rowCount The number of rows in the collection.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setRowCount(int rowCount) {
+ mRowCount = rowCount;
+ return this;
+ }
+
+ /**
+ * Sets the column count.
+ * @param columnCount The number of columns in the collection.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setColumnCount(int columnCount) {
+ mColumnCount = columnCount;
+ return this;
+ }
+ /**
+ * Sets whether the collection is hierarchical.
+ * @param hierarchical Whether the collection is hierarchical.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setHierarchical(boolean hierarchical) {
+ mHierarchical = hierarchical;
+ return this;
+ }
+
+ /**
+ * Sets the selection mode.
+ * @param selectionMode The selection mode.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setSelectionMode(int selectionMode) {
+ mSelectionMode = selectionMode;
+ return this;
+ }
+
+ /**
+ * Sets the number of items in the collection. Can be optionally set for ViewGroups with
+ * clear row and column semantics; should be set for all other clients.
+ *
+ * @param itemCount The number of items in the collection. This should be set to
+ * {@code UNDEFINED} if the item count is not known.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setItemCount(int itemCount) {
+ mItemCount = itemCount;
+ return this;
+ }
+
+ /**
+ * Sets the number of views considered important for accessibility.
+ * @param importantForAccessibilityItemCount The number of items important for
+ * accessibility.
+ * @return This builder.
+ */
+ @NonNull
+ public CollectionInfoCompat.Builder setImportantForAccessibilityItemCount(
+ int importantForAccessibilityItemCount) {
+ mImportantForAccessibilityItemCount = importantForAccessibilityItemCount;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CollectionInfoCompat} instance.
+ */
+ @NonNull
+ public CollectionInfoCompat build() {
+ if (Build.VERSION.SDK_INT >= 35) {
+ return Api35Impl.buildCollectionInfoCompat(mRowCount, mColumnCount,
+ mHierarchical, mSelectionMode, mItemCount,
+ mImportantForAccessibilityItemCount);
+ }
+
+ return CollectionInfoCompat.obtain(mRowCount, mColumnCount, mHierarchical,
+ mSelectionMode);
}
}
}
@@ -1093,13 +1212,8 @@
*/
public static CollectionItemInfoCompat obtain(int rowIndex, int rowSpan,
int columnIndex, int columnSpan, boolean heading, boolean selected) {
- if (Build.VERSION.SDK_INT >= 21) {
- return new CollectionItemInfoCompat(AccessibilityNodeInfo.CollectionItemInfo.obtain(
- rowIndex, rowSpan, columnIndex, columnSpan, heading, selected));
- } else {
- return new CollectionItemInfoCompat(AccessibilityNodeInfo.CollectionItemInfo.obtain(
- rowIndex, rowSpan, columnIndex, columnSpan, heading));
- }
+ return new CollectionItemInfoCompat(AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ rowIndex, rowSpan, columnIndex, columnSpan, heading, selected));
}
/**
@@ -1179,11 +1293,7 @@
* @return If the item is selected.
*/
public boolean isSelected() {
- if (Build.VERSION.SDK_INT >= 21) {
- return ((AccessibilityNodeInfo.CollectionItemInfo) mInfo).isSelected();
- } else {
- return false;
- }
+ return ((AccessibilityNodeInfo.CollectionItemInfo) mInfo).isSelected();
}
/**
@@ -1337,14 +1447,10 @@
if (Build.VERSION.SDK_INT >= 33) {
return Api33Impl.buildCollectionItemInfoCompat(mHeading, mColumnIndex,
mRowIndex, mColumnSpan, mRowSpan, mSelected, mRowTitle, mColumnTitle);
- } else if (Build.VERSION.SDK_INT >= 21) {
- return Api21Impl.createCollectionItemInfo(mRowIndex, mRowSpan, mColumnIndex,
- mColumnSpan, mHeading, mSelected);
} else {
return new CollectionItemInfoCompat(
AccessibilityNodeInfo.CollectionItemInfo.obtain(mRowIndex, mRowSpan,
- mColumnIndex,
- mColumnSpan, mHeading));
+ mColumnIndex, mColumnSpan, mHeading, mSelected));
}
}
}
@@ -2483,11 +2589,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public boolean removeChild(View child) {
- if (Build.VERSION.SDK_INT >= 21) {
- return mInfo.removeChild(child);
- } else {
- return false;
- }
+ return mInfo.removeChild(child);
}
/**
@@ -2501,11 +2603,7 @@
* @see #addChild(View, int)
*/
public boolean removeChild(View root, int virtualDescendantId) {
- if (Build.VERSION.SDK_INT >= 21) {
- return mInfo.removeChild(root, virtualDescendantId);
- } else {
- return false;
- }
+ return mInfo.removeChild(root, virtualDescendantId);
}
/**
@@ -2559,16 +2657,9 @@
*
* @param action The action.
* @throws IllegalStateException If called from an AccessibilityService.
- * <p>
- * Compatibility:
- * <ul>
- * <li>API < 21: No-op</li>
- * </ul>
*/
public void addAction(AccessibilityActionCompat action) {
- if (Build.VERSION.SDK_INT >= 21) {
- mInfo.addAction((AccessibilityNodeInfo.AccessibilityAction) action.mAction);
- }
+ mInfo.addAction((AccessibilityNodeInfo.AccessibilityAction) action.mAction);
}
/**
@@ -2584,18 +2675,9 @@
* @return The action removed from the list of actions.
*
* @throws IllegalStateException If called from an AccessibilityService.
- * <p>
- * Compatibility:
- * <ul>
- * <li>API < 21: Always returns {@code false}</li>
- * </ul>
*/
public boolean removeAction(AccessibilityActionCompat action) {
- if (Build.VERSION.SDK_INT >= 21) {
- return mInfo.removeAction((AccessibilityNodeInfo.AccessibilityAction) action.mAction);
- } else {
- return false;
- }
+ return mInfo.removeAction((AccessibilityNodeInfo.AccessibilityAction) action.mAction);
}
/**
@@ -3898,29 +3980,17 @@
* Gets the actions that can be performed on the node.
*
* @return A list of AccessibilityActions.
- * <p>
- * Compatibility:
- * <ul>
- * <li>API < 21: Always returns {@code null}</li>
- * </ul>
*/
@SuppressWarnings({"unchecked", "MixedMutabilityReturnType"})
public List<AccessibilityActionCompat> getActionList() {
- List<Object> actions = null;
- if (Build.VERSION.SDK_INT >= 21) {
- actions = (List<Object>) (List<?>) mInfo.getActionList();
+ List<Object> actions = (List<Object>) (List<?>) mInfo.getActionList();
+ List<AccessibilityActionCompat> result = new ArrayList<>();
+ final int actionCount = actions.size();
+ for (int i = 0; i < actionCount; i++) {
+ Object action = actions.get(i);
+ result.add(new AccessibilityActionCompat(action));
}
- if (actions != null) {
- List<AccessibilityActionCompat> result = new ArrayList<AccessibilityActionCompat>();
- final int actionCount = actions.size();
- for (int i = 0; i < actionCount; i++) {
- Object action = actions.get(i);
- result.add(new AccessibilityActionCompat(action));
- }
- return result;
- } else {
- return Collections.<AccessibilityActionCompat>emptyList();
- }
+ return result;
}
/**
@@ -4026,9 +4096,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setError(CharSequence error) {
- if (Build.VERSION.SDK_INT >= 21) {
- mInfo.setError(error);
- }
+ mInfo.setError(error);
}
/**
@@ -4037,11 +4105,7 @@
* @return The error text.
*/
public CharSequence getError() {
- if (Build.VERSION.SDK_INT >= 21) {
- return mInfo.getError();
- } else {
- return null;
- }
+ return mInfo.getError();
}
/**
@@ -4276,9 +4340,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setMaxTextLength(int max) {
- if (Build.VERSION.SDK_INT >= 21) {
- mInfo.setMaxTextLength(max);
- }
+ mInfo.setMaxTextLength(max);
}
/**
@@ -4288,11 +4350,7 @@
* @see #setMaxTextLength(int)
*/
public int getMaxTextLength() {
- if (Build.VERSION.SDK_INT >= 21) {
- return mInfo.getMaxTextLength();
- } else {
- return -1;
- }
+ return mInfo.getMaxTextLength();
}
/**
@@ -4466,11 +4524,7 @@
* @see android.accessibilityservice.AccessibilityService#getWindows()
*/
public AccessibilityWindowInfoCompat getWindow() {
- if (Build.VERSION.SDK_INT >= 21) {
- return AccessibilityWindowInfoCompat.wrapNonNullInstance(mInfo.getWindow());
- } else {
- return null;
- }
+ return AccessibilityWindowInfoCompat.wrapNonNullInstance(mInfo.getWindow());
}
/**
@@ -5026,27 +5080,16 @@
builder.append("; accessibilityDataSensitive: ").append(isAccessibilityDataSensitive());
builder.append("; [");
- if (Build.VERSION.SDK_INT >= 21) {
- List<AccessibilityActionCompat> actions = getActionList();
- for (int i = 0; i < actions.size(); i++) {
- AccessibilityActionCompat action = actions.get(i);
- String actionName = getActionSymbolicName(action.getId());
- if (actionName.equals("ACTION_UNKNOWN") && action.getLabel() != null) {
- actionName = action.getLabel().toString();
- }
- builder.append(actionName);
- if (i != actions.size() - 1) {
- builder.append(", ");
- }
+ List<AccessibilityActionCompat> actions = getActionList();
+ for (int i = 0; i < actions.size(); i++) {
+ AccessibilityActionCompat action = actions.get(i);
+ String actionName = getActionSymbolicName(action.getId());
+ if (actionName.equals("ACTION_UNKNOWN") && action.getLabel() != null) {
+ actionName = action.getLabel().toString();
}
- } else {
- for (int actionBits = getActions(); actionBits != 0;) {
- final int action = 1 << Integer.numberOfTrailingZeros(actionBits);
- actionBits &= ~action;
- builder.append(getActionSymbolicName(action));
- if (actionBits != 0) {
- builder.append(", ");
- }
+ builder.append(actionName);
+ if (i != actions.size() - 1) {
+ builder.append(", ");
}
}
builder.append("]");
@@ -5161,20 +5204,6 @@
}
}
- @RequiresApi(21)
- private static class Api21Impl {
- private Api21Impl() {
- // This class is non instantiable.
- }
-
- public static CollectionItemInfoCompat createCollectionItemInfo(int rowIndex, int rowSpan,
- int columnIndex, int columnSpan, boolean heading, boolean selected) {
- return new CollectionItemInfoCompat(
- AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex,
- columnSpan, heading, selected));
- }
- }
-
@RequiresApi(30)
private static class Api30Impl {
private Api30Impl() {
@@ -5318,4 +5347,33 @@
return AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_IN_DIRECTION;
}
}
+
+ @RequiresApi(35)
+ private static class Api35Impl {
+ private Api35Impl() {
+ // This class is non instantiable.
+ }
+
+ public static int getItemCount(Object info) {
+ return ((AccessibilityNodeInfo.CollectionInfo) info).getItemCount();
+ }
+
+ public static int getImportantForAccessibilityItemCount(Object info) {
+ return ((AccessibilityNodeInfo.CollectionInfo) info)
+ .getImportantForAccessibilityItemCount();
+ }
+
+ public static CollectionInfoCompat buildCollectionInfoCompat(int rowCount, int columnCount,
+ boolean hierarchical, int selectionMode, int itemCount,
+ int importantForAccessibilityItemCount) {
+ return new CollectionInfoCompat.Builder()
+ .setRowCount(rowCount)
+ .setColumnCount(columnCount)
+ .setHierarchical(hierarchical)
+ .setSelectionMode(selectionMode)
+ .setItemCount(itemCount)
+ .setImportantForAccessibilityItemCount(importantForAccessibilityItemCount)
+ .build();
+ }
+ }
}
diff --git a/core/haptics/haptics/build.gradle b/core/haptics/haptics/build.gradle
index 044ca4b..5d5ca3c 100644
--- a/core/haptics/haptics/build.gradle
+++ b/core/haptics/haptics/build.gradle
@@ -34,8 +34,8 @@
api(libs.kotlinStdlib)
implementation("androidx.annotation:annotation:1.8.1")
- implementation(projectOrArtifact(":core:core"))
- implementation(projectOrArtifact(":media:media"))
+ implementation(project(":core:core"))
+ implementation(project(":media:media"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 9e4e064..0e2e133 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -17,9 +17,9 @@
}
public abstract class CreateCredentialRequest {
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getCredentialData();
method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
@@ -40,16 +40,16 @@
}
public static final class CreateCredentialRequest.Companion {
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -58,7 +58,7 @@
}
public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
}
public abstract class CreateCredentialResponse {
@@ -139,8 +139,8 @@
}
public abstract class Credential {
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
+ method @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
method public final android.os.Bundle getData();
method public final String getType();
property public final android.os.Bundle data;
@@ -149,8 +149,8 @@
}
public static final class Credential.Companion {
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
+ method @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
}
public interface CredentialManager {
@@ -184,8 +184,8 @@
}
public abstract class CredentialOption {
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
+ method @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getRequestData();
@@ -208,8 +208,8 @@
}
public static final class CredentialOption.Companion {
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
+ method @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
}
public interface CredentialProvider {
@@ -245,13 +245,13 @@
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
method public boolean getPreferIdentityDocUi();
method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method @Discouraged(message="It should only be used by OEM services and library groups") public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ method public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
method public boolean preferImmediatelyAvailableCredentials();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
property public final String? origin;
@@ -273,9 +273,9 @@
}
public static final class GetCredentialRequest.Companion {
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
- method @Discouraged(message="It should only be used by OEM services and library groups") public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ method @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
}
public final class GetCredentialResponse {
@@ -933,6 +933,10 @@
property public final String packageName;
property @RequiresApi(28) public final android.content.pm.SigningInfo signingInfo;
property public final androidx.credentials.provider.SigningInfoCompat signingInfoCompat;
+ field public static final androidx.credentials.provider.CallingAppInfo.Companion Companion;
+ }
+
+ public static final class CallingAppInfo.Companion {
}
@RequiresApi(23) public final class CreateEntry {
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 9e4e064..0e2e133 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -17,9 +17,9 @@
}
public abstract class CreateCredentialRequest {
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @RequiresApi(34) public static final androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getCredentialData();
method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
@@ -40,16 +40,16 @@
}
public static final class CreateCredentialRequest.Companion {
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
- method @Discouraged(message="It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
+ method @RequiresApi(34) public androidx.credentials.CreateCredentialRequest createFrom(android.credentials.CreateCredentialRequest request);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
}
public static final class CreateCredentialRequest.DisplayInfo {
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
- method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
method public CharSequence? getUserDisplayName();
method public CharSequence getUserId();
property public final CharSequence? userDisplayName;
@@ -58,7 +58,7 @@
}
public static final class CreateCredentialRequest.DisplayInfo.Companion {
- method @Discouraged(message="It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor") @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
+ method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
}
public abstract class CreateCredentialResponse {
@@ -139,8 +139,8 @@
}
public abstract class Credential {
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
+ method @RequiresApi(34) public static final androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method public static final androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
method public final android.os.Bundle getData();
method public final String getType();
property public final android.os.Bundle data;
@@ -149,8 +149,8 @@
}
public static final class Credential.Companion {
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
- method @Discouraged(message="It is recommended to construct a Credential by directly instantiating a Credential subclass") public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
+ method @RequiresApi(34) public androidx.credentials.Credential createFrom(android.credentials.Credential credential);
+ method public androidx.credentials.Credential createFrom(String type, android.os.Bundle data);
}
public interface CredentialManager {
@@ -184,8 +184,8 @@
}
public abstract class CredentialOption {
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
+ method @RequiresApi(34) public static final androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method public static final androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
method public final android.os.Bundle getCandidateQueryData();
method public final android.os.Bundle getRequestData();
@@ -208,8 +208,8 @@
}
public static final class CredentialOption.Companion {
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
- method @Discouraged(message="It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass") public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
+ method @RequiresApi(34) public androidx.credentials.CredentialOption createFrom(android.credentials.CredentialOption option);
+ method public androidx.credentials.CredentialOption createFrom(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, java.util.Set<android.content.ComponentName> allowedProviders);
}
public interface CredentialProvider {
@@ -245,13 +245,13 @@
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method @RequiresApi(34) public static androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method public static androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
method public String? getOrigin();
method public boolean getPreferIdentityDocUi();
method public android.content.ComponentName? getPreferUiBrandingComponentName();
- method @Discouraged(message="It should only be used by OEM services and library groups") public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ method public static android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
method public boolean preferImmediatelyAvailableCredentials();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
property public final String? origin;
@@ -273,9 +273,9 @@
}
public static final class GetCredentialRequest.Companion {
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
- method @Discouraged(message="It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest") public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
- method @Discouraged(message="It should only be used by OEM services and library groups") public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
+ method @RequiresApi(34) public androidx.credentials.GetCredentialRequest createFrom(android.credentials.GetCredentialRequest request);
+ method public androidx.credentials.GetCredentialRequest createFrom(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, String? origin, android.os.Bundle metadata);
+ method public android.os.Bundle getRequestMetadataBundle(androidx.credentials.GetCredentialRequest request);
}
public final class GetCredentialResponse {
@@ -933,6 +933,10 @@
property public final String packageName;
property @RequiresApi(28) public final android.content.pm.SigningInfo signingInfo;
property public final androidx.credentials.provider.SigningInfoCompat signingInfoCompat;
+ field public static final androidx.credentials.provider.CallingAppInfo.Companion Companion;
+ }
+
+ public static final class CallingAppInfo.Companion {
}
@RequiresApi(23) public final class CreateEntry {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index fa9b7ee..a1fa61d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -19,7 +19,6 @@
import android.graphics.drawable.Icon
import android.os.Bundle
import android.text.TextUtils
-import androidx.annotation.Discouraged
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
@@ -177,14 +176,15 @@
/**
* Returns a RequestDisplayInfo from a [CreateCredentialRequest.credentialData] Bundle.
*
+ * It is recommended to construct a DisplayInfo by direct constructor calls, instead of
+ * using this API. This API should only be used by a small subset of system apps that
+ * reconstruct an existing object for user interactions such as collecting consents.
+ *
* @param from the raw display data in the Bundle format, retrieved from
* [CreateCredentialRequest.credentialData]
*/
@JvmStatic
@RequiresApi(23) // Icon dependency
- @Discouraged(
- "It is recommended to construct a DisplayInfo by directly using the DisplayInfo constructor"
- )
fun createFrom(from: Bundle): DisplayInfo {
return try {
val displayInfoBundle = from.getBundle(BUNDLE_KEY_REQUEST_DISPLAY_INFO)!!
@@ -215,13 +215,15 @@
/**
* Parses the [request] into an instance of [CreateCredentialRequest].
*
+ * It is recommended to construct a CreateCredentialRequest by directly instantiating a
+ * CreateCredentialRequest subclass, instead of using this API. This API should only be used
+ * by a small subset of system apps that reconstruct an existing object for user
+ * interactions such as collecting consents.
+ *
* @param request the framework CreateCredentialRequest object
*/
@JvmStatic
@RequiresApi(34)
- @Discouraged(
- "It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass"
- )
fun createFrom(
request: android.credentials.CreateCredentialRequest
): CreateCredentialRequest {
@@ -238,6 +240,11 @@
* Attempts to parse the raw data into one of [CreatePasswordRequest],
* [CreatePublicKeyCredentialRequest], and [CreateCustomCredentialRequest].
*
+ * It is recommended to construct a CreateCredentialRequest by directly instantiating a
+ * CreateCredentialRequest subclass, instead of using this API. This API should only be used
+ * by a small subset of system apps that reconstruct an existing object for user
+ * interactions such as collecting consents.
+ *
* @param type matches [CreateCredentialRequest.type]
* @param credentialData matches [CreateCredentialRequest.credentialData], the request data
* in the [Bundle] format; this should be constructed and retrieved from the a given
@@ -254,9 +261,6 @@
@JvmStatic
@JvmOverloads
@RequiresApi(23)
- @Discouraged(
- "It is recommended to construct a CreateCredentialRequest by directly instantiating a CreateCredentialRequest subclass"
- )
fun createFrom(
type: String,
credentialData: Bundle,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
index 6f20564..6f92745 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
@@ -17,7 +17,6 @@
package androidx.credentials
import android.os.Bundle
-import androidx.annotation.Discouraged
import androidx.annotation.RequiresApi
import androidx.credentials.internal.FrameworkClassParsingException
@@ -40,15 +39,17 @@
/**
* Parses the raw data into an instance of [Credential].
*
+ * It is recommended to construct a Credential by directly instantiating a Credential
+ * subclass, instead of using this API. This API should only be used by a small subset of
+ * system apps that reconstruct an existing object for user interactions such as collecting
+ * consents.
+ *
* @param type matches [Credential.type], the credential type
* @param data matches [Credential.data], the credential data in the [Bundle] format; this
* should be constructed and retrieved from the a given [Credential] itself and never be
* created from scratch
*/
@JvmStatic
- @Discouraged(
- "It is recommended to construct a Credential by directly instantiating a Credential subclass"
- )
fun createFrom(type: String, data: Bundle): Credential {
return try {
when (type) {
@@ -70,13 +71,15 @@
/**
* Parses the [credential] into an instance of [Credential].
*
+ * It is recommended to construct a Credential by directly instantiating a Credential
+ * subclass, instead of using this API. This API should only be used by a small subset of
+ * system apps that reconstruct an existing object for user interactions such as collecting
+ * consents.
+ *
* @param credential the framework Credential object
*/
@JvmStatic
@RequiresApi(34)
- @Discouraged(
- "It is recommended to construct a Credential by directly instantiating a Credential subclass"
- )
fun createFrom(credential: android.credentials.Credential): Credential {
return createFrom(credential.type, credential.data)
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
index 89d293c..3c58088 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
@@ -18,7 +18,6 @@
import android.content.ComponentName
import android.os.Bundle
-import androidx.annotation.Discouraged
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -120,13 +119,15 @@
/**
* Parses the [option] into an instance of [CredentialOption].
*
+ * It is recommended to construct a CredentialOption by directly instantiating a
+ * CredentialOption subclass, instead of using this API. This API should only be used by a
+ * small subset of system apps that reconstruct an existing object for user interactions
+ * such as collecting consents.
+ *
* @param option the framework CredentialOption object
*/
@RequiresApi(34)
@JvmStatic
- @Discouraged(
- "It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass"
- )
fun createFrom(option: android.credentials.CredentialOption): CredentialOption {
return createFrom(
option.type,
@@ -140,6 +141,11 @@
/**
* Parses the raw data into an instance of [CredentialOption].
*
+ * It is recommended to construct a CredentialOption by directly instantiating a
+ * CredentialOption subclass, instead of using this API. This API should only be used by a
+ * small subset of system apps that reconstruct an existing object for user interactions
+ * such as collecting consents.
+ *
* @param type matches [CredentialOption.type]
* @param requestData matches [CredentialOption.requestData], the request data in the
* [Bundle] format; this should be constructed and retrieved from the a given
@@ -152,9 +158,6 @@
* provider is eligible
*/
@JvmStatic
- @Discouraged(
- "It is recommended to construct a CredentialOption by directly instantiating a CredentialOption subclass"
- )
fun createFrom(
type: String,
requestData: Bundle,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index 5f49e5e..ef20a75 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -18,7 +18,6 @@
import android.content.ComponentName
import android.os.Bundle
-import androidx.annotation.Discouraged
import androidx.annotation.RequiresApi
import androidx.credentials.internal.FrameworkClassParsingException
@@ -180,11 +179,12 @@
/**
* Returns the request metadata as a `Bundle`.
*
+ * This API should only be used by OEM services and library groups.
+ *
* Note: this is not the equivalent of the complete request itself. For example, it does not
* include the request's `credentialOptions` or `origin`.
*/
@JvmStatic
- @Discouraged("It should only be used by OEM services and library groups")
fun getRequestMetadataBundle(request: GetCredentialRequest): Bundle {
val bundle = Bundle()
bundle.putBoolean(BUNDLE_KEY_PREFER_IDENTITY_DOC_UI, request.preferIdentityDocUi)
@@ -202,13 +202,14 @@
/**
* Parses the [request] into an instance of [GetCredentialRequest].
*
+ * It is recommended to construct a GetCredentialRequest by direct constructor calls,
+ * instead of using this API. This API should only be used by a small subset of system apps
+ * that reconstruct an existing object for user interactions such as collecting consents.
+ *
* @param request the framework GetCredentialRequest object
*/
@RequiresApi(34)
@JvmStatic
- @Discouraged(
- "It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest"
- )
fun createFrom(request: android.credentials.GetCredentialRequest): GetCredentialRequest {
return createFrom(
request.credentialOptions.map { CredentialOption.createFrom(it) },
@@ -220,14 +221,15 @@
/**
* Parses the raw data into an instance of [GetCredentialRequest].
*
+ * It is recommended to construct a GetCredentialRequest by direct constructor calls,
+ * instead of using this API. This API should only be used by a small subset of system apps
+ * that reconstruct an existing object for user interactions such as collecting consents.
+ *
* @param credentialOptions matches [GetCredentialRequest.credentialOptions]
* @param origin matches [GetCredentialRequest.origin]
* @param metadata request metadata serialized as a Bundle using [getRequestMetadataBundle]
*/
@JvmStatic
- @Discouraged(
- "It is recommended to construct a GetCredentialRequest by directly instantiating a GetCredentialRequest"
- )
fun createFrom(
credentialOptions: List<CredentialOption>,
origin: String?,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
index 4a9bead..61d53b7 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
@@ -23,6 +23,7 @@
import android.os.Bundle
import androidx.annotation.DeprecatedSinceApi
import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.credentials.provider.utils.PrivilegedApp
import androidx.credentials.provider.utils.RequestValidationUtil
@@ -104,7 +105,7 @@
origin: String? = null
) : this(packageName, origin, SigningInfoCompat.fromSignatures(signatures), null)
- internal companion object {
+ companion object {
/**
* Constructs an instance of [CallingAppInfo]
*
@@ -118,6 +119,7 @@
* @throws IllegalArgumentException If [packageName] is empty
*/
@RequiresApi(28)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun create(packageName: String, signingInfo: SigningInfo, origin: String? = null) =
CallingAppInfo(packageName, signingInfo, origin)
@@ -135,6 +137,7 @@
* @throws IllegalArgumentException If [packageName] is empty
*/
@DeprecatedSinceApi(28, "Use the SigningInfo based constructor instead")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun create(packageName: String, signatures: List<Signature>, origin: String? = null) =
CallingAppInfo(packageName, signatures, origin)
diff --git a/datastore/datastore-benchmark/build.gradle b/datastore/datastore-benchmark/build.gradle
index 9cfb056..3fdc300 100644
--- a/datastore/datastore-benchmark/build.gradle
+++ b/datastore/datastore-benchmark/build.gradle
@@ -32,6 +32,7 @@
dependencies {
androidTestImplementation(project(":datastore:datastore-core"))
+ androidTestImplementation(project(":datastore:datastore-guava"))
androidTestImplementation(project(":internal-testutils-datastore"))
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
@@ -40,6 +41,7 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/GuavaDataStoreSingleProcessTest.java b/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/GuavaDataStoreSingleProcessTest.java
new file mode 100644
index 0000000..dd8a6bcc
--- /dev/null
+++ b/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/GuavaDataStoreSingleProcessTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.datastore.guava.GuavaDataStore;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+public class GuavaDataStoreSingleProcessTest {
+ @Rule
+ public BenchmarkRule benchmarkRule = new BenchmarkRule();
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ private static Byte incrementByte(Byte byteIn) {
+ return ++byteIn;
+ }
+
+ private static Byte sameValueByte(Byte byteIn) {
+ return byteIn;
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ BenchmarkState state = benchmarkRule.getState();
+ while (state.keepRunning()) {
+ File testFile = tmp.newFile();
+ GuavaDataStore<Byte> store = new GuavaDataStore.Builder<Byte>(
+ new TestingSerializer(),
+ () -> testFile
+ ).build();
+
+ state.pauseTiming();
+ Assert.assertNotNull(store);
+ state.resumeTiming();
+ }
+ }
+
+ @Test
+ public void testRead() throws Exception {
+ BenchmarkState state = benchmarkRule.getState();
+ File testFile = tmp.newFile();
+ GuavaDataStore<Byte> store = new GuavaDataStore.Builder<Byte>(
+ new TestingSerializer(),
+ () -> testFile
+ ).build();
+ ListenableFuture<Byte> updateFuture = store.updateDataAsync(
+ GuavaDataStoreSingleProcessTest::incrementByte
+ );
+ assertThat(updateFuture.get()).isEqualTo(1);
+
+ while (state.keepRunning()) {
+ Byte currentData = store.getDataAsync().get();
+
+ state.pauseTiming();
+ assertThat(currentData).isEqualTo(1);
+ state.resumeTiming();
+ }
+ }
+
+ @Test
+ public void testUpdate_withoutValueChange() throws Exception {
+ BenchmarkState state = benchmarkRule.getState();
+ File testFile = tmp.newFile();
+ GuavaDataStore<Byte> store = new GuavaDataStore.Builder<Byte>(
+ new TestingSerializer(),
+ () -> testFile
+ ).build();
+ ListenableFuture<Byte> updateFuture = store.updateDataAsync(
+ GuavaDataStoreSingleProcessTest::incrementByte
+ );
+ assertThat(updateFuture.get()).isEqualTo(1);
+
+ while (state.keepRunning()) {
+ Byte updatedData = store.updateDataAsync(
+ GuavaDataStoreSingleProcessTest::sameValueByte).get();
+
+ state.pauseTiming();
+ assertThat(updatedData).isEqualTo(1);
+ state.resumeTiming();
+ }
+ }
+
+ @Test
+ public void testUpdate_withValueChange() throws Exception {
+ BenchmarkState state = benchmarkRule.getState();
+ File testFile = tmp.newFile();
+ byte counter = 0;
+ GuavaDataStore<Byte> store = new GuavaDataStore.Builder<Byte>(
+ new TestingSerializer(),
+ () -> testFile
+ ).build();
+ // first update creates the file
+ ListenableFuture<Byte> updateFuture = store.updateDataAsync(
+ GuavaDataStoreSingleProcessTest::incrementByte
+ );
+ counter++;
+ assertThat(updateFuture.get()).isEqualTo(counter);
+
+ while (state.keepRunning()) {
+ Byte updatedData = store.updateDataAsync(
+ GuavaDataStoreSingleProcessTest::incrementByte).get();
+
+ state.pauseTiming();
+ counter++;
+ assertThat(updatedData).isEqualTo(counter);
+ state.resumeTiming();
+ }
+ }
+}
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 07d5fe6..69047af 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -33,19 +33,19 @@
}
dependencies {
- compileOnly(projectOrArtifact(":datastore:datastore-preferences-external-protobuf"))
+ compileOnly(project(":datastore:datastore-preferences-external-protobuf"))
implementation(libs.protobufLite)
implementation(libs.kotlinStdlib)
implementation('androidx.core:core-ktx:1.7.0')
implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.3.1')
implementation('androidx.activity:activity-compose:1.3.1')
- implementation(projectOrArtifact(":compose:ui:ui"))
- implementation(projectOrArtifact(":compose:ui:ui-tooling-preview"))
- implementation(projectOrArtifact(":compose:material:material"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-tooling-preview"))
+ implementation(project(":compose:material:material"))
testImplementation('junit:junit:4.13.2')
- debugImplementation(projectOrArtifact(":compose:ui:ui-tooling"))
- debugImplementation(projectOrArtifact(":compose:ui:ui-test-manifest"))
+ debugImplementation(project(":compose:ui:ui-tooling"))
+ debugImplementation(project(":compose:ui:ui-test-manifest"))
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("com.google.protobuf:protobuf-javalite:3.19.4")
diff --git a/development/JetpadClient.py b/development/JetpadClient.py
index 19b74bc..88eb445 100644
--- a/development/JetpadClient.py
+++ b/development/JetpadClient.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
diff --git a/development/build_log_simplifier/build_log_simplifier.py b/development/build_log_simplifier/build_log_simplifier.py
index 7bff81e..17ba86d 100755
--- a/development/build_log_simplifier/build_log_simplifier.py
+++ b/development/build_log_simplifier/build_log_simplifier.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2016 The Android Open Source Project
#
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index d44adf5..8f9bd91 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -280,7 +280,7 @@
# 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|tvos|tvos_simulator|watchos|watchos_simulator)_(arm|x)[0-9]+\. This target will be excluded from commonization\.
+Warning\: No libraries found for target (macos|ios|ios_simulator|tvos|tvos_simulator|watchos|watchos_simulator|watchos_device)_(arm|x)[0-9]+\. This target will be excluded from commonization\.
# > 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)
# KMP messages on successful XCFramework builds.
diff --git a/development/build_log_simplifier/test.py b/development/build_log_simplifier/test.py
index 40ab24b..5c7bb3d 100755
--- a/development/build_log_simplifier/test.py
+++ b/development/build_log_simplifier/test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
diff --git a/development/copy_screenshots_to_golden_repo.py b/development/copy_screenshots_to_golden_repo.py
index 5f80cbb..1d16446 100755
--- a/development/copy_screenshots_to_golden_repo.py
+++ b/development/copy_screenshots_to_golden_repo.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
# This script helps to rename and copy the golden images required to run the screenshot tests to the golden directory.
# To generate new golden images for a test, run the test on the emulator with the required device type, and download the screenshots from the device using adb
diff --git a/development/file-utils/diff-filterer.py b/development/file-utils/diff-filterer.py
index d7d4cbb..31d9bf2 100755
--- a/development/file-utils/diff-filterer.py
+++ b/development/file-utils/diff-filterer.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2018 The Android Open Source Project
#
diff --git a/development/importMaven/import_maven_artifacts.py b/development/importMaven/import_maven_artifacts.py
index c3f6947..66f02fe 100755
--- a/development/importMaven/import_maven_artifacts.py
+++ b/development/importMaven/import_maven_artifacts.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
"""
Copyright 2018 The Android Open Source Project
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/KmpConfig.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/KmpConfig.kt
index d9e2cb5..13283e7 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/KmpConfig.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/KmpConfig.kt
@@ -27,6 +27,7 @@
KonanTarget.WATCHOS_ARM64,
KonanTarget.WATCHOS_SIMULATOR_ARM64,
KonanTarget.WATCHOS_X64,
+ KonanTarget.WATCHOS_DEVICE_ARM64,
KonanTarget.TVOS_ARM64,
KonanTarget.TVOS_SIMULATOR_ARM64,
KonanTarget.TVOS_X64,
diff --git a/development/offlinifyDocs/offlinify_dackka_docs.py b/development/offlinifyDocs/offlinify_dackka_docs.py
index 932d03c..2c16bfa 100644
--- a/development/offlinifyDocs/offlinify_dackka_docs.py
+++ b/development/offlinifyDocs/offlinify_dackka_docs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
from argparse import ArgumentParser
import os
diff --git a/development/offlinifyDocs/test_offlinify_dackka_docs.py b/development/offlinifyDocs/test_offlinify_dackka_docs.py
index c6695dd..116481c 100644
--- a/development/offlinifyDocs/test_offlinify_dackka_docs.py
+++ b/development/offlinifyDocs/test_offlinify_dackka_docs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
from filecmp import cmp, dircmp
from offlinify_dackka_docs import check_library, process_input, STYLE_FILENAME
diff --git a/development/project-creator/create_project.py b/development/project-creator/create_project.py
index f58b39e..4f4c509 100755
--- a/development/project-creator/create_project.py
+++ b/development/project-creator/create_project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
diff --git a/development/project-creator/test_project_creator.py b/development/project-creator/test_project_creator.py
index 9d3c2c8..cdf4d06 100755
--- a/development/project-creator/test_project_creator.py
+++ b/development/project-creator/test_project_creator.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
diff --git a/development/simplify-build-failure/impl/explode.py b/development/simplify-build-failure/impl/explode.py
index 3ade65a..5ea515f1 100755
--- a/development/simplify-build-failure/impl/explode.py
+++ b/development/simplify-build-failure/impl/explode.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
#
# Copyright (C) 2020 The Android Open Source Project
#
diff --git a/development/suppressFailingTests.py b/development/suppressFailingTests.py
index d9c71af..e39cf95 100755
--- a/development/suppressFailingTests.py
+++ b/development/suppressFailingTests.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
"""
Parses information about failing tests, and then generates a change to disable them.
diff --git a/development/ts.py b/development/ts.py
index f4c54d9..95e8faf 100755
--- a/development/ts.py
+++ b/development/ts.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
# This program reads stdin, prepends the current time to each line, and prints the result
from datetime import datetime
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
# update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
# Pull all UTP artifacts for ADT version
ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index d1966329..31c4e19 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -15,23 +15,23 @@
}
dependencies {
- docs("androidx.activity:activity:1.10.0-alpha01")
- docs("androidx.activity:activity-compose:1.10.0-alpha01")
- docs("androidx.activity:activity-ktx:1.10.0-alpha01")
+ docs("androidx.activity:activity:1.10.0-alpha02")
+ docs("androidx.activity:activity-compose:1.10.0-alpha02")
+ docs("androidx.activity:activity-ktx:1.10.0-alpha02")
// ads-identifier is deprecated
docsWithoutApiSince("androidx.ads:ads-identifier:1.0.0-alpha05")
docsWithoutApiSince("androidx.ads:ads-identifier-common:1.0.0-alpha05")
docsWithoutApiSince("androidx.ads:ads-identifier-provider:1.0.0-alpha05")
- kmpDocs("androidx.annotation:annotation:1.9.0-alpha02")
+ kmpDocs("androidx.annotation:annotation:1.9.0-alpha03")
docs("androidx.annotation:annotation-experimental:1.5.0-alpha01")
docs("androidx.appcompat:appcompat:1.7.0")
docs("androidx.appcompat:appcompat-resources:1.7.0")
- docs("androidx.appsearch:appsearch:1.1.0-alpha04")
- docs("androidx.appsearch:appsearch-builtin-types:1.1.0-alpha04")
- docs("androidx.appsearch:appsearch-ktx:1.1.0-alpha04")
- docs("androidx.appsearch:appsearch-local-storage:1.1.0-alpha04")
- docs("androidx.appsearch:appsearch-platform-storage:1.1.0-alpha04")
- docs("androidx.appsearch:appsearch-play-services-storage:1.1.0-alpha04")
+ docs("androidx.appsearch:appsearch:1.1.0-alpha05")
+ docs("androidx.appsearch:appsearch-builtin-types:1.1.0-alpha05")
+ docs("androidx.appsearch:appsearch-ktx:1.1.0-alpha05")
+ docs("androidx.appsearch:appsearch-local-storage:1.1.0-alpha05")
+ docs("androidx.appsearch:appsearch-platform-storage:1.1.0-alpha05")
+ docs("androidx.appsearch:appsearch-play-services-storage:1.1.0-alpha05")
docs("androidx.arch.core:core-common:2.2.0")
docs("androidx.arch.core:core-runtime:2.2.0")
docs("androidx.arch.core:core-testing:2.2.0")
@@ -47,66 +47,68 @@
docs("androidx.bluetooth:bluetooth:1.0.0-alpha02")
docs("androidx.bluetooth:bluetooth-testing:1.0.0-alpha02")
docs("androidx.browser:browser:1.8.0")
- docs("androidx.camera:camera-camera2:1.4.0-rc01")
- docs("androidx.camera:camera-core:1.4.0-rc01")
- docs("androidx.camera:camera-effects:1.4.0-rc01")
- docs("androidx.camera:camera-extensions:1.4.0-rc01")
+ docs("androidx.camera.viewfinder:viewfinder-compose:1.4.0-alpha08")
+ docs("androidx.camera.viewfinder:viewfinder-core:1.4.0-alpha08")
+ docs("androidx.camera.viewfinder:viewfinder-view:1.4.0-alpha08")
+ docs("androidx.camera:camera-camera2:1.5.0-alpha01")
+ docs("androidx.camera:camera-compose:1.5.0-alpha01")
+ samples("androidx.camera:camera-compose-samples:1.5.0-alpha01")
+ docs("androidx.camera:camera-core:1.5.0-alpha01")
+ docs("androidx.camera:camera-effects:1.5.0-alpha01")
+ docs("androidx.camera:camera-extensions:1.5.0-alpha01")
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
- docs("androidx.camera:camera-lifecycle:1.4.0-rc01")
- docs("androidx.camera:camera-mlkit-vision:1.4.0-rc01")
- docs("androidx.camera:camera-video:1.4.0-rc01")
- docs("androidx.camera:camera-view:1.4.0-rc01")
- docs("androidx.camera:camera-viewfinder:1.4.0-alpha08")
- docs("androidx.camera:camera-viewfinder-compose:1.4.0-alpha03")
- docs("androidx.camera:camera-viewfinder-core:1.4.0-alpha08")
+ docs("androidx.camera:camera-lifecycle:1.5.0-alpha01")
+ docs("androidx.camera:camera-mlkit-vision:1.5.0-alpha01")
+ docs("androidx.camera:camera-video:1.5.0-alpha01")
+ docs("androidx.camera:camera-view:1.5.0-alpha01")
docs("androidx.car.app:app:1.7.0-beta01")
docs("androidx.car.app:app-automotive:1.7.0-beta01")
docs("androidx.car.app:app-projected:1.7.0-beta01")
docs("androidx.car.app:app-testing:1.7.0-beta01")
docs("androidx.cardview:cardview:1.0.0")
- kmpDocs("androidx.collection:collection:1.4.3")
- docs("androidx.collection:collection-ktx:1.4.3")
- kmpDocs("androidx.compose.animation:animation:1.7.0-rc01")
- kmpDocs("androidx.compose.animation:animation-core:1.7.0-rc01")
- kmpDocs("androidx.compose.animation:animation-graphics:1.7.0-rc01")
- kmpDocs("androidx.compose.foundation:foundation:1.7.0-rc01")
- kmpDocs("androidx.compose.foundation:foundation-layout:1.7.0-rc01")
- kmpDocs("androidx.compose.material3.adaptive:adaptive:1.1.0-alpha01")
- kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha01")
- kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha01")
- kmpDocs("androidx.compose.material3:material3:1.3.0-rc01")
- kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0-rc01")
+ kmpDocs("androidx.collection:collection:1.5.0-alpha01")
+ docs("androidx.collection:collection-ktx:1.5.0-alpha01")
+ kmpDocs("androidx.compose.animation:animation:1.8.0-alpha01")
+ kmpDocs("androidx.compose.animation:animation-core:1.8.0-alpha01")
+ kmpDocs("androidx.compose.animation:animation-graphics:1.8.0-alpha01")
+ kmpDocs("androidx.compose.foundation:foundation:1.8.0-alpha01")
+ kmpDocs("androidx.compose.foundation:foundation-layout:1.8.0-alpha01")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive:1.1.0-alpha02")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive-layout:1.1.0-alpha02")
+ kmpDocs("androidx.compose.material3.adaptive:adaptive-navigation:1.1.0-alpha02")
+ kmpDocs("androidx.compose.material3:material3:1.3.0")
+ kmpDocs("androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0")
kmpDocs("androidx.compose.material3:material3-common:1.0.0-alpha01")
- kmpDocs("androidx.compose.material3:material3-window-size-class:1.3.0-rc01")
- kmpDocs("androidx.compose.material:material:1.7.0-rc01")
- kmpDocs("androidx.compose.material:material-icons-core:1.7.0-rc01")
- docs("androidx.compose.material:material-navigation:1.7.0-beta03")
- kmpDocs("androidx.compose.material:material-ripple:1.7.0-rc01")
- kmpDocs("androidx.compose.runtime:runtime:1.7.0-rc01")
- docs("androidx.compose.runtime:runtime-livedata:1.7.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava2:1.7.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava3:1.7.0-rc01")
- kmpDocs("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
+ kmpDocs("androidx.compose.material3:material3-window-size-class:1.3.0")
+ kmpDocs("androidx.compose.material:material:1.8.0-alpha01")
+ kmpDocs("androidx.compose.material:material-icons-core:1.7.0")
+ docs("androidx.compose.material:material-navigation:1.8.0-alpha01")
+ kmpDocs("androidx.compose.material:material-ripple:1.8.0-alpha01")
+ kmpDocs("androidx.compose.runtime:runtime:1.8.0-alpha01")
+ docs("androidx.compose.runtime:runtime-livedata:1.8.0-alpha01")
+ docs("androidx.compose.runtime:runtime-rxjava2:1.8.0-alpha01")
+ docs("androidx.compose.runtime:runtime-rxjava3:1.8.0-alpha01")
+ kmpDocs("androidx.compose.runtime:runtime-saveable:1.8.0-alpha01")
docs("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")
- kmpDocs("androidx.compose.ui:ui:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui:1.8.0-alpha01")
docs("androidx.compose.ui:ui-android-stubs:1.7.0-beta06")
- kmpDocs("androidx.compose.ui:ui-geometry:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-graphics:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-test:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-test-junit4:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-text:1.7.0-rc01")
- docs("androidx.compose.ui:ui-text-google-fonts:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-tooling:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-tooling-data:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-tooling-preview:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-unit:1.7.0-rc01")
- kmpDocs("androidx.compose.ui:ui-util:1.7.0-rc01")
- docs("androidx.compose.ui:ui-viewbinding:1.7.0-rc01")
+ kmpDocs("androidx.compose.ui:ui-geometry:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-graphics:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-test:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-test-junit4:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-text:1.8.0-alpha01")
+ docs("androidx.compose.ui:ui-text-google-fonts:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-tooling:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-tooling-data:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-tooling-preview:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-unit:1.8.0-alpha01")
+ kmpDocs("androidx.compose.ui:ui-util:1.8.0-alpha01")
+ docs("androidx.compose.ui:ui-viewbinding:1.8.0-alpha01")
docs("androidx.concurrent:concurrent-futures:1.2.0")
docs("androidx.concurrent:concurrent-futures-ktx:1.2.0")
- docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha14")
- kmpDocs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha14")
- docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha14")
+ docs("androidx.constraintlayout:constraintlayout:2.2.0-beta01")
+ kmpDocs("androidx.constraintlayout:constraintlayout-compose:1.1.0-beta01")
+ docs("androidx.constraintlayout:constraintlayout-core:1.1.0-beta01")
docs("androidx.contentpager:contentpager:1.0.0")
docs("androidx.coordinatorlayout:coordinatorlayout:1.3.0-alpha02")
docs("androidx.core:core:1.15.0-alpha02")
@@ -123,15 +125,15 @@
docs("androidx.core:core-performance-testing:1.0.0")
docs("androidx.core:core-remoteviews:1.1.0")
docs("androidx.core:core-role:1.2.0-alpha01")
- docs("androidx.core:core-splashscreen:1.2.0-alpha01")
+ docs("androidx.core:core-splashscreen:1.2.0-alpha02")
docs("androidx.core:core-telecom:1.0.0-alpha03")
docs("androidx.core:core-testing:1.15.0-alpha02")
docs("androidx.core.uwb:uwb:1.0.0-alpha08")
docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha08")
- docs("androidx.credentials:credentials:1.5.0-alpha04")
+ docs("androidx.credentials:credentials:1.5.0-alpha05")
docs("androidx.credentials:credentials-e2ee:1.0.0-alpha02")
docs("androidx.credentials:credentials-fido:1.0.0-alpha02")
- docs("androidx.credentials:credentials-play-services-auth:1.5.0-alpha04")
+ docs("androidx.credentials:credentials-play-services-auth:1.5.0-alpha05")
docs("androidx.cursoradapter:cursoradapter:1.0.0")
docs("androidx.customview:customview:1.2.0-alpha02")
// TODO(b/294531403): Turn on apiSince for customview-poolingcontainer when it releases as alpha
@@ -150,21 +152,21 @@
docs("androidx.drawerlayout:drawerlayout:1.2.0")
docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
docs("androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03")
- docs("androidx.emoji2:emoji2:1.5.0-rc01")
- docs("androidx.emoji2:emoji2-bundled:1.5.0-rc01")
- docs("androidx.emoji2:emoji2-emojipicker:1.5.0-rc01")
- docs("androidx.emoji2:emoji2-views:1.5.0-rc01")
- docs("androidx.emoji2:emoji2-views-helper:1.5.0-rc01")
+ docs("androidx.emoji2:emoji2:1.5.0")
+ docs("androidx.emoji2:emoji2-bundled:1.5.0")
+ docs("androidx.emoji2:emoji2-emojipicker:1.5.0")
+ docs("androidx.emoji2:emoji2-views:1.5.0")
+ docs("androidx.emoji2:emoji2-views-helper:1.5.0")
docs("androidx.emoji:emoji:1.2.0-alpha03")
docs("androidx.emoji:emoji-appcompat:1.2.0-alpha03")
docs("androidx.emoji:emoji-bundled:1.2.0-alpha03")
docs("androidx.enterprise:enterprise-feedback:1.1.0")
docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
docs("androidx.exifinterface:exifinterface:1.3.6")
- docs("androidx.fragment:fragment:1.8.2")
- docs("androidx.fragment:fragment-compose:1.8.2")
- docs("androidx.fragment:fragment-ktx:1.8.2")
- docs("androidx.fragment:fragment-testing:1.8.2")
+ docs("androidx.fragment:fragment:1.8.3")
+ docs("androidx.fragment:fragment-compose:1.8.3")
+ docs("androidx.fragment:fragment-ktx:1.8.3")
+ docs("androidx.fragment:fragment-testing:1.8.3")
docs("androidx.glance:glance:1.1.0")
docs("androidx.glance:glance-appwidget:1.1.0")
docs("androidx.glance:glance-appwidget-preview:1.1.0")
@@ -175,12 +177,13 @@
docs("androidx.glance:glance-template:1.0.0-alpha06")
docs("androidx.glance:glance-testing:1.1.0")
docs("androidx.glance:glance-wear-tiles:1.0.0-alpha06")
- docs("androidx.graphics:graphics-core:1.0.0")
+ docs("androidx.graphics:graphics-core:1.0.1")
docs("androidx.graphics:graphics-path:1.0.0")
- kmpDocs("androidx.graphics:graphics-shapes:1.0.0")
+ kmpDocs("androidx.graphics:graphics-shapes:1.0.1")
docs("androidx.gridlayout:gridlayout:1.1.0-beta01")
- docs("androidx.health.connect:connect-client:1.1.0-alpha07")
+ docs("androidx.health.connect:connect-client:1.1.0-alpha08")
samples("androidx.health.connect:connect-client-samples:1.1.0-alpha07")
+ docs("androidx.health.connect:connect-testing:1.0.0-alpha01")
docs("androidx.health:health-services-client:1.1.0-alpha03")
docs("androidx.heifwriter:heifwriter:1.1.0-alpha02")
docs("androidx.hilt:hilt-common:1.2.0")
@@ -196,26 +199,26 @@
docs("androidx.leanback:leanback-paging:1.1.0-alpha11")
docs("androidx.leanback:leanback-preference:1.2.0-alpha04")
docs("androidx.leanback:leanback-tab:1.1.0-beta01")
- kmpDocs("androidx.lifecycle:lifecycle-common:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-common-java8:2.9.0-alpha01")
+ kmpDocs("androidx.lifecycle:lifecycle-common:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-common-java8:2.9.0-alpha02")
docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
- docs("androidx.lifecycle:lifecycle-livedata:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-livedata-core:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-livedata-ktx:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-process:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-reactivestreams:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.9.0-alpha01")
- kmpDocs("androidx.lifecycle:lifecycle-runtime:2.9.0-alpha01")
- kmpDocs("androidx.lifecycle:lifecycle-runtime-compose:2.9.0-alpha01")
- kmpDocs("androidx.lifecycle:lifecycle-runtime-ktx:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-runtime-testing:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-service:2.9.0-alpha01")
- kmpDocs("androidx.lifecycle:lifecycle-viewmodel:2.9.0-alpha01")
- kmpDocs("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.0-alpha01")
- docs("androidx.lifecycle:lifecycle-viewmodel-testing:2.9.0-alpha01")
+ docs("androidx.lifecycle:lifecycle-livedata:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-livedata-core:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-livedata-ktx:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-process:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-reactivestreams:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-runtime:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-runtime-compose:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-runtime-ktx:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-runtime-testing:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-service:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-viewmodel:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0-alpha02")
+ docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.0-alpha02")
+ kmpDocs("androidx.lifecycle:lifecycle-viewmodel-testing:2.9.0-alpha02")
docs("androidx.loader:loader:1.1.0")
docs("androidx.media2:media2-common:1.3.0")
docs("androidx.media2:media2-player:1.3.0")
@@ -223,47 +226,48 @@
docs("androidx.media2:media2-widget:1.3.0")
docs("androidx.media:media:1.7.0")
// androidx.media3 is not hosted in androidx
- docsWithoutApiSince("androidx.media3:media3-cast:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-common:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-container:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-database:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-datasource:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-decoder:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-effect:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-extractor:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-muxer:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-session:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-test-utils:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-transformer:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-ui:1.4.1")
- docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.4.1")
+ docsWithoutApiSince("androidx.media3:media3-cast:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-common:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-common-ktx:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-container:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-database:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-decoder:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-effect:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-extractor:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-muxer:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-session:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-transformer:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-ui:1.5.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.5.0-alpha01")
docs("androidx.mediarouter:mediarouter:1.7.0")
docs("androidx.mediarouter:mediarouter-testing:1.7.0")
docs("androidx.metrics:metrics-performance:1.0.0-beta01")
- docs("androidx.navigation:navigation-common:2.8.0-rc01")
- docs("androidx.navigation:navigation-common-ktx:2.8.0-rc01")
- docs("androidx.navigation:navigation-compose:2.8.0-rc01")
- docs("androidx.navigation:navigation-dynamic-features-fragment:2.8.0-rc01")
- docs("androidx.navigation:navigation-dynamic-features-runtime:2.8.0-rc01")
- docs("androidx.navigation:navigation-fragment:2.8.0-rc01")
- docs("androidx.navigation:navigation-fragment-compose:2.8.0-rc01")
- docs("androidx.navigation:navigation-fragment-ktx:2.8.0-rc01")
- docs("androidx.navigation:navigation-runtime:2.8.0-rc01")
- docs("androidx.navigation:navigation-runtime-ktx:2.8.0-rc01")
- docs("androidx.navigation:navigation-testing:2.8.0-rc01")
- docs("androidx.navigation:navigation-ui:2.8.0-rc01")
- docs("androidx.navigation:navigation-ui-ktx:2.8.0-rc01")
+ docs("androidx.navigation:navigation-common:2.8.0")
+ docs("androidx.navigation:navigation-common-ktx:2.8.0")
+ docs("androidx.navigation:navigation-compose:2.8.0")
+ docs("androidx.navigation:navigation-dynamic-features-fragment:2.8.0")
+ docs("androidx.navigation:navigation-dynamic-features-runtime:2.8.0")
+ docs("androidx.navigation:navigation-fragment:2.8.0")
+ docs("androidx.navigation:navigation-fragment-compose:2.8.0")
+ docs("androidx.navigation:navigation-fragment-ktx:2.8.0")
+ docs("androidx.navigation:navigation-runtime:2.8.0")
+ docs("androidx.navigation:navigation-runtime-ktx:2.8.0")
+ docs("androidx.navigation:navigation-testing:2.8.0")
+ docs("androidx.navigation:navigation-ui:2.8.0")
+ docs("androidx.navigation:navigation-ui-ktx:2.8.0")
kmpDocs("androidx.paging:paging-common:3.3.1")
docs("androidx.paging:paging-common-ktx:3.3.1")
kmpDocs("androidx.paging:paging-compose:3.3.1")
@@ -276,8 +280,8 @@
kmpDocs("androidx.paging:paging-testing:3.3.1")
docs("androidx.palette:palette:1.0.0")
docs("androidx.palette:palette-ktx:1.0.0")
- docs("androidx.pdf:pdf-viewer:1.0.0-alpha01")
- docs("androidx.pdf:pdf-viewer-fragment:1.0.0-alpha01")
+ docs("androidx.pdf:pdf-viewer:1.0.0-alpha02")
+ docs("androidx.pdf:pdf-viewer-fragment:1.0.0-alpha02")
docs("androidx.percentlayout:percentlayout:1.0.1")
docs("androidx.preference:preference:1.2.1")
docs("androidx.preference:preference-ktx:1.2.1")
@@ -294,7 +298,7 @@
docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha09")
docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha09")
docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha09")
- docs("androidx.profileinstaller:profileinstaller:1.4.0-beta01")
+ docs("androidx.profileinstaller:profileinstaller:1.4.0-rc01")
docs("androidx.recommendation:recommendation:1.0.0")
docs("androidx.recyclerview:recyclerview:1.4.0-beta01")
docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
@@ -331,7 +335,7 @@
kmpDocs("androidx.sqlite:sqlite-bundled:2.5.0-alpha07")
kmpDocs("androidx.sqlite:sqlite-framework:2.5.0-alpha07")
docs("androidx.sqlite:sqlite-ktx:2.5.0-alpha07")
- docs("androidx.startup:startup-runtime:1.2.0-beta01")
+ docs("androidx.startup:startup-runtime:1.2.0-rc01")
docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
// androidx.test is not hosted in androidx
docsWithoutApiSince("androidx.test:core:1.6.1")
@@ -371,12 +375,12 @@
docs("androidx.versionedparcelable:versionedparcelable:1.2.0")
docs("androidx.viewpager2:viewpager2:1.1.0")
docs("androidx.viewpager:viewpager:1.1.0-alpha01")
- docs("androidx.wear.compose:compose-foundation:1.4.0-rc01")
- docs("androidx.wear.compose:compose-material:1.4.0-rc01")
- docs("androidx.wear.compose:compose-material-core:1.4.0-rc01")
- docs("androidx.wear.compose:compose-material3:1.0.0-alpha23")
- docs("androidx.wear.compose:compose-navigation:1.4.0-rc01")
- docs("androidx.wear.compose:compose-ui-tooling:1.4.0-rc01")
+ docs("androidx.wear.compose:compose-foundation:1.5.0-alpha01")
+ docs("androidx.wear.compose:compose-material:1.5.0-alpha01")
+ docs("androidx.wear.compose:compose-material-core:1.5.0-alpha01")
+ docs("androidx.wear.compose:compose-material3:1.0.0-alpha24")
+ docs("androidx.wear.compose:compose-navigation:1.5.0-alpha01")
+ docs("androidx.wear.compose:compose-ui-tooling:1.5.0-alpha01")
docs("androidx.wear.protolayout:protolayout:1.2.0")
docs("androidx.wear.protolayout:protolayout-expression:1.2.0")
docs("androidx.wear.protolayout:protolayout-expression-pipeline:1.2.0")
@@ -415,23 +419,23 @@
docs("androidx.wear:wear-remote-interactions:1.1.0-beta01")
samples("androidx.wear:wear-remote-interactions-samples:1.1.0-alpha02")
docs("androidx.wear:wear-tooling-preview:1.0.0")
- docs("androidx.webkit:webkit:1.12.0-beta01")
+ docs("androidx.webkit:webkit:1.12.0-rc01")
docs("androidx.window.extensions.core:core:1.0.0")
- docs("androidx.window:window:1.4.0-alpha01")
+ docs("androidx.window:window:1.4.0-alpha02")
stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
- kmpDocs("androidx.window:window-core:1.4.0-alpha01")
+ kmpDocs("androidx.window:window-core:1.4.0-alpha02")
stubs("androidx.window:window-extensions:1.0.0-alpha01")
- docs("androidx.window:window-java:1.4.0-alpha01")
- docs("androidx.window:window-rxjava2:1.4.0-alpha01")
- docs("androidx.window:window-rxjava3:1.4.0-alpha01")
- docs("androidx.window:window-testing:1.4.0-alpha01")
- docs("androidx.work:work-gcm:2.10.0-alpha02")
- docs("androidx.work:work-multiprocess:2.10.0-alpha02")
- docs("androidx.work:work-runtime:2.10.0-alpha02")
- docs("androidx.work:work-runtime-ktx:2.10.0-alpha02")
- docs("androidx.work:work-rxjava2:2.10.0-alpha02")
- docs("androidx.work:work-rxjava3:2.10.0-alpha02")
- docs("androidx.work:work-testing:2.10.0-alpha02")
+ docs("androidx.window:window-java:1.4.0-alpha02")
+ docs("androidx.window:window-rxjava2:1.4.0-alpha02")
+ docs("androidx.window:window-rxjava3:1.4.0-alpha02")
+ docs("androidx.window:window-testing:1.4.0-alpha02")
+ docs("androidx.work:work-gcm:2.10.0-alpha03")
+ docs("androidx.work:work-multiprocess:2.10.0-alpha03")
+ docs("androidx.work:work-runtime:2.10.0-alpha03")
+ docs("androidx.work:work-runtime-ktx:2.10.0-alpha03")
+ docs("androidx.work:work-rxjava2:2.10.0-alpha03")
+ docs("androidx.work:work-rxjava3:2.10.0-alpha03")
+ docs("androidx.work:work-testing:2.10.0-alpha03")
}
afterEvaluate {
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index e89eb38..ee86b2b 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -47,7 +47,6 @@
docs(project(":benchmark:benchmark-macro"))
docs(project(":benchmark:benchmark-macro-junit4"))
docs(project(":biometric:biometric"))
- docs(project(":biometric:biometric-ktx"))
docs(project(":bluetooth:bluetooth"))
docs(project(":bluetooth:bluetooth-testing"))
docs(project(":browser:browser"))
@@ -63,6 +62,7 @@
docs(project(":camera:camera-feature-combination-query-play-services"))
docs(project(":camera:camera-lifecycle"))
samples(project(":camera:camera-lifecycle:camera-lifecycle-samples"))
+ docs(project(":camera:camera-media3-effect"))
docs(project(":camera:camera-mlkit-vision"))
docs(project(":camera:camera-testing"))
docs(project(":camera:camera-video"))
@@ -86,6 +86,7 @@
kmpDocs(project(":compose:material3:adaptive:adaptive"))
kmpDocs(project(":compose:material3:adaptive:adaptive-layout"))
kmpDocs(project(":compose:material3:adaptive:adaptive-navigation"))
+ kmpDocs(project(":compose:material3:adaptive:adaptive-render-strategy"))
kmpDocs(project(":compose:material3:material3"))
kmpDocs(project(":compose:material3:material3-adaptive-navigation-suite"))
kmpDocs(project(":compose:material3:material3-common"))
@@ -180,6 +181,7 @@
docs(project(":fragment:fragment-testing"))
docs(project(":glance:glance"))
docs(project(":glance:glance-appwidget"))
+ docs(project(":glance:glance-appwidget-multiprocess"))
docs(project(":glance:glance-appwidget-testing"))
docs(project(":glance:glance-appwidget-preview"))
docs(project(":glance:glance-material"))
@@ -206,6 +208,7 @@
kmpDocs(project(":ink:ink-geometry"))
kmpDocs(project(":ink:ink-nativeloader"))
kmpDocs(project(":ink:ink-strokes"))
+ kmpDocs(project(":ink:ink-rendering"))
docs(project(":input:input-motionprediction"))
docs(project(":interpolator:interpolator"))
docs(project(":javascriptengine:javascriptengine"))
diff --git a/docs/api_guidelines/annotations.md b/docs/api_guidelines/annotations.md
index fd9af8d..acff88f 100644
--- a/docs/api_guidelines/annotations.md
+++ b/docs/api_guidelines/annotations.md
@@ -112,6 +112,10 @@
it's considered binary compatible to refactor or remove an experimental marker
annotation.
+Note: Experimental APIs are reviewed by API Council both when the APIs are first
+introduced, and when they are stabilized. API Council may have additional
+feedback during stabilization.
+
### `@RestrictTo` APIs {#restricted-api}
Jetpack's library tooling supports hiding JVM-visible (ex. `public` and
diff --git a/docs/benchmarking_images/filter_initial.png b/docs/benchmarking_images/filter_initial.png
deleted file mode 100644
index b538051..0000000
--- a/docs/benchmarking_images/filter_initial.png
+++ /dev/null
Binary files differ
diff --git a/docs/benchmarking_images/filter_metric.png b/docs/benchmarking_images/filter_metric.png
deleted file mode 100644
index 0e714fd..0000000
--- a/docs/benchmarking_images/filter_metric.png
+++ /dev/null
Binary files differ
diff --git a/docs/benchmarking_images/filter_test.png b/docs/benchmarking_images/filter_test.png
deleted file mode 100644
index cfc8a28..0000000
--- a/docs/benchmarking_images/filter_test.png
+++ /dev/null
Binary files differ
diff --git a/docs/benchmarking_images/query_first.png b/docs/benchmarking_images/query_first.png
new file mode 100644
index 0000000..a16fcd6
--- /dev/null
+++ b/docs/benchmarking_images/query_first.png
Binary files differ
diff --git a/docs/benchmarking_images/query_second.png b/docs/benchmarking_images/query_second.png
new file mode 100644
index 0000000..b4b5c7a
--- /dev/null
+++ b/docs/benchmarking_images/query_second.png
Binary files differ
diff --git a/docs/benchmarking_images/result_plot.png b/docs/benchmarking_images/result_plot.png
index d0b878e..d2f875d 100644
--- a/docs/benchmarking_images/result_plot.png
+++ b/docs/benchmarking_images/result_plot.png
Binary files differ
diff --git a/draganddrop/draganddrop/build.gradle b/draganddrop/draganddrop/build.gradle
index f58b809..089c9e8 100644
--- a/draganddrop/draganddrop/build.gradle
+++ b/draganddrop/draganddrop/build.gradle
@@ -33,8 +33,8 @@
api("androidx.appcompat:appcompat:1.4.0")
api("androidx.core:core:1.7.0")
annotationProcessor(libs.nullaway)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/dynamicanimation/dynamicanimation-ktx/build.gradle b/dynamicanimation/dynamicanimation-ktx/build.gradle
index f7e8e34..fbfd7c9 100644
--- a/dynamicanimation/dynamicanimation-ktx/build.gradle
+++ b/dynamicanimation/dynamicanimation-ktx/build.gradle
@@ -39,8 +39,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/dynamicanimation/dynamicanimation/build.gradle b/dynamicanimation/dynamicanimation/build.gradle
index 26594fb..aba2b5a 100644
--- a/dynamicanimation/dynamicanimation/build.gradle
+++ b/dynamicanimation/dynamicanimation/build.gradle
@@ -21,8 +21,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/emoji/emoji/build.gradle b/emoji/emoji/build.gradle
index 83a20ed..40bfe37 100644
--- a/emoji/emoji/build.gradle
+++ b/emoji/emoji/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
}
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index a7db4fa..b5c2de3 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -49,14 +49,14 @@
dependencies {
androidTestImplementation(project(":emoji2:emoji2"))
androidTestImplementation(project(":emoji2:emoji2-bundled"))
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index d48ac83..df76592 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
// view tests that use font are in this module as well; for licensing reasons
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index 629fc9f..1362d13 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/emoji2/emoji2-views/build.gradle b/emoji2/emoji2-views/build.gradle
index 41926fe..9e6c338 100644
--- a/emoji2/emoji2-views/build.gradle
+++ b/emoji2/emoji2-views/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 182c191..a4597af 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
}
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index e782228..6e83dc1 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -46,8 +46,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
lintPublish(project(":fragment:fragment-testing-lint"))
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 2d7e64a..01fbd329 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -40,7 +40,7 @@
api("androidx.lifecycle:lifecycle-livedata-core:2.6.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
api("androidx.savedstate:savedstate:1.2.1")
api("androidx.annotation:annotation-experimental:1.4.1")
api(libs.kotlinStdlib)
@@ -61,13 +61,13 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
})
- testImplementation(projectOrArtifact(":fragment:fragment"))
+ testImplementation(project(":fragment:fragment"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testRuntimeOnly(libs.testCore)
diff --git a/fragment/integration-tests/testapp/build.gradle b/fragment/integration-tests/testapp/build.gradle
index 364175d..5447bc6 100644
--- a/fragment/integration-tests/testapp/build.gradle
+++ b/fragment/integration-tests/testapp/build.gradle
@@ -30,7 +30,7 @@
implementation("androidx.core:core-ktx:1.13.0")
implementation(project(":fragment:fragment-ktx"))
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
- implementation(projectOrArtifact(":transition:transition"))
+ implementation(project(":transition:transition"))
implementation("androidx.recyclerview:recyclerview:1.1.0")
debugImplementation(project(":fragment:fragment-testing-manifest"))
diff --git a/glance/glance-appwidget-multiprocess/api/current.txt b/glance/glance-appwidget-multiprocess/api/current.txt
new file mode 100644
index 0000000..19b4a8a
--- /dev/null
+++ b/glance/glance-appwidget-multiprocess/api/current.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.multiprocess {
+
+ public final class MultiProcessConfig {
+ ctor public MultiProcessConfig(android.content.ComponentName remoteWorkerService, android.content.ComponentName actionTrampolineActivity, android.content.ComponentName invisibleActionTrampolineActivity, android.content.ComponentName actionCallbackBroadcastReceiver, android.content.ComponentName remoteViewsService);
+ method public android.content.ComponentName getActionCallbackBroadcastReceiver();
+ method public android.content.ComponentName getActionTrampolineActivity();
+ method public android.content.ComponentName getInvisibleActionTrampolineActivity();
+ method public android.content.ComponentName getRemoteViewsService();
+ method public android.content.ComponentName getRemoteWorkerService();
+ property public final android.content.ComponentName actionCallbackBroadcastReceiver;
+ property public final android.content.ComponentName actionTrampolineActivity;
+ property public final android.content.ComponentName invisibleActionTrampolineActivity;
+ property public final android.content.ComponentName remoteViewsService;
+ property public final android.content.ComponentName remoteWorkerService;
+ field public static final androidx.glance.appwidget.multiprocess.MultiProcessConfig.Companion Companion;
+ }
+
+ public static final class MultiProcessConfig.Companion {
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig getDefault(android.content.Context context);
+ }
+
+ public abstract class MultiProcessGlanceAppWidget extends androidx.glance.appwidget.GlanceAppWidget {
+ ctor public MultiProcessGlanceAppWidget();
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig();
+ property public androidx.glance.appwidget.multiprocess.MultiProcessConfig? multiProcessConfig;
+ }
+
+}
+
diff --git a/biometric/biometric-ktx/api/res-current.txt b/glance/glance-appwidget-multiprocess/api/res-current.txt
similarity index 100%
copy from biometric/biometric-ktx/api/res-current.txt
copy to glance/glance-appwidget-multiprocess/api/res-current.txt
diff --git a/glance/glance-appwidget-multiprocess/api/restricted_current.txt b/glance/glance-appwidget-multiprocess/api/restricted_current.txt
new file mode 100644
index 0000000..19b4a8a
--- /dev/null
+++ b/glance/glance-appwidget-multiprocess/api/restricted_current.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.multiprocess {
+
+ public final class MultiProcessConfig {
+ ctor public MultiProcessConfig(android.content.ComponentName remoteWorkerService, android.content.ComponentName actionTrampolineActivity, android.content.ComponentName invisibleActionTrampolineActivity, android.content.ComponentName actionCallbackBroadcastReceiver, android.content.ComponentName remoteViewsService);
+ method public android.content.ComponentName getActionCallbackBroadcastReceiver();
+ method public android.content.ComponentName getActionTrampolineActivity();
+ method public android.content.ComponentName getInvisibleActionTrampolineActivity();
+ method public android.content.ComponentName getRemoteViewsService();
+ method public android.content.ComponentName getRemoteWorkerService();
+ property public final android.content.ComponentName actionCallbackBroadcastReceiver;
+ property public final android.content.ComponentName actionTrampolineActivity;
+ property public final android.content.ComponentName invisibleActionTrampolineActivity;
+ property public final android.content.ComponentName remoteViewsService;
+ property public final android.content.ComponentName remoteWorkerService;
+ field public static final androidx.glance.appwidget.multiprocess.MultiProcessConfig.Companion Companion;
+ }
+
+ public static final class MultiProcessConfig.Companion {
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig getDefault(android.content.Context context);
+ }
+
+ public abstract class MultiProcessGlanceAppWidget extends androidx.glance.appwidget.GlanceAppWidget {
+ ctor public MultiProcessGlanceAppWidget();
+ method public androidx.glance.appwidget.multiprocess.MultiProcessConfig? getMultiProcessConfig();
+ property public androidx.glance.appwidget.multiprocess.MultiProcessConfig? multiProcessConfig;
+ }
+
+}
+
diff --git a/biometric/biometric-ktx/build.gradle b/glance/glance-appwidget-multiprocess/build.gradle
similarity index 68%
rename from biometric/biometric-ktx/build.gradle
rename to glance/glance-appwidget-multiprocess/build.gradle
index 77d810c..72922dd 100644
--- a/biometric/biometric-ktx/build.gradle
+++ b/glance/glance-appwidget-multiprocess/build.gradle
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,9 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -33,19 +31,21 @@
dependencies {
api(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
- api(project(":biometric:biometric"))
-}
-
-androidx {
- name = "Biometric Kotlin Extensions"
- type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
- inceptionYear = "2020"
- description = "Kotlin extensions for the Biometric Library."
- samples(project(":biometric:biometric-ktx-samples"))
+ implementation(project(":glance:glance-appwidget"))
+ implementation("androidx.work:work-runtime-ktx:2.9.1")
+ implementation("androidx.work:work-multiprocess:2.9.1")
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.2.0")
+ // Add dependencies here
}
android {
+ namespace "androidx.glance.appwidget.multiprocess"
compileSdk 35
- namespace "androidx.biometric.ktx"
+}
+
+androidx {
+ name = "androidx.glance:glance-appwidget-multiprocess"
+ type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ inceptionYear = "2024"
+ description = "Use Glance with multiprocess setups"
}
diff --git a/glance/glance-appwidget-multiprocess/src/main/java/androidx/glance/androidx-glance-glance-appwidget-multiprocess-documentation.md b/glance/glance-appwidget-multiprocess/src/main/java/androidx/glance/androidx-glance-glance-appwidget-multiprocess-documentation.md
new file mode 100644
index 0000000..2a56bc6
--- /dev/null
+++ b/glance/glance-appwidget-multiprocess/src/main/java/androidx/glance/androidx-glance-glance-appwidget-multiprocess-documentation.md
@@ -0,0 +1,15 @@
+# Module root
+
+androidx.glance glance-appwidget-multiprocess
+
+# Package androidx.glance.appwidget.multiprocess
+
+This module introduces support for running GlanceAppWidgets from multiple processes,
+with the WorkManager multiprocess library ("androidx.work:work-multiprocess").
+
+Developers can define a widget using the `RemoteGlanceAppWidget` class, which allows them
+to specify the component name of the RemoteWorkerService it will run in, as well as the
+component names of action receivers and RemoteViewsService. These components must then
+be set to the same process as the `GlanceAppWidgetReceiver` in the manifest.
+
+This way, developers can choose to provide different widgets from different processes.
diff --git a/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt
new file mode 100644
index 0000000..cbf61a8
--- /dev/null
+++ b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/MultiProcessGlanceAppWidget.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.appwidget.AppWidgetSession
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceComponents
+import androidx.glance.session.GlanceSessionManager
+import androidx.glance.session.SessionManager
+import androidx.work.multiprocess.RemoteWorkerService
+
+/**
+ * MultiProcessGlanceAppWidget can be used with [androidx.glance.appwidget.GlanceAppWidgetReceiver]
+ * to support multiprocess use cases where different widget receivers run in different processes.
+ * Note that the worker must still run in the same process as the receiver.
+ */
+public abstract class MultiProcessGlanceAppWidget : GlanceAppWidget() {
+ /**
+ * Override [multiProcessConfig] to provide a [androidx.work.multiprocess.RemoteWorkerService]
+ * that runs in the same process as the [androidx.glance.appwidget.GlanceAppWidgetReceiver] that
+ * this is attached to.
+ *
+ * If null, then this widget will be run with normal WorkManager, i.e. the same behavior as
+ * GlanceAppWidget.
+ */
+ public open val multiProcessConfig: MultiProcessConfig? = null
+
+ @get:RestrictTo(Scope.LIBRARY_GROUP)
+ final override val components: GlanceComponents?
+ get() = multiProcessConfig?.toGlanceComponents()
+
+ @get:RestrictTo(Scope.LIBRARY_GROUP)
+ protected final override val sessionManager: SessionManager
+ get() = if (multiProcessConfig != null) RemoteSessionManager else GlanceSessionManager
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected final override fun createAppWidgetSession(
+ id: AppWidgetId,
+ options: Bundle?
+ ): AppWidgetSession {
+ return multiProcessConfig?.let { config ->
+ RemoteAppWidgetSession(this, config.remoteWorkerService, id, options)
+ } ?: AppWidgetSession(this, id, options)
+ }
+}
+
+/**
+ * This class specifies the components to be used when creating layouts for
+ * [MultiProcessGlanceAppWidget].
+ */
+public class MultiProcessConfig(
+ /**
+ * The remote worker service used to run jobs for a [MultiProcessGlanceAppWidget]. This must be
+ * set to run in the same `android:process` as the
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] and Glance components.
+ */
+ public val remoteWorkerService: ComponentName,
+ /**
+ * Action Trampoline Activity. Must be set to run in the same process as
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] and [remoteWorkerService].
+ */
+ public val actionTrampolineActivity: ComponentName,
+ /**
+ * Invisible Action Trampoline Activity. Must be set to run in the same process as
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] and [remoteWorkerService].
+ */
+ public val invisibleActionTrampolineActivity: ComponentName,
+ /**
+ * Action callback BroadcastReceiver. Must be set to run in the same process as
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] and [remoteWorkerService].
+ */
+ public val actionCallbackBroadcastReceiver: ComponentName,
+ /**
+ * Glance RemoteViewsService. Must be set to run in the same process as
+ * [androidx.glance.appwidget.GlanceAppWidgetReceiver] and [remoteWorkerService].
+ */
+ public val remoteViewsService: ComponentName,
+) {
+
+ public companion object {
+ public fun getDefault(context: Context): MultiProcessConfig =
+ GlanceComponents.getDefault(context).run {
+ MultiProcessConfig(
+ remoteWorkerService = ComponentName(context, RemoteWorkerService::class.java),
+ actionTrampolineActivity = actionTrampolineActivity,
+ invisibleActionTrampolineActivity = invisibleActionTrampolineActivity,
+ actionCallbackBroadcastReceiver = actionCallbackBroadcastReceiver,
+ remoteViewsService = remoteViewsService,
+ )
+ }
+ }
+
+ internal fun toGlanceComponents() =
+ GlanceComponents(
+ actionTrampolineActivity,
+ invisibleActionTrampolineActivity,
+ actionTrampolineActivity,
+ remoteViewsService
+ )
+}
diff --git a/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/RemoteSessionManager.kt b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/RemoteSessionManager.kt
new file mode 100644
index 0000000..9743cf6
--- /dev/null
+++ b/glance/glance-appwidget-multiprocess/src/main/kotlin/androidx/glance/appwidget/multiprocess/RemoteSessionManager.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.concurrent.futures.await
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.appwidget.AppWidgetSession
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.session.SessionManager
+import androidx.glance.session.SessionManagerImpl
+import androidx.glance.session.SessionWorker
+import androidx.glance.session.WorkManagerProxy
+import androidx.glance.state.ConfigManager
+import androidx.glance.state.GlanceState
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkQuery
+import androidx.work.WorkerParameters
+import androidx.work.multiprocess.RemoteCoroutineWorker
+import androidx.work.multiprocess.RemoteListenableWorker
+import androidx.work.multiprocess.RemoteWorkManager
+import androidx.work.workDataOf
+
+///
+/// Private APIs: these are just versions of the normal session classes (Session, SessionManager,
+/// SessionWorker) that use RemoteWorkManager instead of the normal WorkManager.
+///
+
+private interface RemoteSession {
+ val remoteWorkService: ComponentName
+}
+
+internal class RemoteAppWidgetSession(
+ private val widget: MultiProcessGlanceAppWidget,
+ override val remoteWorkService: ComponentName,
+ id: AppWidgetId,
+ initialOptions: Bundle? = null,
+ configManager: ConfigManager = GlanceState,
+ lambdaReceiver: ComponentName? = null,
+ sizeMode: SizeMode = widget.sizeMode,
+ shouldPublish: Boolean = true,
+ initialGlanceState: Any? = null,
+) :
+ AppWidgetSession(
+ widget,
+ id,
+ initialOptions,
+ configManager,
+ lambdaReceiver,
+ sizeMode,
+ shouldPublish,
+ initialGlanceState
+ ),
+ RemoteSession
+
+internal object RemoteSessionManager :
+ SessionManager by SessionManagerImpl(
+ workerClass = RemoteSessionWorker::class.java,
+ workManagerProxy = RemoteWorkManagerProxy,
+ inputDataFactory = { session ->
+ val className = (session as? RemoteSession)?.remoteWorkService?.className
+ val packageName = (session as? RemoteSession)?.remoteWorkService?.packageName
+ workDataOf(
+ keyParam to session.key,
+ RemoteListenableWorker.ARGUMENT_CLASS_NAME to className,
+ RemoteListenableWorker.ARGUMENT_PACKAGE_NAME to packageName,
+ )
+ }
+ )
+
+private object RemoteWorkManagerProxy : WorkManagerProxy {
+ override suspend fun enqueueUniqueWork(
+ context: Context,
+ uniqueWorkName: String,
+ existingWorkPolicy: ExistingWorkPolicy,
+ workRequest: OneTimeWorkRequest,
+ ) {
+ RemoteWorkManager.getInstance(context)
+ .enqueueUniqueWork(uniqueWorkName, existingWorkPolicy, workRequest)
+ .await()
+ }
+
+ override suspend fun workerIsRunningOrEnqueued(
+ context: Context,
+ uniqueWorkName: String
+ ): Boolean =
+ RemoteWorkManager.getInstance(context)
+ .getWorkInfos(WorkQuery.Builder.fromUniqueWorkNames(listOf(uniqueWorkName)).build())
+ .await()
+ .any { it.state in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED) }
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class RemoteSessionWorker(
+ private val context: Context,
+ private val parameters: WorkerParameters
+) : RemoteCoroutineWorker(context, parameters) {
+ override suspend fun doRemoteWork(): Result =
+ SessionWorker(context, parameters, RemoteSessionManager).doWork()
+}
diff --git a/glance/glance-appwidget-testing/build.gradle b/glance/glance-appwidget-testing/build.gradle
index f8a6763..8b6c34a 100644
--- a/glance/glance-appwidget-testing/build.gradle
+++ b/glance/glance-appwidget-testing/build.gradle
@@ -69,5 +69,5 @@
inceptionYear = "2023"
description = "This library provides APIs for developers to use for testing their appWidget specific Glance composables."
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
+ samples(project(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
}
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index f212864..6a2e4dc 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -100,6 +100,11 @@
method public suspend Object? compose(android.content.Context context, long size, optional Object? state, optional android.os.Bundle appWidgetOptions, kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.coroutines.Continuation<? super androidx.glance.appwidget.RemoteViewsCompositionResult>);
}
+ public class GlanceRemoteViewsService extends android.widget.RemoteViewsService {
+ ctor public GlanceRemoteViewsService();
+ method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent? intent);
+ }
+
public final class ImageProvidersKt {
method public static androidx.glance.ImageProvider ImageProvider(android.net.Uri uri);
}
@@ -180,6 +185,19 @@
method public suspend Object? onAction(android.content.Context context, androidx.glance.GlanceId glanceId, androidx.glance.action.ActionParameters parameters, kotlin.coroutines.Continuation<? super kotlin.Unit>);
}
+ public class ActionCallbackBroadcastReceiver extends android.content.BroadcastReceiver {
+ ctor public ActionCallbackBroadcastReceiver();
+ method public void onReceive(android.content.Context? context, android.content.Intent? intent);
+ }
+
+ public class ActionTrampolineActivity extends android.app.Activity {
+ ctor public ActionTrampolineActivity();
+ }
+
+ public class InvisibleActionTrampolineActivity extends android.app.Activity {
+ ctor public InvisibleActionTrampolineActivity();
+ }
+
public final class RunCallbackActionKt {
method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index f212864..6a2e4dc 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -100,6 +100,11 @@
method public suspend Object? compose(android.content.Context context, long size, optional Object? state, optional android.os.Bundle appWidgetOptions, kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.coroutines.Continuation<? super androidx.glance.appwidget.RemoteViewsCompositionResult>);
}
+ public class GlanceRemoteViewsService extends android.widget.RemoteViewsService {
+ ctor public GlanceRemoteViewsService();
+ method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent? intent);
+ }
+
public final class ImageProvidersKt {
method public static androidx.glance.ImageProvider ImageProvider(android.net.Uri uri);
}
@@ -180,6 +185,19 @@
method public suspend Object? onAction(android.content.Context context, androidx.glance.GlanceId glanceId, androidx.glance.action.ActionParameters parameters, kotlin.coroutines.Continuation<? super kotlin.Unit>);
}
+ public class ActionCallbackBroadcastReceiver extends android.content.BroadcastReceiver {
+ ctor public ActionCallbackBroadcastReceiver();
+ method public void onReceive(android.content.Context? context, android.content.Intent? intent);
+ }
+
+ public class ActionTrampolineActivity extends android.app.Activity {
+ ctor public ActionTrampolineActivity();
+ }
+
+ public class InvisibleActionTrampolineActivity extends android.app.Activity {
+ ctor public InvisibleActionTrampolineActivity();
+ }
+
public final class RunCallbackActionKt {
method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index 7558274..5357000 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -116,7 +116,7 @@
"using a Jetpack Compose-style API."
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
- samples(projectOrArtifact(":glance:glance-appwidget:glance-appwidget-samples"))
+ samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
}
LayoutGeneratorTask.registerLayoutGenerator(
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 2bb9ac2..a3d01ba 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.datastore.dataStoreFile
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
@@ -644,7 +645,6 @@
@Test
fun layoutConfigurationCanBeDeleted() {
- val fakeIndex = 9999
TestGlanceAppWidget.uiDefinition = { Text("something") }
mHostRule.startHost()
@@ -655,8 +655,7 @@
}
val appWidgetId = (glanceId as AppWidgetId).appWidgetId
- val config = LayoutConfiguration.create(context, appWidgetId, nextIndex = fakeIndex)
- val file = config.dataStoreFile
+ val file = context.dataStoreFile(layoutDatastoreKey(appWidgetId))
assertThat(file.exists())
val isDeleted = LayoutConfiguration.delete(context, glanceId)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
index 15dc5b4..a9c4fc1 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
@@ -21,6 +21,7 @@
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
+import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -70,7 +71,8 @@
* [android.appwidget.AppWidgetManager.updateAppWidget]. The [id] must be valid
* ([AppWidgetId.isRealId]) in that case.
*/
-internal class AppWidgetSession(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+open class AppWidgetSession(
private val widget: GlanceAppWidget,
private val id: AppWidgetId,
initialOptions: Bundle? = null,
@@ -184,7 +186,8 @@
layoutConfig,
layoutConfig.addLayout(root),
DpSize.Unspecified,
- receiver
+ receiver,
+ widget.components ?: GlanceComponents.getDefault(context),
)
if (shouldPublish) {
appWidgetManager.updateAppWidget(id.appWidgetId, rv)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index 9b03f3c..8317afb 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -17,6 +17,7 @@
package androidx.glance.appwidget
import android.appwidget.AppWidgetManager
+import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.os.Bundle
@@ -28,6 +29,9 @@
import androidx.compose.runtime.Composable
import androidx.glance.GlanceComposable
import androidx.glance.GlanceId
+import androidx.glance.appwidget.action.ActionCallbackBroadcastReceiver
+import androidx.glance.appwidget.action.ActionTrampolineActivity
+import androidx.glance.appwidget.action.InvisibleActionTrampolineActivity
import androidx.glance.appwidget.state.getAppWidgetState
import androidx.glance.session.GlanceSessionManager
import androidx.glance.session.SessionManager
@@ -53,7 +57,8 @@
abstract class GlanceAppWidget(
@LayoutRes internal open val errorUiLayout: Int = R.layout.glance_error_layout,
) {
- private val sessionManager: SessionManager = GlanceSessionManager
+ @get:RestrictTo(Scope.LIBRARY_GROUP)
+ protected open val sessionManager: SessionManager = GlanceSessionManager
/**
* Override this function to provide the Glance Composable.
@@ -135,7 +140,7 @@
val glanceId = AppWidgetId(appWidgetId)
sessionManager.runWithLock {
if (!isSessionRunning(context, glanceId.toSessionKey())) {
- startSession(context, AppWidgetSession(this@GlanceAppWidget, glanceId, options))
+ startSession(context, createAppWidgetSession(glanceId, options))
return@runWithLock
}
val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
@@ -217,11 +222,23 @@
block: suspend SessionManagerScope.(AppWidgetSession) -> Unit
) = runWithLock {
if (!isSessionRunning(context, glanceId.toSessionKey())) {
- startSession(context, AppWidgetSession(this@GlanceAppWidget, glanceId, options))
+ startSession(context, createAppWidgetSession(glanceId, options))
}
val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
block(session)
}
+
+ /**
+ * Override this function to specify the components that will be used for actions and
+ * RemoteViewsService. All of the components must run in the same process.
+ *
+ * If null, then the default components will be used.
+ */
+ @get:RestrictTo(Scope.LIBRARY_GROUP) open val components: GlanceComponents? = null
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected open fun createAppWidgetSession(id: AppWidgetId, options: Bundle? = null) =
+ AppWidgetSession(this@GlanceAppWidget, id, options)
}
@RestrictTo(Scope.LIBRARY_GROUP) data class AppWidgetId(val appWidgetId: Int) : GlanceId
@@ -266,3 +283,30 @@
"GlanceAppWidget.provideGlance"
)
}
+
+/**
+ * Specifies which components will be used as targets for action trampolines, RunCallback actions,
+ * and RemoteViewsService when creating RemoteViews. These components must all run in the same
+ * process.
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+class GlanceComponents(
+ val actionTrampolineActivity: ComponentName,
+ val invisibleActionTrampolineActivity: ComponentName,
+ val actionCallbackBroadcastReceiver: ComponentName,
+ val remoteViewsService: ComponentName,
+) {
+ companion object {
+ /** The default components used for GlanceAppWidget. */
+ fun getDefault(context: Context) =
+ GlanceComponents(
+ actionTrampolineActivity =
+ ComponentName(context, ActionTrampolineActivity::class.java),
+ invisibleActionTrampolineActivity =
+ ComponentName(context, InvisibleActionTrampolineActivity::class.java),
+ actionCallbackBroadcastReceiver =
+ ComponentName(context, ActionCallbackBroadcastReceiver::class.java),
+ remoteViewsService = ComponentName(context, GlanceRemoteViewsService::class.java),
+ )
+ }
+}
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
index a5ecba7..ac38d66 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetManager.kt
@@ -16,6 +16,7 @@
package androidx.glance.appwidget
+import android.app.Application
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
@@ -33,6 +34,7 @@
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
+import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.glance.GlanceId
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
@@ -61,6 +63,12 @@
synchronized(GlanceAppWidgetManager) {
return dataStoreSingleton
?: run {
+ // Delete old file format that did not include the process name.
+ context
+ .preferencesDataStoreFile("GlanceAppWidgetManager")
+ .takeIf { it.exists() }
+ ?.delete()
+
val newValue = context.appManagerDataStore
dataStoreSingleton = newValue
newValue
@@ -346,7 +354,19 @@
private companion object {
private val Context.appManagerDataStore by
- preferencesDataStore(name = "GlanceAppWidgetManager")
+ preferencesDataStore(name = "GlanceAppWidgetManager-$processName")
+
+ private val processName: String
+ get() =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ Application.getProcessName()
+ } else {
+ Class.forName("android.app.ActivityThread")
+ .getDeclaredMethod("currentProcessName")
+ .apply { isAccessible = true }
+ .invoke(null) as String
+ }
+
private var dataStoreSingleton: DataStore<Preferences>? = null
private val providersKey = stringSetPreferencesKey("list::Providers")
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
index 573b01a..3e0422f 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
@@ -34,9 +34,9 @@
* [RemoteViewsService] to be connected to for a remote adapter that returns RemoteViews for lazy
* lists / grids.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class GlanceRemoteViewsService : RemoteViewsService() {
- override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
+open class GlanceRemoteViewsService : RemoteViewsService() {
+ override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
+ requireNotNull(intent) { "Intent is null" }
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
check(appWidgetId != -1) { "No app widget id was present in the intent" }
@@ -49,10 +49,11 @@
return GlanceRemoteViewsFactory(this, appWidgetId, viewId, sizeInfo)
}
- companion object {
- const val EXTRA_VIEW_ID = "androidx.glance.widget.extra.view_id"
- const val EXTRA_SIZE_INFO = "androidx.glance.widget.extra.size_info"
- const val TAG = "GlanceRemoteViewService"
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ internal companion object {
+ internal const val EXTRA_VIEW_ID = "androidx.glance.widget.extra.view_id"
+ internal const val EXTRA_SIZE_INFO = "androidx.glance.widget.extra.size_info"
+ internal const val TAG = "GlanceRemoteViewService"
// An in-memory store containing items to be returned via the adapter when requested.
private val InMemoryStore = RemoteCollectionItemsInMemoryStore()
@@ -227,8 +228,7 @@
*/
@Suppress("DEPRECATION")
internal fun RemoteViews.setRemoteAdapter(
- context: Context,
- appWidgetId: Int,
+ translationContext: TranslationContext,
viewId: Int,
sizeInfo: String,
items: RemoteCollectionItems
@@ -236,8 +236,11 @@
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
CollectionItemsApi31Impl.setRemoteAdapter(this, viewId, items)
} else {
+ val context = translationContext.context
+ val appWidgetId = translationContext.appWidgetId
val intent =
- Intent(context, GlanceRemoteViewsService::class.java)
+ Intent()
+ .setComponent(translationContext.glanceComponents.remoteViewsService)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
.putExtra(GlanceRemoteViewsService.EXTRA_VIEW_ID, viewId)
.putExtra(GlanceRemoteViewsService.EXTRA_SIZE_INFO, sizeInfo)
@@ -246,7 +249,7 @@
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
check(context.packageManager.resolveService(intent, 0) != null) {
- "GlanceRemoteViewsService could not be resolved, check the app manifest."
+ "${intent.component} could not be resolved, check the app manifest."
}
setRemoteAdapter(viewId, intent)
GlanceRemoteViewsService.saveItems(appWidgetId, viewId, sizeInfo, items)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
index ed0f454..4f100a4 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -70,6 +70,7 @@
rootViewIndex: Int,
layoutSize: DpSize,
actionBroadcastReceiver: ComponentName? = null,
+ glanceComponents: GlanceComponents = GlanceComponents.getDefault(context),
) =
translateComposition(
TranslationContext(
@@ -80,6 +81,7 @@
itemPosition = -1,
layoutSize = layoutSize,
actionBroadcastReceiver = actionBroadcastReceiver,
+ glanceComponents = glanceComponents,
),
element.children,
rootViewIndex,
@@ -166,7 +168,8 @@
val layoutCollectionItemId: Int = -1,
val canUseSelectableGroup: Boolean = false,
val actionTargetId: Int? = null,
- val actionBroadcastReceiver: ComponentName? = null
+ val actionBroadcastReceiver: ComponentName? = null,
+ val glanceComponents: GlanceComponents,
) {
fun nextViewId() = lastViewId.incrementAndGet()
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
index d1595e4..8574c68 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/WidgetLayout.kt
@@ -86,9 +86,6 @@
private val existingLayoutIds: MutableSet<Int> = mutableSetOf(),
) {
- @VisibleForTesting
- internal val dataStoreFile = context.dataStoreFile(layoutDatastoreKey(appWidgetId))
-
internal companion object {
/** Creates a [LayoutConfiguration] retrieving known layouts from file, if they exist. */
@@ -300,7 +297,8 @@
private val GlanceModifier.heightModifier: Dimension
get() = findModifier<HeightModifier>()?.height ?: Dimension.Wrap
-private fun layoutDatastoreKey(appWidgetId: Int): String = "appWidgetLayout-$appWidgetId"
+@VisibleForTesting
+internal fun layoutDatastoreKey(appWidgetId: Int): String = "appWidgetLayout-$appWidgetId"
private object LayoutStateDefinition : GlanceStateDefinition<LayoutProto.LayoutConfig> {
override fun getLocation(context: Context, fileKey: String): File =
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionCallbackBroadcastReceiver.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionCallbackBroadcastReceiver.kt
index b820300..6249896 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionCallbackBroadcastReceiver.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionCallbackBroadcastReceiver.kt
@@ -20,21 +20,25 @@
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
+import androidx.annotation.RestrictTo
import androidx.core.os.bundleOf
import androidx.glance.action.ActionParameters
import androidx.glance.action.mutableActionParametersOf
import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.appwidget.TranslationContext
import androidx.glance.appwidget.goAsync
import androidx.glance.appwidget.logException
import kotlinx.coroutines.CancellationException
/** Responds to broadcasts from [RunCallbackAction] clicks by executing the associated action. */
-internal class ActionCallbackBroadcastReceiver : BroadcastReceiver() {
+open class ActionCallbackBroadcastReceiver : BroadcastReceiver() {
@Suppress("DEPRECATION")
- override fun onReceive(context: Context, intent: Intent) {
+ override fun onReceive(context: Context?, intent: Intent?) {
goAsync {
try {
+ requireNotNull(context) { "Context is null" }
+ requireNotNull(intent) { "Intent is null" }
val extras =
requireNotNull(intent.extras) {
"The intent must have action parameters extras."
@@ -72,21 +76,21 @@
}
}
- companion object {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ internal companion object {
private const val AppWidgetId = "ActionCallbackBroadcastReceiver:appWidgetId"
private const val ExtraCallbackClassName = "ActionCallbackBroadcastReceiver:callbackClass"
private const val ExtraParameters = "ActionCallbackBroadcastReceiver:parameters"
internal fun createIntent(
- context: Context,
+ translationContext: TranslationContext,
callbackClass: Class<out ActionCallback>,
- appWidgetId: Int,
parameters: ActionParameters
) =
- Intent(context, ActionCallbackBroadcastReceiver::class.java)
- .setPackage(context.packageName)
+ Intent()
+ .setComponent(translationContext.glanceComponents.actionCallbackBroadcastReceiver)
.putExtra(ExtraCallbackClassName, callbackClass.canonicalName)
- .putExtra(AppWidgetId, appWidgetId)
+ .putExtra(AppWidgetId, translationContext.appWidgetId)
.putParameterExtras(parameters)
private fun Intent.putParameterExtras(parameters: ActionParameters): Intent {
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampoline.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampoline.kt
index 349855a..a4ab714 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampoline.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampoline.kt
@@ -49,13 +49,13 @@
type: ActionTrampolineType,
activityOptions: Bundle? = null,
): Intent {
- val target =
- if (type == ActionTrampolineType.ACTIVITY) {
- ActionTrampolineActivity::class.java
- } else {
- InvisibleActionTrampolineActivity::class.java
- }
- return Intent(translationContext.context, target).also { intent ->
+ return Intent().also { intent ->
+ intent.component =
+ if (type == ActionTrampolineType.ACTIVITY) {
+ translationContext.glanceComponents.actionTrampolineActivity
+ } else {
+ translationContext.glanceComponents.invisibleActionTrampolineActivity
+ }
intent.data = createUniqueUri(translationContext, viewId, type)
intent.putExtra(ActionTypeKey, type.name)
intent.putExtra(ActionIntentKey, this)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampolineActivity.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampolineActivity.kt
index 2c32f61..3a39b04 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampolineActivity.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ActionTrampolineActivity.kt
@@ -24,7 +24,7 @@
* This trampoline is only used for device versions before [android.os.Build.VERSION_CODES.Q].
*/
@Suppress("ForbiddenSuperClass")
-internal class ActionTrampolineActivity : Activity() {
+open class ActionTrampolineActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ApplyAction.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ApplyAction.kt
index a248d03..5a20820 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ApplyAction.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/ApplyAction.kt
@@ -149,9 +149,8 @@
translationContext.context,
0,
ActionCallbackBroadcastReceiver.createIntent(
- translationContext.context,
+ translationContext,
action.callbackClass,
- translationContext.appWidgetId,
editParams(action.parameters)
)
.apply {
@@ -263,10 +262,9 @@
}
is RunCallbackAction -> {
ActionCallbackBroadcastReceiver.createIntent(
- context = translationContext.context,
- callbackClass = action.callbackClass,
- appWidgetId = translationContext.appWidgetId,
- parameters = editParams(action.parameters)
+ translationContext,
+ action.callbackClass,
+ editParams(action.parameters)
)
.applyTrampolineIntent(
translationContext,
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/InvisibleActionTrampolineActivity.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/InvisibleActionTrampolineActivity.kt
index 0462188..aee25e7 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/InvisibleActionTrampolineActivity.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/action/InvisibleActionTrampolineActivity.kt
@@ -24,7 +24,7 @@
* that don't launch an activity. Thus not showing any UI.
*/
@Suppress("ForbiddenSuperClass")
-internal class InvisibleActionTrampolineActivity : Activity() {
+open class InvisibleActionTrampolineActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
index 196630f..54da621 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -98,8 +98,7 @@
}
.build()
setRemoteAdapter(
- translationContext.context,
- translationContext.appWidgetId,
+ translationContext,
viewDef.mainViewId,
translationContext.layoutSize.toSizeString(),
items
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
index 44185c4..b9e0fa7 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -110,8 +110,7 @@
}
.build()
setRemoteAdapter(
- translationContext.context,
- translationContext.appWidgetId,
+ translationContext,
viewDef.mainViewId,
translationContext.layoutSize.toSizeString(),
items
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/ActionCallbackBroadcastReceiverTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/ActionCallbackBroadcastReceiverTest.kt
index 128e60a..9d9c681 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/ActionCallbackBroadcastReceiverTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/ActionCallbackBroadcastReceiverTest.kt
@@ -55,26 +55,28 @@
}
private fun createPendingIntent(parameters: ActionParameters, viewId: Int): PendingIntent {
+ val translationContext =
+ TranslationContext(
+ context,
+ appWidgetId = 1,
+ isRtl = false,
+ layoutConfiguration = LayoutConfiguration.create(context, 1),
+ itemPosition = -1,
+ isLazyCollectionDescendant = false,
+ glanceComponents = GlanceComponents.getDefault(context),
+ )
return PendingIntent.getBroadcast(
context,
0,
ActionCallbackBroadcastReceiver.createIntent(
- context = context,
+ translationContext = translationContext,
callbackClass = ActionCallback::class.java,
- appWidgetId = 1,
parameters = parameters
)
.apply {
data =
createUniqueUri(
- TranslationContext(
- context,
- appWidgetId = 1,
- isRtl = false,
- layoutConfiguration = LayoutConfiguration.create(context, 1),
- itemPosition = -1,
- isLazyCollectionDescendant = false,
- ),
+ translationContext,
viewId = viewId,
type = ActionTrampolineType.CALLBACK,
)
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 543e207..7b40114 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -42,6 +42,7 @@
implementation("androidx.work:work-runtime:2.7.1")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+ implementation("androidx.compose.ui:ui-util:1.6.8")
implementation(libs.kotlinStdlib)
// Force upgrade since 1.2.0 is not compatible with latest lint.
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
index efa53ff..dc04207 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
@@ -20,8 +20,11 @@
import android.content.Context
import android.util.Log
import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.compose.ui.util.fastAny
import androidx.concurrent.futures.await
import androidx.work.Constraints
+import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
@@ -37,7 +40,7 @@
* [SessionManager] is the entrypoint for Glance surfaces to start a session worker that will handle
* their composition.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(LIBRARY_GROUP)
interface SessionManager {
/**
* [runWithLock] provides a scope in which to run operations on SessionManager.
@@ -59,7 +62,7 @@
get() = "KEY"
}
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(LIBRARY_GROUP)
interface SessionManagerScope {
/** Start a session for the Glance in [session]. */
suspend fun startSession(context: Context, session: Session)
@@ -74,11 +77,19 @@
fun getSession(key: String): Session?
}
-@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@get:RestrictTo(LIBRARY_GROUP)
val GlanceSessionManager: SessionManager = SessionManagerImpl(SessionWorker::class.java)
-internal class SessionManagerImpl(private val workerClass: Class<out ListenableWorker>) :
- SessionManager {
+typealias InputDataFactory = SessionManagerImpl.(Session) -> Data
+
+@RestrictTo(LIBRARY_GROUP)
+class SessionManagerImpl(
+ private val workerClass: Class<out ListenableWorker>,
+ private val inputDataFactory: InputDataFactory = { session ->
+ workDataOf(keyParam to session.key)
+ },
+ private val workManagerProxy: WorkManagerProxy = WorkManagerProxy.Default
+) : SessionManager {
private companion object {
const val TAG = "GlanceSessionManager"
const val DEBUG = false
@@ -101,12 +112,14 @@
}
val workRequest =
OneTimeWorkRequest.Builder(workerClass)
- .setInputData(workDataOf(keyParam to session.key))
+ .setInputData(inputDataFactory(session))
.build()
- WorkManager.getInstance(context)
- .enqueueUniqueWork(session.key, ExistingWorkPolicy.REPLACE, workRequest)
- .result
- .await()
+ workManagerProxy.enqueueUniqueWork(
+ context,
+ session.key,
+ ExistingWorkPolicy.REPLACE,
+ workRequest
+ )
enqueueDelayedWorker(context)
}
@@ -115,9 +128,7 @@
@SuppressLint("ListIterator")
override suspend fun isSessionRunning(context: Context, key: String): Boolean {
val workerIsRunningOrEnqueued =
- WorkManager.getInstance(context).getWorkInfosForUniqueWork(key).await().any {
- it.state in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED)
- }
+ workManagerProxy.workerIsRunningOrEnqueued(context, key)
val hasOpenSession = sessions[key]?.isOpen ?: false
val isRunning = hasOpenSession && workerIsRunningOrEnqueued
if (DEBUG) Log.d(TAG, "isSessionRunning($key) == $isRunning")
@@ -134,15 +145,57 @@
mutex.withLock { scope.block() }
/** Workaround worker to fix b/119920965 */
- private fun enqueueDelayedWorker(context: Context) {
- WorkManager.getInstance(context)
- .enqueueUniqueWork(
- "sessionWorkerKeepEnabled",
- ExistingWorkPolicy.KEEP,
- OneTimeWorkRequest.Builder(workerClass)
- .setInitialDelay(10 * 365, TimeUnit.DAYS)
- .setConstraints(Constraints.Builder().setRequiresCharging(true).build())
- .build()
- )
+ private suspend fun enqueueDelayedWorker(context: Context) {
+ workManagerProxy.enqueueUniqueWork(
+ context,
+ "sessionWorkerKeepEnabled",
+ ExistingWorkPolicy.KEEP,
+ OneTimeWorkRequest.Builder(workerClass)
+ .setInitialDelay(10 * 365, TimeUnit.DAYS)
+ .setConstraints(Constraints.Builder().setRequiresCharging(true).build())
+ .build()
+ )
}
}
+
+// This interface is used to allow us to use the same SessionManagerImpl with WorkManager or
+// RemoteWorkManager (which do not have a common supertype),
+@RestrictTo(LIBRARY_GROUP)
+interface WorkManagerProxy {
+ companion object {
+ val Default =
+ object : WorkManagerProxy {
+ override suspend fun enqueueUniqueWork(
+ context: Context,
+ uniqueWorkName: String,
+ existingWorkPolicy: ExistingWorkPolicy,
+ workRequest: OneTimeWorkRequest,
+ ) {
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(uniqueWorkName, existingWorkPolicy, workRequest)
+ .result
+ .await()
+ }
+
+ override suspend fun workerIsRunningOrEnqueued(
+ context: Context,
+ uniqueWorkName: String
+ ): Boolean =
+ WorkManager.getInstance(context)
+ .getWorkInfosForUniqueWork(uniqueWorkName)
+ .await()
+ .fastAny {
+ it.state in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED)
+ }
+ }
+ }
+
+ suspend fun enqueueUniqueWork(
+ context: Context,
+ uniqueWorkName: String,
+ existingWorkPolicy: ExistingWorkPolicy,
+ workRequest: OneTimeWorkRequest
+ )
+
+ suspend fun workerIsRunningOrEnqueued(context: Context, uniqueWorkName: String): Boolean
+}
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
index 1e57797..26c6937 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.util.Log
import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composition
import androidx.compose.runtime.Recomposer
@@ -53,7 +54,8 @@
* standby mode.
* @property timeSource The time source for measuring progress towards timeouts.
*/
-internal data class TimeoutOptions(
+@RestrictTo(LIBRARY_GROUP)
+data class TimeoutOptions(
val initialTimeout: Duration = 45.seconds,
val additionalTime: Duration = 5.seconds,
val idleTimeout: Duration = 5.seconds,
@@ -68,7 +70,8 @@
* those of successive recompositions) to [Session.processEmittableTree]. After the initial
* composition, the worker blocks on [Session.receiveEvents] until [Session.close] is called.
*/
-internal class SessionWorker(
+@RestrictTo(LIBRARY_GROUP)
+class SessionWorker(
appContext: Context,
private val params: WorkerParameters,
private val sessionManager: SessionManager = GlanceSessionManager,
@@ -254,7 +257,7 @@
* called or this call is cancelled. This can be used to run a session without
* [GlanceSessionManager], i.e. without starting a worker.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(LIBRARY_GROUP)
suspend fun Session.runSession(context: Context) {
noopTimer { runSession(context, this@runSession, TimeoutOptions()) }
}
diff --git a/glance/glance/src/main/java/androidx/glance/session/TimerScope.kt b/glance/glance/src/main/java/androidx/glance/session/TimerScope.kt
index a7e6c105..d4882a2 100644
--- a/glance/glance/src/main/java/androidx/glance/session/TimerScope.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/TimerScope.kt
@@ -16,6 +16,8 @@
package androidx.glance.session
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -37,7 +39,8 @@
}
/** This interface is similar to [kotlin.time.TimeSource], which is still marked experimental. */
-internal fun interface TimeSource {
+@RestrictTo(LIBRARY_GROUP)
+fun interface TimeSource {
/** Current time in milliseconds. */
fun markNow(): Long
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 99e5b63..932cb8d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -56,7 +56,7 @@
ksp = "2.0.10-1.0.24"
ktfmt = "0.50"
leakcanary = "2.13"
-media3 = "1.1.0"
+media3 = "1.4.1"
metalava = "1.0.0-alpha12"
mockito = "2.25.0"
moshi = "1.13.0"
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 4a0d0b9..de71d39 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -31,6 +31,45 @@
=7AnO
-----END PGP PUBLIC KEY BLOCK-----
+pub 81C27DE945332233
+sub 126BBD1EF340F4BD
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQMuBEvACI4RCACNKnrdBk54/vBJSu2RJCG3VXR1h5DPGxJR1QIXDylZC8g4XQoj
+0cE7Ij61hd1VYAWlzlrfXtHi80BIXoGza61OaA0pUCxTuLyAL22x0GmdNpGBkigz
+r58LnwaqTxmGwQk/1mjJID2lDzi+GeEtKZd2xmYg7thIYAaKTQaQAzShfsntIdtl
+8XH7X7qpze+CGqh1cQR89aJXjdaspj9hT4Bgj5EfQk3DKUXDXAD4lg5IP2mLGpeb
+MzBPdhdvF1L6nR+CbNgwCFdDwcqsYxaAc2gxFUAoX4U9p/rG4lfZ8ft3zkKv/N0c
+2bRTpMR3HCSk/Q6io6C8JfEynpTXE1HjG633AQCpTien60HqyCD5o8riRDOeSN8F
+Ib7CSH5l9A+eVcPWGQf/UuYeEjOPMl9RChB6ZpqmjwnXpDWlE8hVaWVOHp80CDuB
+gxEcsWJ62VCiXJPrC7fBrQo6rjaSioP8rT2Ge8CSESANVBFLtuyhpNFeek0SJGQ9
+0bCplP9cizGDCVKxiEH8Toi1ifzeFRQYjum22YElrhpcPQhf0PLIxUmllFHr1P+t
+f8jlfkf23IgdhAhUedVloh0oPS0epixPB098acZCRKPoYZzTXmEXjp5q+JGPE/Fh
+3Rlt0SGXEcrHILQThpGjn5xG9xzH+WNYz6kZ01CLpgUdNvB291C0lvsVq3r4U0lq
+eCZCNJ4JfCm3KYzAt603nlWILnmTynavaWDO2JYhXQf+OyuDPrOrYdkELczLKI4p
++1pEaVLQ8dZRL0W/XJ+RFMtmWyoQ4ypqlYkhxj+zslaG9UAOBwodIACMvlGrVHMi
+BaY0q+/9oZtMqEKN+jp8z/sYyDlOPFeDOU00vk7XU0v6Vy4Jm9ogSitlMRuvSQxr
+XGh1tDLbz+3MNyFjISdHxtc2gLqA74uAfIfI0Wwkx07xLwiI4y/GR6BB4oHVmytn
+R2jjTquLfbNXRwf/4x1GVj/V7ztszVqNa3n4LtDcK0cfXFfwJ7/skhuGmReyzxjA
+yTiSlbCf13zA6GLr1qZaJv3t8iSNA/TfP9NplbZoaROLhHSOp++GwplpbJFaBec2
+XrkCDQRLwAiOEAgA/CUGWGQsNucNS2dDPbk/lUx6MaFqRV+CcwAsCLUPCBQ22Crz
+T1OvwnybE+/wc9KxqBsGcQxg8STGP01nFEttn755O55Avk19kWP5EWVzndIqsjFb
+cInQHVAeglz6F0a/7SFmaznrKvWCeHwjawEbAQXkCd8ZPRqeP2RWpwcxjoxFJRhQ
+nVaEJ2u8XAU5RCrfsnfaRD/99NcCrE+5xfyWncF6n5FEmgVt9GQNyjYZPzO3Ikzj
+mpay0nboxpocDV3iTJe1aRBAZfUxP9R4mFMb0R7/6CP4YGKF4CnKCzUZFaD7aswh
+mi+VVVFOcyurT+VJrX7Iaxe/305JWqKy0G/W0wADBgf/TZE2OQJ2EC+Dqkk74I1e
+UPcVwsdDdzhVsRvjeyISg03nuTT/VZ/9qObCe69l1M4SfJNoqLm2QD8dUzWTBG7l
+SseL4HjMckapIql8Vy5lNhnnzOjk2znIvaL4CuS0vFSGaXC0FS52BhtbxHmUsHr7
+xgASj95Us7dp3C9oO/iD+k1rWpoD7QungfXP7HrXxzoFiYFZhcY+YMdM2K/pr+Ba
+z3vakSsNGU1vs96l94QWGUf681xGlyw2uyWRuNPWhMYgGSJ05LIAtm3vIcWfNp7r
+T14lVSSJCqEP8/f/50N5MeqBXEDrOO4lj1meUL5MK8Nxk6qEacYaMwxTpz5losxt
+7IhhBBgRCAAJBQJLwAiOAhsMAAoJEIHCfelFMyIzCE4A/iu1wm8vFwBxrqYWOO/c
+2BbjtX4tROpH2+P2Fk13iYkMAP4xCfyw10teMvj6fu3u9+H1XvC1AQZTqHnaG/8Y
+ss9xVg==
+=Zs5g
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 82B5574242C20D6F
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: BCPG v@RELEASE_NAME@
@@ -3675,6 +3714,42 @@
=BZt3
-----END PGP PUBLIC KEY BLOCK-----
+pub E3706F202E9D239F
+uid Kevin Galligan <[email protected]>
+
+sub D78536E5591A9F98
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQENBFv2t+sBCAD0cnAam8BG5SU9U2raD+KzaZI2q16YPR0DJwTGoeYyD2eDgVJl
+p6rRxuOwMxXz+jqbgZRpXf3Obww6zD8w9fKZLoMRHOlUL2su+/9SDvTmfGPMLAJN
+4+mKkQQC7AorsCMJdOdj7qHI8yNTACVXfUvGFKsjBHfF/VDJJE/Epfe5TAWO+7wI
+SbUr4rwDBFKs+ImrKFuYJ1E/0fe4KI4o90wHix/dIlorCcLA6lVMEv7+4K1hKxI6
+n06ePVOkAvyd8fhF5vuilPSj7z/maKuJk4sxiDukDbhUP0Pi1sm+Q5CVhHxr1sMb
+vbuofW2LEDy9vXOH82Yx8cEorG3wuYPLUZQhABEBAAG0JEtldmluIEdhbGxpZ2Fu
+IDxrZ2FsbGlnYW5AZ21haWwuY29tPrkBDQRb9rfrAQgAwPWCdDqGjwiu/ZeqTnVy
++RjKz0WVLbto+oejZc6IpoV9tfhuPe0RQCaN/XQO7caydd6r3O/UPLIhM96n0M22
+fl8UlAB26ktPk/qB3P5/Ct7JwcvQL63JN/VeVkTokhIM7yRoTaN3v4hXxd3iKUqG
+tfJroTG8P2qAgYp6Ip9toobJjR+E2H2TqzhVpiQi1O3ckwJoswt/CcLvOAumNvNi
+vG7bscacdNbKFvgdF9c7X5pdz+tpIroGUTP4T+ks0JYg/mUb3vi6M5di4j/Th3Zq
+7LvxAsCQ4PnTgwMqOGsg1zr0tDUNrYGKKaDnO2GDFB4p0Qp4v850MLSdu1ikcR8z
+VwARAQABiQE8BBgBCAAmAhsMFiEEMTp3/l2aFlc/Qz1043BvIC6dI58FAl/rcWgF
+CQe3IH0ACgkQ43BvIC6dI5+U7ggA4b1RiObBRlUBMtHWSzW4kfll3DzeJ50wMI//
+teDUrUFver7cUgAL78TppHzSAop5OVdhGgQ4Pky1jOwut16DPYcyk9VFM+36Osi2
+br6/+BlerFTAPGwqA89J6Nn5Hp8RwRViuZETRDmMa37kzP+kyOnqpG9qyc9g7snx
+VyL9/pr+2nJUujqOz/wQJ3pVhQfdAWD4qxn3l2hbTZodjHA7xrFEA/Bjmws4FWmG
+YNEPWh5Y2xP6cIYLvvCWv0EfTpVecmxhGRuEQnVjNVHd+Cvj0kCQFA0crVqeua9o
+xdguiKIAQGZSY/FBfZSb/qlA2p4qOVcCyNZu/D6KfuZ7V/nHkIkBPAQYAQgAJhYh
+BDE6d/5dmhZXP0M9dONwbyAunSOfBQJb9rfrAhsMBQkDwmcAAAoJEONwbyAunSOf
+rjAH/AtAgoux6VBs14KqF3pHybeWeC8sRinetIPzVDS+tdWIkjQlumN8jtJ2yyDc
+gxaA9uWMBr2RHhCx025C2vdZS1tAzsl5nph2tmMSB23XxXw0nCesAtVCG13SdYbY
+GmqP9RlTFCkhCLcbTMTsruZ/RGY/pSQ94ZQUnN5j+/f580laNcBcWDQVxFigtbT6
+nD9sodquxSrNudDLMJtQKyPjp73t8WqRK2XIVXLti+wwRjRa/toOKxQ9WN5qJWB6
+UozXpk2LhYACdb0srg4k7gFESpaMveYkQYbpDyrg62NUvVAhOhWjorx9uZlIxukz
+oet48NTC5KcQaZnUl4QNhii5r5Y=
+=XJL9
+-----END PGP PUBLIC KEY BLOCK-----
+
pub E3822B59020A349D
sub 9351716690874F25
sub 60EB70DDAAC2EC21
@@ -4347,6 +4422,35 @@
=Jcla
-----END PGP PUBLIC KEY BLOCK-----
+pub 021E3BE573F727ED
+uid Martin Grebac <[email protected]>
+
+sub BDF1323BF318930C
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQENBE61V0EBCADCoY3yP7fLCiOl51HcW2AhJmsit4U3YHihnoiX2jieO0Xw5grz
+esFdx5tP8OTwh3YjEXp3Q2UzbAr25di3YhImVw+kLa+cpJPED0DNboR3zySjcehB
+/Z7ugel2MjZAUPMnRXYGCMhf4e4EufJDN7md86Ky8Fml/lImJvF+9wY1UrrroBLR
+wNqy5FheOoPIMsEV2u8fNGS2PiqJSTiSFzLwJvVxT7vR9C+ZuNWjCdYcYxpr6Drc
+fUpWDU/q0cOoHuZnIulBPo38GBF8lwELgJC9HxhqdYjXl+l7eZlbDFADNME0Mm25
+fcnnwWTJaBEn36ui70731cASsOMnfOxW2AVfABEBAAG0KE1hcnRpbiBHcmViYWMg
+PG1hcnRpbi5ncmViYWNAb3JhY2xlLmNvbT65AQ0ETrVXQQEIAOAsXQH0kFQhfz7Z
+rWQwjCdpE4vmmIQNHCK7lC0x7i+1BkWkdpoTmE5F7soh07zlIN4x+Ph3dw2FRAIu
+zQYb8ZO8ikGTCzkdVUa6yGYowBJArnrjQZYoTJWLENqAC47/e73s5JHo/mb2rF5R
+u19l4A/xJUZ6idjBNZ9pXVJxxmYdajA+B3Muft/dUKtEg1TjWXziAh5iqXqBG49Y
+G/TQdQeHPqaizhG2KeRSmPLkrBFI+ehj6qLBwf3S5KzFWrQGxbHJoVLSG32lJhzf
+fURysXoAlKEoF7BL1PShG8szMEe/x52xU2/na1x8dvhT4X60OMYS2Etd892+Zz8k
+ekIyOp8AEQEAAYkBHwQYAQIACQUCTrVXQQIbDAAKCRACHjvlc/cn7RUPCACbEFsB
+ZhSf3K3L5at+Od4dMuSmsIRT+GarrUg1gDNeCkA4qHI0xhDt+AyM4B0CzgoO0M08
+9xln4NVpvGM7GIt5DW0OlvSz1fJl+OU2tFyBiPV7/rNQh4eoEhQ9cXdEyKW/xSRJ
+7rPHXGXK0Ty1L3haQXtH3bvlVdgNjwM07VXHNhDhv/ibWvJcywX1zr3gRH30AmrG
+MASErCvzBz1m2953e8ijTGhKynjOFhJ/aHWT7QF/ugjDPILZP782fdiww7ByDuzh
+0FSvMv+4beiBt86Ky5STNQeePIWwPDJwTjktJ6fQm8J1VeBrs58JjmjbmRQ0Hs65
++zsMlK6ZweSFADZU
+=dWYO
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 02216ED811210DAA
sub 8C40458A5F28CF7B
-----BEGIN PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 3cbe2f3..1ab52b7 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -11,9 +11,6 @@
</key-servers>
<trusted-artifacts>
<trust file="apiLevels.json" reason="We do not sign this metadata"/>
- <trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
- <trust group="androidx[.]annotation" version="1\.9\.0-alpha[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
- <trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
<trust group="com.android.ndk.thirdparty" reason="b/215430394"/>
<trust group="com.android.tools" name="desugar_jdk_libs" reason="b/215430394"/>
<trust group="com.android.tools" name="desugar_jdk_libs_configuration" reason="b/215430394"/>
@@ -57,6 +54,10 @@
<trust group="^com[.]android($|([.].*))" version="8.0.0" regex="true" reason="old version, before signing"/>
<trust group="^com[.]android($|([.].*))" version="8.1.0" regex="true" reason="old version, before signing"/>
<trust group="^com[.]android($|([.].*))" version="8.5.0-alpha05" regex="true" reason="old version, before signing"/>
+ <trust group="androidx[.]annotation" version="1\.9\.0-alpha[0-9][2-9]" regex="true" reason="New versions, not yet signed"/>
+ <trust group="androidx[.]annotation" version="1\.[0-7]\..*" regex="true" reason="Old versions, before signing"/>
+ <trust group="androidx[.]collection" version="1\.5\.0-alpha[0-9][1-9]" regex="true" reason="Old versions, before signing"/>
+ <trust group="androidx[.]collection" version="1\.[0-3]\..*" regex="true" reason="Old versions, before signing"/>
</trusted-artifacts>
<trusted-keys>
<trusted-key id="00089EE8C3AFA95A854D0F1DF800DD0933ECF7F7" group="com.google.guava"/>
@@ -155,6 +156,7 @@
<trusted-key id="2E5B73C6EFD2EB453104C2EAE6EC76B4C6D3AE8E" group="com.google.protobuf"/>
<trusted-key id="2E92113263FC31C74CCBAAB20E91C2DE43B72BB1" group="org.ec4j.core"/>
<trusted-key id="3051D45031E13516A6E8FAFF280D66A55F5316C5" group="org.bitbucket.b_c"/>
+ <trusted-key id="313A77FE5D9A16573F433D74E3706F202E9D239F" group="co.touchlab"/>
<trusted-key id="31BAE2E51D95E0F8AD9B7BCC40A3C4432BD7308C" group="com.googlecode.juniversalchardet"/>
<trusted-key id="31FAE244A81D64507B47182E1B2718089CE964B8" group="com.thoughtworks.qdox"/>
<trusted-key id="3288B8BE8512D6C0CA185268C51E6CBC7FF46F0B">
@@ -204,6 +206,7 @@
<trusted-key id="4BF79B8259007B566D2FCE82296CD27F60EED12C" group="com.google.crypto.tink"/>
<trusted-key id="4DADED739CDF2CD0E48E0EC44044EDF1BB73EFEA" group="jaxen" name="jaxen"/>
<trusted-key id="4DB1A49729B053CAF015CEE9A6ADFC93EF34893E" group="org.hamcrest"/>
+ <trusted-key id="4F332021E20F59100C978257021E3BE573F727ED" group="net.java.dev.msv"/>
<trusted-key id="4F7E32D440EF90A83011A8FC6425559C47CC79C4">
<trusting group="com.sun.activation"/>
<trusting group="javax.activation"/>
@@ -257,18 +260,19 @@
<trusted-key id="6A814B1F869C2BBEAB7CB7271A2A1C94BDE89688" group="org.codehaus.plexus"/>
<trusted-key id="6BDACA2C0493CCA133B372D09C4F7E9D98B1CC53" group="org.apache"/>
<trusted-key id="6CB87B18A453990EAC9453F87D713008CC07E9AD" group="xerces" name="xercesImpl"/>
+ <trusted-key id="6D98490C6F1ACDDD448E45954F77679369475BAA" group="com.yarnpkg"/>
<trusted-key id="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC">
<trusting group="com.sun.activation"/>
<trusting group="jakarta.activation"/>
</trusted-key>
<trusted-key id="6EAD752B3E2B38E8E2236D7BA9321EDAA5CB3202" group="ch.randelshofer" name="fastdoubleparser"/>
<trusted-key id="6F538074CCEBF35F28AF9B066A0975F8B1127B83">
- <trusting group="org.jetbrains.kotlin"/>
- <trusting group="org.jetbrains.kotlin.jvm"/>
- <trusting group="org.jetbrains.kotlin.plugin.serialization"/>
<trusting group="" name="kotlin-native-prebuilt-linux-x86_64"/>
<trusting group="" name="kotlin-native-prebuilt-macos-aarch64"/>
<trusting group="" name="kotlin-native-prebuilt-macos-x86_64"/>
+ <trusting group="org.jetbrains.kotlin"/>
+ <trusting group="org.jetbrains.kotlin.jvm"/>
+ <trusting group="org.jetbrains.kotlin.plugin.serialization"/>
</trusted-key>
<trusted-key id="6F656B7F6BFB238D38ACF81F3C27D97B0C83A85C" group="com.google.errorprone"/>
<trusted-key id="6F7E5ACBCD02DB60DFD232E45E1F79A7C298661E">
@@ -287,7 +291,6 @@
<trusting group="org.jvnet.staxex"/>
<trusting group="^com[.]sun($|([.].*))" regex="true"/>
</trusted-key>
- <trusted-key id="6D98490C6F1ACDDD448E45954F77679369475BAA" group="com.yarnpkg"/>
<trusted-key id="713DA88BE50911535FE716F5208B0AB1D63011C7" group="org.apache.tomcat" name="annotations-api"/>
<trusted-key id="7186BBF993566D8C2F4F7ED7D945E643368FEF62" group="io.github.eisop"/>
<trusted-key id="719F7C29985A8E95F58F47194D8159D6A1159B69" group="dev.zacsweers.moshix"/>
@@ -343,13 +346,14 @@
</trusted-key>
<trusted-key id="8B39C4ACE0F448789FE19C8BAC0E2034B1389C89" group="androidx.build.gradle.gcpbuildcache" name="gcpbuildcache"/>
<trusted-key id="8DF3B0AA23ED78BE5233F6C2DEA3D207428EF16D" group="com.linkedin.dexmaker"/>
- <trusted-key id="8E3A02905A1AE67E7B0F9ACD3967D4EDA591B991" group="org.jetbrains.kotlinx" name="kotlinx-html-jvm"/>
+ <trusted-key id="8E3A02905A1AE67E7B0F9ACD3967D4EDA591B991" group="org.jetbrains.kotlinx"/>
<trusted-key id="8F9A3C6D105B9F57844A721D79E193516BE7998F" group="org.dom4j" name="dom4j"/>
<trusted-key id="908366594E746BF3C449F5622BE5D98F751F4136" group="org.pcollections"/>
<trusted-key id="90EE19787A7BCF6FD37A1E9180C08B1C29100955">
<trusting group="com.jakewharton.android.repackaged"/>
<trusting group="com.squareup" name="javawriter"/>
<trusting group="com.squareup.retrofit2"/>
+ <trusting group="com.squareup.wire"/>
</trusted-key>
<trusted-key id="95115197C5227C0887299D000F9FE62F88E938D8" group="com.google.dagger"/>
<trusted-key id="98465301A4939C0279F2E847D89D05374952262B" group="org.jetbrains.dokka"/>
@@ -385,6 +389,7 @@
<trusting group="com.android"/>
<trusting group="com.android.databinding"/>
<trusting group="com.android.kotlin.multiplatform.library"/>
+ <trusting group="com.android.settings"/>
<trusting group="com.android.tools"/>
<trusting group="com.android.tools.analytics-library"/>
<trusting group="com.android.tools.build"/>
@@ -396,7 +401,6 @@
<trusting group="com.android.tools.layoutlib"/>
<trusting group="com.android.tools.lint"/>
<trusting group="com.android.tools.utp"/>
- <trusting group="com.android.settings"/>
<trusting group="^androidx\..*" regex="true"/>
</trusted-key>
<trusted-key id="A6D6C97108B8585F91B158748671A8DF71296252" group="^com[.]squareup($|([.].*))" regex="true"/>
@@ -472,7 +476,10 @@
<trusted-key id="D066F5C471D32A00D244F99D6ED0F678B90EB06E" group="com.github.johnrengelman"/>
<trusted-key id="D196A5E3E70732EEB2E5007F1861C322C56014B2" group="commons-lang"/>
<trusted-key id="D433F9C895710DB8AB087FA6B7C3B43D18EAA8B7" group="org.codehaus.mojo"/>
- <trusted-key id="D477D51812E692011DB11E66A6EA2E2BF22E0543" group="io.github.java-diff-utils"/>
+ <trusted-key id="D477D51812E692011DB11E66A6EA2E2BF22E0543">
+ <trusting group="com.github.jsqlparser"/>
+ <trusting group="io.github.java-diff-utils"/>
+ </trusted-key>
<trusted-key id="D4C89EA4AAF455FD88B22087EFE8086F9E93774E" group="junit"/>
<trusted-key id="D54A395B5CF3F86EB45F6E426B1B008864323B92" group="org.antlr"/>
<trusted-key id="D5F46BC0B86AF5DC56DF58F05E975CB00C643DBF" group="com.google.inject"/>
@@ -503,6 +510,7 @@
<trusted-key id="E01ED293981AE484403B65D7DA70BCBA6D76AD03" group="com.charleskorn.kaml"/>
<trusted-key id="E0D98C5FD55A8AF232290E58DEE12B9896F97E34" group="org.pcollections"/>
<trusted-key id="E2ACB037933CDEAAB7BF77D49A2C7A98E457C53D" group="org.springframework"/>
+ <trusted-key id="E376140124BED4E3C06D227581C27DE945332233" group="com.google.auto"/>
<trusted-key id="E3A9F95079E84CE201F7CF60BEDE11EAF1164480" group="org.hamcrest"/>
<trusted-key id="E62231331BCA7E1F292C9B88C1B12A5D99C0729D" group="org.jetbrains"/>
<trusted-key id="E77417AC194160A3FABD04969A259C7EE636C5ED">
@@ -716,11 +724,21 @@
<sha256 value="f4d85c3e4d411694337cb873abea09b242b664bb013320be6105327c45991537" origin="Generated by Gradle" reason="https://github.com/google/guava/issues/7154"/>
</artifact>
</component>
+ <component group="com.google.guava" name="guava" version="33.0.0-jre">
+ <artifact name="guava-33.0.0-android.jar">
+ <sha1 value="cfbbdc54f232feedb85746aeeea0722f5244bb9a" origin="Generated by Gradle" reason="https://github.com/google/guava/issues/7154"/>
+ </artifact>
+ </component>
<component group="com.google.guava" name="guava" version="33.2.1-jre">
<artifact name="guava-33.2.1-android.jar">
<sha256 value="6b55fbe6ffee621454c03df7bea720d189789e136391a524e29506ff40654180" origin="Generated by Gradle" reason="https://github.com/google/guava/issues/7154"/>
</artifact>
</component>
+ <component group="com.google.guava" name="guava-parent" version="23.5-jre">
+ <artifact name="guava-parent-23.5-jre.pom">
+ <sha256 value="d69af85990f77ef54b4aa8e744c014de811cad8a62e790b177c219b59c75d918" reason="https://github.com/google/guava/issues/7154"/>
+ </artifact>
+ </component>
<component group="com.google.j2objc" name="j2objc-annotations" version="3.0.0">
<artifact name="j2objc-annotations-3.0.0.pom">
<pgp value="BDB5FA4FE719D787FB3D3197F6D4A1D411E9D1AE"/>
@@ -927,6 +945,14 @@
<sha256 value="06d119f29e22323371017da67d10c74a156b15f20d9c82116a57fee1454c6c68" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
+ <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.11">
+ <artifact name="kotlinx-benchmark-plugin-0.4.11.jar">
+ <sha256 value="5c337e082137eb3cdf64b27b11e16b97e5dd0a7905aa9c2d7c8c323601e5c31b" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ <artifact name="kotlinx-benchmark-plugin-0.4.11.module">
+ <sha256 value="af5e8d80674cd6f5bce92451296f3238c6524b880179d56f08187cef3de723ec" origin="Generated by Gradle" reason="Artifact is not signed"/>
+ </artifact>
+ </component>
<component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.8">
<artifact name="kotlinx-benchmark-plugin-0.4.8.jar">
<sha256 value="dcae0aabbae9374f6326e2dd26493dafaac0a7790d2d982a61fa3c779eea660c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -958,14 +984,6 @@
<sha256 value="35428beab195a9a9df2afd6614ff1e4e4feac1a2c689006b1d649abe4a9391e7" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.11">
- <artifact name="kotlinx-benchmark-plugin-0.4.11.jar">
- <sha256 value="5c337e082137eb3cdf64b27b11e16b97e5dd0a7905aa9c2d7c8c323601e5c31b" origin="Generated by Gradle" reason="Artifact is not signed"/>
- </artifact>
- <artifact name="kotlinx-benchmark-plugin-0.4.11.module">
- <sha256 value="af5e8d80674cd6f5bce92451296f3238c6524b880179d56f08187cef3de723ec" origin="Generated by Gradle" reason="Artifact is not signed"/>
- </artifact>
- </component>
<component group="org.jetbrains.skiko" name="skiko" version="0.7.7">
<artifact name="skiko-metadata-0.7.7-all.jar">
<sha256 value="c0c39f941138dd193676a3b1c28b8a36b7433ec760b979c69699241bdecee4cb" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -976,6 +994,17 @@
<pgp value="FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3"/>
</artifact>
</component>
+ <component group="org.nodejs" name="node" version="16.20.2">
+ <artifact name="node-16.20.2-darwin-arm64.tar.gz">
+ <sha256 value="6a5c4108475871362d742b988566f3fe307f6a67ce14634eb3fbceb4f9eea88c" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
+ </artifact>
+ <artifact name="node-16.20.2-darwin-x64.tar.gz">
+ <sha256 value="d7a46eaf2b57ffddeda16ece0d887feb2e31a91ad33f8774da553da0249dc4a6" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
+ </artifact>
+ <artifact name="node-16.20.2-linux-x64.tar.gz">
+ <sha256 value="c9193e6c414891694759febe846f4f023bf48410a6924a8b1520c46565859665" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
+ </artifact>
+ </component>
<component group="org.ow2" name="ow2" version="1.5">
<artifact name="ow2-1.5.pom">
<sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1059,6 +1088,14 @@
<sha256 value="4823677670797c2b71e8ebbe5437c41151f4e8edb7c6c0d473279715070f36d3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
+ <component group="relaxngDatatype" name="relaxngDatatype" version="20020414">
+ <artifact name="relaxngDatatype-20020414.jar">
+ <sha1 value="de7952cecd05b65e0e4370cc93fc03035175eef5" origin="Generated by relaxngDatatype" reason="Artifact from 2002. No SHA256"/>
+ </artifact>
+ <artifact name="relaxngDatatype-20020414.pom">
+ <sha1 value="4b062d8eb2bd190074fc686daea9531411b1e123" origin="Generated by relaxngDatatype" reason="Artifact from 2002. No SHA256"/>
+ </artifact>
+ </component>
<component group="xmlpull" name="xmlpull" version="1.1.3.1">
<artifact name="xmlpull-1.1.3.1.jar">
<sha256 value="34e08ee62116071cbb69c0ed70d15a7a5b208d62798c59f2120bb8929324cb63" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1075,16 +1112,5 @@
<sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="org.nodejs" name="node" version="16.20.2">
- <artifact name="node-16.20.2-darwin-arm64.tar.gz">
- <sha256 value="6a5c4108475871362d742b988566f3fe307f6a67ce14634eb3fbceb4f9eea88c" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
- </artifact>
- <artifact name="node-16.20.2-darwin-x64.tar.gz">
- <sha256 value="d7a46eaf2b57ffddeda16ece0d887feb2e31a91ad33f8774da553da0249dc4a6" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
- </artifact>
- <artifact name="node-16.20.2-linux-x64.tar.gz">
- <sha256 value="c9193e6c414891694759febe846f4f023bf48410a6924a8b1520c46565859665" origin="Generated by Node" reason="Artifact is not signed. Remove when https://github.com/nodejs/node/issues/53917 is resolved"/>
- </artifact>
- </component>
</components>
</verification-metadata>
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
index 598c67e..2110acd 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
@@ -70,13 +70,15 @@
@JvmOverloads
constructor(
surfaceView: SurfaceView,
- private val callback: Callback<T>,
+ callback: Callback<T>,
@HardwareBufferFormat val bufferFormat: Int = HardwareBuffer.RGBA_8888
) {
/** Target SurfaceView for rendering */
private var mSurfaceView: SurfaceView? = null
+ private var mCallback: Callback<T>? = null
+
/**
* Executor used to deliver callbacks for rendering as well as issuing surface control
* transactions
@@ -185,6 +187,7 @@
init {
mSurfaceView = surfaceView
+ mCallback = callback
surfaceView.holder.addCallback(mHolderCallback)
with(surfaceView.holder) {
if (surface != null && surface.isValid) {
@@ -253,7 +256,7 @@
}
canvas.drawColor(Color.BLACK, BlendMode.CLEAR)
}
- callback.onDrawFrontBufferedLayer(canvas, width, height, param)
+ mCallback?.onDrawFrontBufferedLayer(canvas, width, height, param)
}
@SuppressLint("WrongConstant")
@@ -293,7 +296,7 @@
transformHint
)
}
- callback.onFrontBufferedLayerRenderComplete(
+ mCallback?.onFrontBufferedLayerRenderComplete(
frontBufferSurfaceControl,
transaction
)
@@ -460,7 +463,7 @@
if (transform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
transaction.setBufferTransform(parentSurfaceControl, transform)
}
- callback.onMultiBufferedLayerRenderComplete(
+ mCallback?.onMultiBufferedLayerRenderComplete(
frontBufferSurfaceControl,
parentSurfaceControl,
transaction
@@ -566,7 +569,7 @@
with(multiBufferedRenderer) {
mMultiBufferedRenderNode?.let { renderNode ->
val canvas = renderNode.beginRecording()
- callback.onDrawMultiBufferedLayer(canvas, width, height, params)
+ mCallback?.onDrawMultiBufferedLayer(canvas, width, height, params)
renderNode.endRecording()
}
@@ -671,6 +674,7 @@
mSurfaceView?.holder?.removeCallback(mHolderCallback)
mSurfaceView = null
releaseInternal(cancelPending) {
+ mCallback = null
onReleaseComplete?.invoke()
mHandlerThread.quit()
}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt
index 8d031b0..bd41873 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrontBufferUtils.kt
@@ -101,7 +101,10 @@
return if (isSupported(HardwareBuffer.USAGE_FRONT_BUFFER)) {
FrontBufferUtils.BaseFlags or HardwareBuffer.USAGE_FRONT_BUFFER
} else {
- FrontBufferUtils.BaseFlags
+ // If the front buffer usage flag is not supported, configure the CPU write flag
+ // in order to prevent arm frame buffer compression from causing visual artifacts
+ // on certain devices like the Samsung Galaxy Tab S6 lite. See b/365131024
+ FrontBufferUtils.BaseFlags or HardwareBuffer.USAGE_CPU_WRITE_OFTEN
}
}
}
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index d5d6059..f363226 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -54,11 +54,13 @@
@SuppressCompatibility @androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi public interface HealthConnectFeatures {
method public int getFeatureStatus(int feature);
field public static final androidx.health.connect.client.HealthConnectFeatures.Companion Companion;
+ field public static final int FEATURE_READ_HEALTH_DATA_IN_BACKGROUND = 1; // 0x1
field public static final int FEATURE_STATUS_AVAILABLE = 2; // 0x2
field public static final int FEATURE_STATUS_UNAVAILABLE = 1; // 0x1
}
public static final class HealthConnectFeatures.Companion {
+ field public static final int FEATURE_READ_HEALTH_DATA_IN_BACKGROUND = 1; // 0x1
field public static final int FEATURE_STATUS_AVAILABLE = 2; // 0x2
field public static final int FEATURE_STATUS_UNAVAILABLE = 1; // 0x1
}
@@ -159,6 +161,7 @@
method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
+ field public static final String PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND = "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
field public static final String PERMISSION_WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE";
}
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 518453c..08fa34d 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -54,11 +54,13 @@
@SuppressCompatibility @androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi public interface HealthConnectFeatures {
method public int getFeatureStatus(int feature);
field public static final androidx.health.connect.client.HealthConnectFeatures.Companion Companion;
+ field public static final int FEATURE_READ_HEALTH_DATA_IN_BACKGROUND = 1; // 0x1
field public static final int FEATURE_STATUS_AVAILABLE = 2; // 0x2
field public static final int FEATURE_STATUS_UNAVAILABLE = 1; // 0x1
}
public static final class HealthConnectFeatures.Companion {
+ field public static final int FEATURE_READ_HEALTH_DATA_IN_BACKGROUND = 1; // 0x1
field public static final int FEATURE_STATUS_AVAILABLE = 2; // 0x2
field public static final int FEATURE_STATUS_UNAVAILABLE = 1; // 0x1
}
@@ -159,6 +161,7 @@
method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
+ field public static final String PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND = "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
field public static final String PERMISSION_WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE";
}
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 9b1779f..39ed3f1 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -77,10 +77,7 @@
}
testOptions.unitTests.includeAndroidResources = true
namespace "androidx.health.connect.client"
- compileSdk = 34
- compileSdkExtension = 10
- // TODO(b/352609562): Typedef with `toLong()`
- experimentalProperties["android.lint.useK2Uast"] = false
+ compileSdk = 35
}
androidx {
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 08b7839..fb7bcef 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -51,4 +51,5 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
index 33be6e1..68cb5f4 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
@@ -20,7 +20,9 @@
import androidx.activity.result.ActivityResultCaller
import androidx.annotation.Sampled
+import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
import androidx.health.connect.client.records.StepsRecord
@@ -42,8 +44,12 @@
requestPermission.launch(setOf(HealthPermission.getReadPermission(StepsRecord::class)))
}
+@OptIn(ExperimentalFeatureAvailabilityApi::class)
@Sampled
-fun RequestBackgroundReadPermission(activity: ActivityResultCaller) {
+fun RequestBackgroundReadPermission(
+ features: HealthConnectFeatures,
+ activity: ActivityResultCaller
+) {
val requestPermission =
activity.registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
@@ -55,7 +61,13 @@
}
}
- requestPermission.launch(setOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND))
+ if (
+ features.getFeatureStatus(HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND) ==
+ HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
+ ) {
+ // The feature is available, request background reads permission
+ requestPermission.launch(setOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND))
+ }
}
@Sampled
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index 14f303c..833b0ae 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -16,7 +16,6 @@
package androidx.health.connect.client.impl
-import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
@@ -75,7 +74,6 @@
@OptIn(ExperimentalFeatureAvailabilityApi::class)
@RunWith(AndroidJUnit4::class)
@MediumTest
-@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
class HealthConnectClientUpsideDownImplTest {
@@ -95,7 +93,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
@@ -117,12 +115,24 @@
}
@Test
+ fun backgroundReads_ext13_isSupported() {
+ assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 13)
+
+ assertThat(
+ healthConnectClient.features.getFeatureStatus(
+ HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND
+ )
+ )
+ .isEqualTo(HealthConnectFeatures.FEATURE_STATUS_AVAILABLE)
+ }
+
+ @Test
fun allFeatures_belowUExt13_noneSupported() {
assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) < 13)
val features =
listOf(
- HealthConnectFeatures.FEATURE_HEALTH_DATA_BACKGROUND_READ,
+ HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND,
HealthConnectFeatures.FEATURE_HEALTH_DATA_HISTORIC_READ,
HealthConnectFeatures.FEATURE_SKIN_TEMPERATURE,
HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
index 05fe60c..653ea0c 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -78,7 +78,7 @@
context.packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
)
- .requestedPermissions
+ .requestedPermissions!!
.filter { it.startsWith(PERMISSION_PREFIX) }
.toTypedArray()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectFeatures.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectFeatures.kt
index 3d4a6a0..c9621e4 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectFeatures.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectFeatures.kt
@@ -19,6 +19,7 @@
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
+import androidx.health.connect.client.feature.HealthConnectPlatformVersion
import androidx.health.connect.client.feature.HealthConnectVersionInfo
/** Interface for checking availability of features in [HealthConnectClient]. */
@@ -37,7 +38,7 @@
companion object {
/** Feature constant for reading health data in background. */
- @RestrictTo(RestrictTo.Scope.LIBRARY) const val FEATURE_HEALTH_DATA_BACKGROUND_READ = 1
+ const val FEATURE_READ_HEALTH_DATA_IN_BACKGROUND = 1
/** Feature constant for skin temperature. */
@RestrictTo(RestrictTo.Scope.LIBRARY) const val FEATURE_SKIN_TEMPERATURE = 2
@@ -52,7 +53,7 @@
@IntDef(
value =
[
- FEATURE_HEALTH_DATA_BACKGROUND_READ,
+ FEATURE_READ_HEALTH_DATA_IN_BACKGROUND,
FEATURE_SKIN_TEMPERATURE,
FEATURE_PLANNED_EXERCISE,
FEATURE_HEALTH_DATA_HISTORIC_READ
@@ -77,6 +78,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class FeatureStatus
- internal val FEATURE_TO_VERSION_INFO_MAP: Map<Int, HealthConnectVersionInfo> = mapOf()
+ private val SDK_EXT_13_PLATFORM_VERSION: HealthConnectPlatformVersion =
+ HealthConnectPlatformVersion(buildVersionCode = 34, sdkExtensionVersion = 13)
+
+ internal val FEATURE_TO_VERSION_INFO_MAP: Map<Int, HealthConnectVersionInfo> =
+ mapOf(
+ FEATURE_READ_HEALTH_DATA_IN_BACKGROUND to
+ HealthConnectVersionInfo(platformVersion = SDK_EXT_13_PLATFORM_VERSION)
+ )
}
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index d3b5712b..c4d4ccd 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client.permission
import androidx.annotation.RestrictTo
+import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
import androidx.health.connect.client.records.BasalBodyTemperatureRecord
import androidx.health.connect.client.records.BasalMetabolicRateRecord
@@ -151,10 +152,13 @@
*
* An attempt to read data in background without this permission may result in an error.
*
+ * This feature is dependent on the version of HealthConnect installed on the device. To
+ * check if it's available call [HealthConnectFeatures.getFeatureStatus] and pass
+ * [HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND] as an argument.
+ *
* @sample androidx.health.connect.client.samples.RequestBackgroundReadPermission
* @sample androidx.health.connect.client.samples.ReadRecordsInBackground
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY) // Hidden for now
const val PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND =
PERMISSION_PREFIX + "READ_HEALTH_DATA_IN_BACKGROUND"
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a03c963..e38be6e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -312,7 +312,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectFeaturesApkImplTest.kt
similarity index 97%
rename from health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
rename to health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectFeaturesApkImplTest.kt
index dea6c6b..149c680 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectFeaturesApkImplTest.kt
@@ -69,7 +69,7 @@
@OptIn(ExperimentalFeatureAvailabilityApi::class)
@RunWith(AndroidJUnit4::class)
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
-class HealthConnectApkImplTest {
+class HealthConnectFeaturesApkImplTest {
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -121,7 +121,7 @@
@Suppress("Deprecation")
packageInfo.versionCode = versionCode
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = true
+ packageInfo.applicationInfo!!.enabled = true
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index f1480f8..45237b9 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -181,7 +181,7 @@
fun allFeatures_defaultVersion_unavailable() {
val features =
listOf(
- HealthConnectFeatures.FEATURE_HEALTH_DATA_BACKGROUND_READ,
+ HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND,
HealthConnectFeatures.FEATURE_HEALTH_DATA_HISTORIC_READ,
HealthConnectFeatures.FEATURE_SKIN_TEMPERATURE,
HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
@@ -841,7 +841,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index 609c83a5..ff2ea5c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -186,7 +186,7 @@
val packageInfo = PackageInfo()
packageInfo.packageName = packageName
packageInfo.applicationInfo = ApplicationInfo()
- packageInfo.applicationInfo.enabled = enabled
+ packageInfo.applicationInfo!!.enabled = enabled
val packageManager = context.packageManager
shadowOf(packageManager).installPackage(packageInfo)
}
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 9439dd3..1c1c2b5 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -47,8 +47,7 @@
}
namespace "androidx.health.connect.testing"
testOptions.unitTests.includeAndroidResources = true
- compileSdk = 34
- compileSdkExtension = 10
+ compileSdk = 35
}
androidx {
diff --git a/health/connect/connect-testing/samples/build.gradle b/health/connect/connect-testing/samples/build.gradle
index c93f7cc..a9cb0e5 100644
--- a/health/connect/connect-testing/samples/build.gradle
+++ b/health/connect/connect-testing/samples/build.gradle
@@ -51,6 +51,7 @@
defaultConfig {
minSdkVersion 26
}
+ compileSdk = 35
}
tasks.withType(KotlinCompile).configureEach {
diff --git a/hilt/hilt-navigation-compose/build.gradle b/hilt/hilt-navigation-compose/build.gradle
index 7a73f47..19b6acb 100644
--- a/hilt/hilt-navigation-compose/build.gradle
+++ b/hilt/hilt-navigation-compose/build.gradle
@@ -45,7 +45,7 @@
dependencies {
implementation(libs.kotlinStdlib)
- api projectOrArtifact(":hilt:hilt-navigation")
+ api project(":hilt:hilt-navigation")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
@@ -59,13 +59,13 @@
androidTestImplementation(libs.hiltAndroid)
androidTestImplementation(libs.hiltAndroidTesting)
kspAndroidTest(libs.hiltCompiler)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
+ androidTestImplementation(project(":compose:material:material"))
androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
+ androidTestImplementation(project(":lifecycle:lifecycle-livedata-core"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
}
hilt {
@@ -79,6 +79,6 @@
inceptionYear = "2021"
description = "Navigation Compose Hilt Integration"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":hilt:hilt-navigation-compose-samples"))
+ samples(project(":hilt:hilt-navigation-compose-samples"))
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index c59ff49..06d2f91 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -37,7 +37,7 @@
implementation(libs.kotlinStdlib)
compileOnly(project(":annotation:annotation-sampled"))
- implementation(projectOrArtifact(":hilt:hilt-navigation-compose"))
+ implementation(project(":hilt:hilt-navigation-compose"))
}
androidx {
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..353b80f 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -50,9 +50,9 @@
jvmAndroidMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
api(libs.androidx.annotation)
- implementation(project(":collection:collection"))
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.collection:collection:1.4.3")
implementation(project(":ink:ink-geometry"))
implementation(project(":ink:ink-nativeloader"))
}
@@ -100,5 +100,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Define brushes for freehand input."
- metalavaK2UastEnabled = false
}
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
private val testFamily = BrushFamily(uri = "/brush-family:pencil")
@Test
- fun brushGetAndroidColor_getsCorrectColor() {
+ fun brushCreateAndroidColor_getsCorrectColor() {
val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
// Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
// the
// color internally as a ColorLong anyway).
val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+ assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
}
@Test
@@ -97,7 +97,7 @@
}
@Test
- fun brushBuilderAndroidColor_setsColor() {
+ fun brushBuilderSetAndroidColor_setsColor() {
val brush =
Brush.builder()
.setFamily(testFamily)
@@ -110,7 +110,7 @@
}
@Test
- fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
Brush.builder()
@@ -126,13 +126,13 @@
}
@Test
- fun brushWithAndroidColor_createsBrushWithColor() {
+ fun brushCreateWithAndroidColor_createsBrushWithColor() {
val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
assertThat(brush.colorLong).isEqualTo(testColorLong)
}
@Test
- fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
@@ -142,21 +142,10 @@
}
@Test
- fun brushUtilGetAndroidColor_getsCorrectColor() {
- val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
- // Note that expectedColor is not necessarily the same as testColor, because of precision
- // loss
- // when converting from testColor to testColorLong.
- val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
- }
-
- @Test
- fun brushUtilToBuilderWithAndroidColor_setsColor() {
+ fun brushToBuilderWithAndroidColor_setsColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
assertThat(newBrush.colorLong).isEqualTo(testColorLong)
assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
}
@Test
- fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
// unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
}
@Test
- fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+ fun createBrushBuilderWithAndroidColor_setsColor() {
val brush =
- BrushUtil.createBuilderWithAndroidColor(testColor)
+ createBrushBuilderWithAndroidColor(testColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -196,10 +185,10 @@
}
@Test
- fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
- BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+ createBrushBuilderWithAndroidColor(unsupportedColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -209,20 +198,4 @@
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
}
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
- val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
- assertThat(brush.colorLong).isEqualTo(testColorLong)
- }
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
- val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
- // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
- val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
- assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
- }
}
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
package androidx.ink.brush
@@ -22,17 +22,20 @@
import android.os.Build
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
/**
* The brush color as an [android.graphics.Color] instance, which can express colors in several
* different color spaces. sRGB and Display P3 are supported; a color in any other color space will
* be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
/**
* Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
* several different color spaces. sRGB and Display P3 are supported; a color in any other color
* space will be converted to Display P3.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
/**
* Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
* are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
setColorLong(color.pack())
/**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ Brush.Builder().setAndroidColor(color)
+
+/**
* Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
* encode several different color spaces. sRGB and Display P3 are supported; a color in any other
* color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
color: AndroidColor,
size: Float,
epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
- /**
- * The brush color as an [android.graphics.Color] instance, which can express colors in several
- * different color spaces. sRGB and Display P3 are supported; a color in any other color space
- * will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
- /**
- * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
- * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
- * Display P3 are supported; a color in any other color space will be converted to Display P3.
- * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
- * [copyWithAndroidColor] method.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
- brush.toBuilder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
- * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
- * a color in any other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
- Brush.Builder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
- * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
- * other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createWithAndroidColor(
- family: BrushFamily,
- color: AndroidColor,
- size: Float,
- epsilon: Float,
- ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt
index af765a0..9697d8c 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushBehavior.kt
@@ -145,7 +145,7 @@
val result = node as T
if (predicate(result)) return result
}
- stack.addAll(node.inputs())
+ stack.addAll(node.inputs)
}
return null
}
@@ -346,6 +346,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is BrushBehavior) return false
+ if (other === this) return true
return targetNodes == other.targetNodes
}
@@ -368,7 +369,7 @@
while (!stack.isEmpty()) {
stack.removeLast().let { node ->
orderedNodes.addFirst(node)
- stack.addAll(node.inputs())
+ stack.addAll(node.inputs)
}
}
@@ -1023,15 +1024,54 @@
}
}
+ /** Interpolation functions for use in an [InterpolationNode]. */
+ public class Interpolation private constructor(@JvmField internal val value: Int) {
+
+ internal fun toSimpleString(): String =
+ when (this) {
+ LERP -> "LERP"
+ INVERSE_LERP -> "INVERSE_LERP"
+ else -> "INVALID"
+ }
+
+ override fun toString(): String = PREFIX + this.toSimpleString()
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is Interpolation) return false
+ return value == other.value
+ }
+
+ override fun hashCode(): Int = value.hashCode()
+
+ public companion object {
+ /**
+ * Linear interpolation. Evaluates to the [InterpolationNode.startInput] value when the
+ * [InterpolationNode.paramInput] value is 0, and to the [InterpolationNode.endInput]
+ * value when the [InterpolationNode.paramInput] value is 1.
+ */
+ @JvmField public val LERP: Interpolation = Interpolation(0)
+ /**
+ * Inverse linear interpolation. Evaluates to 0 when the [InterpolationNode.paramInput]
+ * value is equal to the [InterpolationNode.startInput] value, and to 1 when the
+ * parameter is equal to the [InterpolationNode.endInput] value. Evaluates to null when
+ * the [InterpolationNode.startInput] and [InterpolationNode.endInput] values are equal.
+ */
+ @JvmField public val INVERSE_LERP: Interpolation = Interpolation(1)
+
+ private const val PREFIX = "BrushBehavior.Interpolation."
+ }
+ }
+
/**
* Represents one node in a [BrushBehavior]'s expression graph. [Node] objects are immutable and
* their inputs must be chosen at construction time; therefore, they can only ever be assembled
* into an acyclic graph.
*/
- public abstract class Node internal constructor() {
- /** Returns the ordered list of inputs that this node directly depends on. */
- public open fun inputs(): List<ValueNode> = emptyList()
-
+ public abstract class Node
+ internal constructor(
+ /** The ordered list of inputs that this node directly depends on. */
+ public val inputs: List<ValueNode>
+ ) {
/** Appends a native version of this [Node] to a native [BrushBehavior]. */
internal abstract fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long)
}
@@ -1040,7 +1080,7 @@
* A [ValueNode] is a non-terminal node in the graph; it produces a value to be consumed as an
* input by other [Node]s, and may itself depend on zero or more inputs.
*/
- public abstract class ValueNode internal constructor() : Node() {}
+ public abstract class ValueNode internal constructor(inputs: List<ValueNode>) : Node(inputs) {}
/** A [ValueNode] that gets data from the stroke input batch. */
public class SourceNode
@@ -1050,7 +1090,7 @@
public val sourceValueRangeLowerBound: Float,
public val sourceValueRangeUpperBound: Float,
public val sourceOutOfRangeBehavior: OutOfRange = OutOfRange.CLAMP,
- ) : ValueNode() {
+ ) : ValueNode(emptyList()) {
init {
require(sourceValueRangeLowerBound.isFinite()) {
"sourceValueRangeLowerBound must be finite, was $sourceValueRangeLowerBound"
@@ -1104,7 +1144,7 @@
}
/** A [ValueNode] that produces a constant output value. */
- public class ConstantNode constructor(public val value: Float) : ValueNode() {
+ public class ConstantNode constructor(public val value: Float) : ValueNode(emptyList()) {
init {
require(value.isFinite()) { "value must be finite, was $value" }
}
@@ -1135,9 +1175,7 @@
*/
public class FallbackFilterNode
constructor(public val isFallbackFor: OptionalInputProperty, public val input: ValueNode) :
- ValueNode() {
- override fun inputs(): List<ValueNode> = listOf(input)
-
+ ValueNode(listOf(input)) {
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
nativeAppendFallbackFilterNode(nativeBehaviorPointer, isFallbackFor.value)
}
@@ -1147,6 +1185,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is FallbackFilterNode) return false
+ if (other === this) return true
return isFallbackFor == other.isFallbackFor && input == other.input
}
@@ -1175,15 +1214,13 @@
// The [enabledToolTypes] val below is a defensive copy of this parameter.
enabledToolTypes: Set<InputToolType>,
public val input: ValueNode,
- ) : ValueNode() {
+ ) : ValueNode(listOf(input)) {
public val enabledToolTypes: Set<InputToolType> = unmodifiableSet(enabledToolTypes.toSet())
init {
require(!enabledToolTypes.isEmpty()) { "enabledToolTypes must be non-empty" }
}
- override fun inputs(): List<ValueNode> = listOf(input)
-
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
nativeAppendToolTypeFilterNode(
nativeBehaviorPointer = nativeBehaviorPointer,
@@ -1198,6 +1235,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is ToolTypeFilterNode) return false
+ if (other === this) return true
return enabledToolTypes == other.enabledToolTypes && input == other.input
}
@@ -1229,15 +1267,13 @@
public val dampingSource: DampingSource,
public val dampingGap: Float,
public val input: ValueNode,
- ) : ValueNode() {
+ ) : ValueNode(listOf(input)) {
init {
require(dampingGap.isFinite() && dampingGap >= 0.0f) {
"dampingGap must be finite and non-negative, was $dampingGap"
}
}
- override fun inputs(): List<ValueNode> = listOf(input)
-
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
nativeAppendDampingNode(nativeBehaviorPointer, dampingSource.value, dampingGap)
}
@@ -1247,6 +1283,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is DampingNode) return false
+ if (other === this) return true
return dampingSource == other.dampingSource &&
dampingGap == other.dampingGap &&
input == other.input
@@ -1271,9 +1308,7 @@
/** A [ValueNode] that maps an input value through a response curve. */
public class ResponseNode
constructor(public val responseCurve: EasingFunction, public val input: ValueNode) :
- ValueNode() {
- override fun inputs(): List<ValueNode> = listOf(input)
-
+ ValueNode(listOf(input)) {
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
when (responseCurve) {
is EasingFunction.Predefined ->
@@ -1312,6 +1347,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is ResponseNode) return false
+ if (other === this) return true
return responseCurve == other.responseCurve && input == other.input
}
@@ -1372,9 +1408,7 @@
public val operation: BinaryOp,
public val firstInput: ValueNode,
public val secondInput: ValueNode,
- ) : ValueNode() {
- override fun inputs(): List<ValueNode> = listOf(firstInput, secondInput)
-
+ ) : ValueNode(listOf(firstInput, secondInput)) {
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
nativeAppendBinaryOpNode(nativeBehaviorPointer, operation.value)
}
@@ -1384,6 +1418,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is BinaryOpNode) return false
+ if (other === this) return true
return operation == other.operation &&
firstInput == other.firstInput &&
secondInput == other.secondInput
@@ -1404,6 +1439,55 @@
}
/**
+ * A [ValueNode] that interpolates between two inputs based on a parameter input. The specific
+ * kind of interpolation performed depends on the [Interpolation] parameter.
+ */
+ public class InterpolationNode
+ constructor(
+ /** What kind of interpolation to perform. */
+ public val interpolation: Interpolation,
+ /** The input whose value is used as the parameter within the interpolation range. */
+ public val paramInput: ValueNode,
+ /** The input whose value forms the start of the interpolation range. */
+ public val startInput: ValueNode,
+ /** The input whose value forms the end of the interpolation range. */
+ public val endInput: ValueNode,
+ ) : ValueNode(listOf(paramInput, startInput, endInput)) {
+ override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
+ nativeAppendInterpolationNode(nativeBehaviorPointer, interpolation.value)
+ }
+
+ override fun toString(): String =
+ "InterpolationNode(${interpolation.toSimpleString()}, $paramInput, $startInput, $endInput)"
+
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is InterpolationNode) return false
+ if (other === this) return true
+ return interpolation == other.interpolation &&
+ paramInput == other.paramInput &&
+ startInput == other.startInput &&
+ endInput == other.endInput
+ }
+
+ override fun hashCode(): Int {
+ var result = interpolation.hashCode()
+ result = 31 * result + paramInput.hashCode()
+ result = 31 * result + startInput.hashCode()
+ result = 31 * result + endInput.hashCode()
+ return result
+ }
+
+ /**
+ * Appends a native `BrushBehavior::InterpolationNode` to a native brush behavior struct.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeAppendInterpolationNode(
+ nativeBehaviorPointer: Long,
+ interpolation: Int,
+ )
+ }
+
+ /**
* A [TargetNode] is a terminal node in the graph; it does not produce a value and cannot be
* used as an input to other [Node]s, but instead applies a modification to the brush tip state.
* A [BrushBehavior] consists of a list of [TargetNode]s and the various [ValueNode]s that they
@@ -1415,7 +1499,7 @@
public val targetModifierRangeLowerBound: Float,
public val targetModifierRangeUpperBound: Float,
public val input: ValueNode,
- ) : Node() {
+ ) : Node(listOf(input)) {
init {
require(targetModifierRangeLowerBound.isFinite()) {
"targetModifierRangeLowerBound must be finite, was $targetModifierRangeLowerBound"
@@ -1428,8 +1512,6 @@
}
}
- override fun inputs(): List<ValueNode> = listOf(input)
-
override fun appendToNativeBrushBehavior(nativeBehaviorPointer: Long) {
nativeAppendTargetNode(
nativeBehaviorPointer,
@@ -1444,6 +1526,7 @@
override fun equals(other: Any?): Boolean {
if (other == null || other !is TargetNode) return false
+ if (other === this) return true
return target == other.target &&
targetModifierRangeLowerBound == other.targetModifierRangeLowerBound &&
targetModifierRangeUpperBound == other.targetModifierRangeUpperBound &&
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt
index 002412f..4e3a1ab 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/BrushPaint.kt
@@ -208,6 +208,12 @@
SRC_ATOP -> "BrushPaint.BlendMode.SRC_ATOP"
SRC_IN -> "BrushPaint.BlendMode.SRC_IN"
SRC_OVER -> "BrushPaint.BlendMode.SRC_OVER"
+ DST_OVER -> "BrushPaint.BlendMode.DST_OVER"
+ SRC -> "BrushPaint.BlendMode.SRC"
+ DST -> "BrushPaint.BlendMode.DST"
+ SRC_OUT -> "BrushPaint.BlendMode.SRC_OUT"
+ DST_ATOP -> "BrushPaint.BlendMode.DST_ATOP"
+ XOR -> "BrushPaint.BlendMode.XOR"
else -> "BrushPaint.BlendMode.INVALID($value)"
}
diff --git a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
index 90626ce..db2f8f5 100644
--- a/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
+++ b/ink/ink-brush/src/jvmAndroidMain/kotlin/androidx/ink/brush/InputToolType.kt
@@ -21,8 +21,8 @@
import kotlin.jvm.JvmStatic
/**
- * The type of input tool used in producing [com.google.inputmethod.ink.strokes.StrokeInput], used
- * by [BrushBehavior] to define when a behavior is applicable.
+ * The type of input tool used in producing [androidx.ink.strokes.StrokeInput], used by
+ * [BrushBehavior] to define when a behavior is applicable.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
diff --git a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt
index 18d8afe..2b071ca 100644
--- a/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt
+++ b/ink/ink-brush/src/jvmAndroidTest/kotlin/androidx/ink/brush/BrushBehaviorTest.kt
@@ -466,6 +466,42 @@
}
@Test
+ fun interpolationConstants_areDistinct() {
+ val list =
+ listOf<BrushBehavior.Interpolation>(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.Interpolation.INVERSE_LERP,
+ )
+ assertThat(list.toSet()).hasSize(list.size)
+ }
+
+ @Test
+ fun interpolationHashCode_withIdenticalValues_match() {
+ assertThat(BrushBehavior.Interpolation.LERP.hashCode())
+ .isEqualTo(BrushBehavior.Interpolation.LERP.hashCode())
+
+ assertThat(BrushBehavior.Interpolation.LERP.hashCode())
+ .isNotEqualTo(BrushBehavior.Interpolation.INVERSE_LERP.hashCode())
+ }
+
+ @Test
+ fun interpolationEquals_checksEqualityOfValues() {
+ assertThat(BrushBehavior.Interpolation.LERP).isEqualTo(BrushBehavior.Interpolation.LERP)
+
+ assertThat(BrushBehavior.Interpolation.LERP)
+ .isNotEqualTo(BrushBehavior.Interpolation.INVERSE_LERP)
+ assertThat(BrushBehavior.Interpolation.LERP).isNotEqualTo(null)
+ }
+
+ @Test
+ fun interpolationToString_returnsCorrectString() {
+ assertThat(BrushBehavior.Interpolation.LERP.toString())
+ .isEqualTo("BrushBehavior.Interpolation.LERP")
+ assertThat(BrushBehavior.Interpolation.INVERSE_LERP.toString())
+ .isEqualTo("BrushBehavior.Interpolation.INVERSE_LERP")
+ }
+
+ @Test
fun sourceNodeConstructor_throwsForNonFiniteSourceValueRange() {
assertFailsWith<IllegalArgumentException> {
BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, Float.NaN, 1f)
@@ -489,7 +525,7 @@
@Test
fun sourceNodeInputs_isEmpty() {
val node = BrushBehavior.SourceNode(BrushBehavior.Source.NORMALIZED_PRESSURE, 0f, 1f)
- assertThat(node.inputs()).isEmpty()
+ assertThat(node.inputs).isEmpty()
}
@Test
@@ -526,7 +562,7 @@
@Test
fun constantNodeInputs_isEmpty() {
- assertThat(BrushBehavior.ConstantNode(42f).inputs()).isEmpty()
+ assertThat(BrushBehavior.ConstantNode(42f).inputs).isEmpty()
}
@Test
@@ -557,7 +593,7 @@
val input = BrushBehavior.ConstantNode(0f)
val node =
BrushBehavior.FallbackFilterNode(BrushBehavior.OptionalInputProperty.PRESSURE, input)
- assertThat(node.inputs()).containsExactly(input)
+ assertThat(node.inputs).containsExactly(input)
}
@Test
@@ -622,7 +658,7 @@
fun toolTypeFilterNodeInputs_containsInput() {
val input = BrushBehavior.ConstantNode(0f)
val node = BrushBehavior.ToolTypeFilterNode(setOf(InputToolType.STYLUS), input)
- assertThat(node.inputs()).containsExactly(input)
+ assertThat(node.inputs).containsExactly(input)
}
@Test
@@ -702,7 +738,7 @@
fun dampingNodeInputs_containsInput() {
val input = BrushBehavior.ConstantNode(0f)
val node = BrushBehavior.DampingNode(BrushBehavior.DampingSource.TIME_IN_SECONDS, 1f, input)
- assertThat(node.inputs()).containsExactly(input)
+ assertThat(node.inputs).containsExactly(input)
}
@Test
@@ -765,7 +801,7 @@
fun responseNodeInputs_containsInput() {
val input = BrushBehavior.ConstantNode(0f)
val node = BrushBehavior.ResponseNode(EasingFunction.Predefined.EASE, input)
- assertThat(node.inputs()).containsExactly(input)
+ assertThat(node.inputs).containsExactly(input)
}
@Test
@@ -823,7 +859,7 @@
val firstInput = BrushBehavior.ConstantNode(0f)
val secondInput = BrushBehavior.ConstantNode(1f)
val node = BrushBehavior.BinaryOpNode(BrushBehavior.BinaryOp.SUM, firstInput, secondInput)
- assertThat(node.inputs()).containsExactly(firstInput, secondInput).inOrder()
+ assertThat(node.inputs).containsExactly(firstInput, secondInput).inOrder()
}
@Test
@@ -884,6 +920,90 @@
}
@Test
+ fun interpolationNodeInputs_containsInputsInOrder() {
+ val paramInput = BrushBehavior.ConstantNode(0.5f)
+ val startInput = BrushBehavior.ConstantNode(0f)
+ val endInput = BrushBehavior.ConstantNode(1f)
+ val node =
+ BrushBehavior.InterpolationNode(
+ interpolation = BrushBehavior.Interpolation.LERP,
+ paramInput = paramInput,
+ startInput = startInput,
+ endInput = endInput,
+ )
+ assertThat(node.inputs).containsExactly(paramInput, startInput, endInput).inOrder()
+ }
+
+ @Test
+ fun interpolationNodeToString() {
+ val node =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ assertThat(node.toString())
+ .isEqualTo(
+ "InterpolationNode(LERP, ConstantNode(0.5), ConstantNode(0.0), ConstantNode(1.0))"
+ )
+ }
+
+ @Test
+ fun interpolationNodeEquals_checksEqualityOfValues() {
+ val node1 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1).isEqualTo(node2)
+ assertThat(node1).isNotEqualTo(node3)
+ }
+
+ @Test
+ fun interpolationNodeHashCode_withIdenticalValues_match() {
+ val node1 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node2 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(1f),
+ )
+ val node3 =
+ BrushBehavior.InterpolationNode(
+ BrushBehavior.Interpolation.LERP,
+ BrushBehavior.ConstantNode(0.5f),
+ BrushBehavior.ConstantNode(0f),
+ BrushBehavior.ConstantNode(2f),
+ )
+ assertThat(node1.hashCode()).isEqualTo(node2.hashCode())
+ assertThat(node1.hashCode()).isNotEqualTo(node3.hashCode())
+ }
+
+ @Test
fun targetNodeConstructor_throwsForNonFiniteTargetModifierRange() {
val input = BrushBehavior.ConstantNode(0f)
assertFailsWith<IllegalArgumentException> {
@@ -911,7 +1031,7 @@
fun targetNodeInputs_containsInput() {
val input = BrushBehavior.ConstantNode(0f)
val node = BrushBehavior.TargetNode(BrushBehavior.Target.SIZE_MULTIPLIER, 0f, 1f, input)
- assertThat(node.inputs()).containsExactly(input)
+ assertThat(node.inputs).containsExactly(input)
}
@Test
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt b/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
index 6efc190..6ff4706 100644
--- a/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
+++ b/ink/ink-geometry/src/androidMain/kotlin/androidx/ink/geometry/AndroidGraphicsConversionExtensions.android.kt
@@ -20,7 +20,6 @@
import android.graphics.Matrix
import androidx.annotation.RestrictTo
-import androidx.ink.geometry.internal.getValue
import androidx.ink.geometry.internal.threadLocal
/** Scratch space to be used as the argument to [Matrix.getValues] and [Matrix.setValues]. */
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
*
* @return `this`
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+ public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
/**
* Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
package androidx.ink.geometry
-import androidx.annotation.RestrictTo
import androidx.ink.nativeloader.NativeLoader
/**
@@ -116,7 +115,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
return nativeMeshVecIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
return nativeMeshSegmentIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Triangle.intersects(
mesh: PartitionedMesh,
meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
return nativeMeshBoxIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Parallelogram.intersects(
mesh: PartitionedMesh,
meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
* coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
other: PartitionedMesh,
thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
point.intersects(this, meshToPoint)
@@ -578,7 +570,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
segment: Segment,
meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
triangle: Triangle,
meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
box.intersects(this, meshToBox)
@@ -619,7 +608,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
parallelogram: Parallelogram,
meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..c6d218a 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -20,28 +20,26 @@
import androidx.annotation.IntRange
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
-import androidx.ink.geometry.internal.getValue
import androidx.ink.geometry.internal.threadLocal
import androidx.ink.nativeloader.NativeLoader
/**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows ink to create strokes using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
*
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
* some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
* supports disjoint meshes such as dashed lines.
*
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
* index.
*
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
* however, from the perspective of a caller, its properties do not change over the course of its
* lifetime. The entire object is thread-safe.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class PartitionedMesh
/** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +71,53 @@
@VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
/**
- * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
- * exactly one render group, which are numbered in z-order: the group with index zero should be
- * rendered on bottom; the group with the highest index should be rendered on top.
+ * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+ * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+ * should be rendered on bottom; the group with the highest index should be rendered on top.
*/
@IntRange(from = 0)
- public val renderGroupCount: Int =
+ public fun getRenderGroupCount(): Int =
ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
/** The [Mesh] objects that make up this shape. */
private val meshesByGroup: List<List<Mesh>> = buildList {
- for (groupIndex in 0 until renderGroupCount) {
+ for (groupIndex in 0 until getRenderGroupCount()) {
val nativeAddressesOfMeshes =
ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
add(nativeAddressesOfMeshes.map(::Mesh))
}
}
+ private var _bounds: Box? = null
+
/**
- * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
- * is empty.
+ * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+ * [PartitionedMesh] is empty.
*/
- public val bounds: Box? = run {
+ public fun computeBoundingBox(): Box? {
+ // If we've already computed the bounding box, re-use it -- it won't change over the
+ // lifetime of
+ // this object.
+ if (_bounds != null) return _bounds
+
+ // If we have no meshes, then the bounding box is null.
+ if (meshesByGroup.isEmpty()) return null
+
val envelope = BoxAccumulator()
for (meshes in meshesByGroup) {
for (mesh in meshes) {
envelope.add(mesh.bounds)
}
}
- envelope.box
+ _bounds = envelope.box
+ return envelope.box
}
/** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and getRenderGroupCount()=${getRenderGroupCount()}"
}
return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
}
@@ -119,51 +128,51 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and getRenderGroupCount()=${getRenderGroupCount()}"
}
return meshesByGroup[groupIndex]
}
- /** The number of outlines that comprise this shape. */
+ /** Returns the number of outlines that comprise the render group at [groupIndex]. */
@IntRange(from = 0)
- public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and getRenderGroupCount()=${getRenderGroupCount()}"
}
return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
}
/**
- * The number of vertices that are in the outline at index [outlineIndex], and within the render
- * group at [groupIndex].
+ * Returns the number of vertices that are in the outline at [outlineIndex] in the render group
+ * at [groupIndex].
*/
@IntRange(from = 0)
- public fun outlineVertexCount(
+ public fun getOutlineVertexCount(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
): Int {
- require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
- "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+ require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+ "outlineIndex=$outlineIndex must be between 0 and getOutlineCount=${getOutlineCount(groupIndex)}"
}
return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
.also { check(it >= 0) }
}
/**
- * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
- * up to, but not including, [outlineCount]), and the vertex from within that outline at index
- * [outlineVertexIndex] (which can be up to, but not including, the result of calling
- * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
- * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+ * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+ * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+ * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+ * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+ * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
*/
public fun populateOutlinePosition(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
@IntRange(from = 0) outlineVertexIndex: Int,
outPosition: MutableVec,
- ) {
- val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+ ): MutableVec {
+ val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
"outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
"outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +187,7 @@
val (meshIndex, meshVertexIndex) = scratchIntArray
val mesh = meshesByGroup[groupIndex][meshIndex]
mesh.fillPosition(meshVertexIndex, outPosition)
+ return outPosition
}
/**
@@ -187,18 +197,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
triangle: Triangle,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -225,17 +235,20 @@
* [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
* [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
* itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+ * instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+ public fun computeCoverage(
+ box: Box,
+ boxToThis: AffineTransform = AffineTransform.IDENTITY,
+ ): Float =
ModeledShapeNative.modeledShapeBoxCoverage(
nativeAddress = nativeAddress,
boxXMin = box.xMin,
@@ -257,18 +270,18 @@
* of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
parallelogram: Parallelogram,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -295,18 +308,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.t
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
other: PartitionedMesh,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -327,7 +340,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(triangle, triangleToThis) > coverageThreshold
+ * computeCoverage(triangle, triangleToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -335,11 +348,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
triangle: Triangle,
coverageThreshold: Float,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -367,7 +380,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(box, boxToThis) > coverageThreshold
+ * computeCoverage(box, boxToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -375,10 +388,10 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
box: Box,
coverageThreshold: Float,
boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -404,7 +417,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(parallelogram, parallelogramToThis) > coverageThreshold
+ * computeCoverage(parallelogram, parallelogramToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -413,10 +426,10 @@
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
parallelogram: Parallelogram,
coverageThreshold: Float,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -444,7 +457,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(other, otherShapeToThis) > coverageThreshold
+ * computeCoverage(other, otherShapeToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -452,11 +465,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
other: PartitionedMesh,
coverageThreshold: Float,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +501,7 @@
override fun toString(): String {
val address = java.lang.Long.toHexString(nativeAddress)
- return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+ return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
}
protected fun finalize() {
@@ -551,7 +564,7 @@
)
/**
- * JNI method to construct C++ [ModeledShape] and [Triangle] objects and calculate coverage
+ * JNI method to construct C++ `ModeledShape` and `Triangle` objects and calculate coverage
* using them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
@@ -572,7 +585,7 @@
): Float
/**
- * JNI method to construct C++ [ModeledShape] and [Triangle] objects and calculate coverage
+ * JNI method to construct C++ `ModeledShape` and `Triangle` objects and calculate coverage
* using them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
@@ -591,8 +604,8 @@
): Float
/**
- * JNI method to construct C++ [ModeledShape] and [Parallelogram] objects and calculate coverage
- * using them.
+ * JNI method to construct C++ `ModeledShape` and `Quad` objects and calculate coverage using
+ * them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeParallelogramCoverage(
@@ -611,7 +624,7 @@
parallelogramToThisTransformF: Float,
): Float
- /** JNI method to construct C++ two [ModeledShape] objects and calculate coverage using them. */
+ /** JNI method to construct C++ two `ModeledShape` objects and calculate coverage using them. */
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeModeledShapeCoverage(
thisShapeNativeAddress: Long,
@@ -625,8 +638,8 @@
): Float
/**
- * JNI method to construct C++ [ModeledShape] and [Triangle] objects and call native
- * [coverageIsGreaterThan] on them.
+ * JNI method to construct C++ `ModeledShape` and `Triangle` objects and call native
+ * `CoverageIsGreaterThan` on them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeTriangleCoverageIsGreaterThan(
@@ -647,8 +660,8 @@
): Boolean
/**
- * JNI method to construct C++ [ModeledShape] and [Box] objects and call native
- * [coverageIsGreaterThan] on them.
+ * JNI method to construct C++ `ModeledShape` and `Rect` objects and call native
+ * `CoverageIsGreaterThan` on them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeBoxCoverageIsGreaterThan(
@@ -667,8 +680,8 @@
): Boolean
/**
- * JNI method to construct C++ [ModeledShape] and [Parallelogram] objects and call native
- * [coverageIsGreaterThan] on them.
+ * JNI method to construct C++ `ModeledShape` and `Quad` objects and call native
+ * `CoverageIsGreaterThan` on them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeParallelogramCoverageIsGreaterThan(
@@ -689,8 +702,8 @@
): Boolean
/**
- * JNI method to construct two C++ [ModeledShape] objects and call native
- * [coverageIsGreaterThan] on them.
+ * JNI method to construct two C++ `ModeledShape` objects and call native
+ * `CoverageIsGreaterThan` on them.
*/
// TODO: b/355248266 - @Keep must go in Proguard config file instead.
external fun modeledShapeModeledShapeCoverageIsGreaterThan(
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/internal/ThreadLocalDelegate.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/internal/ThreadLocalDelegate.kt
index af7bfed..9422c12 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/internal/ThreadLocalDelegate.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/internal/ThreadLocalDelegate.kt
@@ -16,12 +16,10 @@
package androidx.ink.geometry.internal
-import androidx.annotation.RestrictTo
import kotlin.reflect.KProperty
/**
- * Allows more convenient lambda syntax for declaring and initializing a [ThreadLocal]. Use with
- * `by` to treat it as a delegate and access its value implicitly.
+ * [ThreadLocal] subclass that can be used as a read-only delegate with the `by` operator.
*
* Example:
* ```
@@ -30,18 +28,13 @@
* foo.y = 6F
* ```
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun <T> threadLocal(initialValueProvider: () -> T): ThreadLocal<T> =
- object : ThreadLocal<T>() {
- override fun initialValue(): T = initialValueProvider()
- }
+internal fun <T> threadLocal(initialValueProvider: () -> T): ThreadLocalDelegate<T> =
+ ThreadLocalDelegate(initialValueProvider)
-/**
- * Allows a [ThreadLocal] to act as a delegate, so a `ThreadLocal<T>` can act in code like a simple
- * `T` object. This method doesn't need to be called explicitly, as it is an operator for access.
- * See [threadLocal] for easier syntax for declaration and initialization, as well as for examples.
- */
-@Suppress("NOTHING_TO_INLINE")
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public inline operator fun <T> ThreadLocal<T>.getValue(thisObj: Any?, property: KProperty<*>): T =
- get()!!
+internal class ThreadLocalDelegate<T> constructor(private val initialValueProvider: () -> T) :
+ ThreadLocal<T>() {
+ override fun initialValue(): T = initialValueProvider()
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline operator fun getValue(thisObj: Any?, property: KProperty<*>): T = get()!!
+}
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..adf567e 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,43 @@
class PartitionedMeshTest {
@Test
- fun bounds_shouldBeEmpty() {
+ fun computeBoundingBox_shouldBeEmpty() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.bounds).isNull()
+ assertThat(partitionedMesh.computeBoundingBox()).isNull()
}
@Test
- fun renderGroupCount_whenEmptyShape_shouldBeZero() {
- val partitionedMesh = PartitionedMesh()
+ fun computeBoundingBox_reusesAllocations() {
+ val partitionedMesh = buildTestStrokeShape()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+ val boundingBox = partitionedMesh.computeBoundingBox()
+ assertThat(partitionedMesh.computeBoundingBox()).isSameInstanceAs(boundingBox)
}
@Test
- fun outlineCount_whenEmptyShape_shouldThrow() {
+ fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
}
@Test
- fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
+ }
+
+ @Test
+ fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
+ val partitionedMesh = PartitionedMesh()
+
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
}
@Test
@@ -92,14 +100,14 @@
fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
val shape = buildTestStrokeShape()
- assertThat(shape.renderGroupCount).isEqualTo(1)
- assertThat(shape.outlineCount(0)).isEqualTo(1)
- assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+ assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+ assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
- val bounds = assertNotNull(shape.bounds)
+ val bounds = assertNotNull(shape.computeBoundingBox())
val p = MutableVec()
- for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+ for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
assertThat(p.x).isAtLeast(bounds.xMin)
assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +132,7 @@
@Test
fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
val partitionedMesh = buildTestStrokeShape()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
val shapeFormat = partitionedMesh.renderGroupFormat(0)
val meshes = partitionedMesh.renderGroupMeshes(0)
assertThat(meshes).isNotEmpty()
@@ -133,11 +141,11 @@
}
/**
- * Verifies that [PartitionedMesh.coverage] calls the correct JNI method for [PartitionedMesh]
- * and [Triangle].
+ * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
+ * [PartitionedMesh] and [Triangle].
*/
@Test
- fun coverage_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
+ fun computeCoverage_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
val partitionedMesh = buildTestStrokeShape()
val intersectingTriangle =
ImmutableTriangle(
@@ -152,34 +160,34 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
- * Verifies that [PartitionedMesh.coverage] calls the correct JNI method for [PartitionedMesh]
- * and [Box].
+ * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
+ * [PartitionedMesh] and [Box].
*/
@Test
- fun coverage_forPartitionedMeshAndBox_callsJniAndReturnsFloat() {
+ fun computeCoverage_forPartitionedMeshAndBox_callsJniAndReturnsFloat() {
val partitionedMesh = buildTestStrokeShape()
val intersectingBox =
ImmutableBox.fromTwoPoints(ImmutableVec(0f, 0f), ImmutableVec(100f, 100f))
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
- * Verifies that [PartitionedMesh.coverage] calls the correct JNI method for [PartitionedMesh]
- * and [Parallelogram].
+ * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
+ * [PartitionedMesh] and [Parallelogram].
*/
@Test
- fun coverage_forPartitionedMeshAndParallelogram_callsJniAndReturnsFloat() {
+ fun computeCoverage_forPartitionedMeshAndParallelogram_callsJniAndReturnsFloat() {
val partitionedMesh = buildTestStrokeShape()
val intersectingParallelogram =
ImmutableParallelogram.fromCenterAndDimensions(
@@ -196,17 +204,18 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+ .isEqualTo(0f)
}
/**
- * Verifies that [PartitionedMesh.coverage] calls the correct JNI method for two
+ * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for two
* [PartitionedMesh]es.
*/
@Test
- fun coverage_forTwoPartitionedMeshes_callsJniAndReturnsFloat() {
+ fun computeCoverage_forTwoPartitionedMeshes_callsJniAndReturnsFloat() {
val partitionedMesh = buildTestStrokeShape()
val intersectingShape =
Stroke(
@@ -221,17 +230,17 @@
)
.shape
- assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
- * Verifies that [PartitionedMesh.coverageIsGreaterThan] calls the correct JNI method for
+ * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
* [PartitionedMesh] and [Triangle].
*/
@Test
- fun coverageIsGreaterThan_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
+ fun computeCoverageIsGreaterThan_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
val partitionedMesh = buildTestStrokeShape()
val intersectingTriangle =
ImmutableTriangle(
@@ -246,14 +255,16 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+ assertThat(
+ partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+ )
.isFalse()
}
/**
- * Verifies that [PartitionedMesh.coverageIsGreaterThan] calls the correct JNI method for
+ * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
* [PartitionedMesh] and [Box].
*
* For this test, `partitionedMesh` consists of triangulation of a straight line [Stroke] from
@@ -263,25 +274,25 @@
* coverage of zero.
*/
@Test
- fun coverageIsGreaterThan_forPartitionedMeshAndBox_callsJniAndReturnsBoolean() {
+ fun computeCoverageIsGreaterThan_forPartitionedMeshAndBox_callsJniAndReturnsBoolean() {
val partitionedMesh = buildTestStrokeShape()
val intersectingBox =
ImmutableBox.fromTwoPoints(ImmutableVec(10f, 3f), ImmutableVec(15f, 5f))
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
.isFalse()
}
/**
- * Verifies that [PartitionedMesh.coverageIsGreaterThan] calls the correct JNI method for
+ * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
* [PartitionedMesh] and [Parallelogram].
*/
@Test
- fun coverageIsGreaterThan_forPartitionedMeshAndParallelogram_callsJniAndReturnsBoolean() {
+ fun computeCoverageIsGreaterThan_forPartitionedMeshAndParallelogram_callsJniAndReturnsBoolean() {
val partitionedMesh = buildTestStrokeShape()
val intersectingParallelogram =
ImmutableParallelogram.fromCenterAndDimensions(
@@ -298,16 +309,22 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+ .isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+ .isFalse()
assertThat(
- partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+ partitionedMesh.computeCoverageIsGreaterThan(
+ externalParallelogram,
+ 0f,
+ SCALE_TRANSFORM
+ )
)
.isFalse()
}
/**
- * Verifies that [PartitionedMesh.coverage] calls the correct JNI method for two
+ * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for two
* [PartitionedMesh]s.
*
* For this test, `partitionedMesh` consists of triangulation of a straight line [Stroke] from
@@ -318,7 +335,7 @@
* `partitionedMesh`, and has zero coverage.
*/
@Test
- fun coverageIsGreaterThan_forTwoPartitionedMeshes_callsJniAndReturnsBoolean() {
+ fun computeCoverageIsGreaterThan_forTwoPartitionedMeshes_callsJniAndReturnsBoolean() {
val partitionedMesh = buildTestStrokeShape()
val intersectingShape =
Stroke(
@@ -333,9 +350,9 @@
)
.shape
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -360,7 +377,7 @@
)
assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
- assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+ assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
}
diff --git a/ink/ink-rendering/api/current.txt b/ink/ink-rendering/api/current.txt
new file mode 100644
index 0000000..adc06c7
--- /dev/null
+++ b/ink/ink-rendering/api/current.txt
@@ -0,0 +1,28 @@
+// Signature format: 4.0
+package androidx.ink.rendering.android.canvas {
+
+ public interface CanvasStrokeRenderer {
+ method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ field public static final androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion Companion;
+ }
+
+ public static final class CanvasStrokeRenderer.Companion {
+ method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ }
+
+ public final class StrokeDrawScope {
+ method public void drawStroke(androidx.ink.strokes.Stroke stroke);
+ }
+
+}
+
+package androidx.ink.rendering.android.view {
+
+ public final class ViewStrokeRenderer {
+ ctor public ViewStrokeRenderer(androidx.ink.rendering.android.canvas.CanvasStrokeRenderer canvasStrokeRenderer, android.view.View view);
+ method public inline void drawWithStrokes(android.graphics.Canvas canvas, kotlin.jvm.functions.Function1<? super androidx.ink.rendering.android.canvas.StrokeDrawScope,kotlin.Unit> block);
+ method public void drawWithStrokes(android.graphics.Canvas canvas, kotlin.jvm.functions.Function2<? super android.graphics.Canvas,? super androidx.ink.rendering.android.canvas.StrokeDrawScope,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/biometric/biometric-ktx/api/res-current.txt b/ink/ink-rendering/api/res-current.txt
similarity index 100%
rename from biometric/biometric-ktx/api/res-current.txt
rename to ink/ink-rendering/api/res-current.txt
diff --git a/ink/ink-rendering/api/restricted_current.txt b/ink/ink-rendering/api/restricted_current.txt
new file mode 100644
index 0000000..e06360d
--- /dev/null
+++ b/ink/ink-rendering/api/restricted_current.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.ink.rendering.android.canvas {
+
+ public interface CanvasStrokeRenderer {
+ method public static androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ field public static final androidx.ink.rendering.android.canvas.CanvasStrokeRenderer.Companion Companion;
+ }
+
+ public static final class CanvasStrokeRenderer.Companion {
+ method public androidx.ink.rendering.android.canvas.CanvasStrokeRenderer create();
+ }
+
+ public final class StrokeDrawScope {
+ method public void drawStroke(androidx.ink.strokes.Stroke stroke);
+ }
+
+}
+
+package androidx.ink.rendering.android.view {
+
+ public final class ViewStrokeRenderer {
+ ctor public ViewStrokeRenderer(androidx.ink.rendering.android.canvas.CanvasStrokeRenderer canvasStrokeRenderer, android.view.View view);
+ method public inline void drawWithStrokes(android.graphics.Canvas canvas, kotlin.jvm.functions.Function1<? super androidx.ink.rendering.android.canvas.StrokeDrawScope,kotlin.Unit> block);
+ method public void drawWithStrokes(android.graphics.Canvas canvas, kotlin.jvm.functions.Function2<? super android.graphics.Canvas,? super androidx.ink.rendering.android.canvas.StrokeDrawScope,kotlin.Unit> block);
+ method @kotlin.PublishedApi internal androidx.ink.rendering.android.canvas.StrokeDrawScope obtainDrawScope(android.graphics.Canvas canvas);
+ method @kotlin.PublishedApi internal void recycleDrawScope(androidx.ink.rendering.android.canvas.StrokeDrawScope scope);
+ }
+
+}
+
diff --git a/ink/ink-rendering/build.gradle b/ink/ink-rendering/build.gradle
new file mode 100644
index 0000000..e7df440
--- /dev/null
+++ b/ink/ink-rendering/build.gradle
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+androidXMultiplatform {
+ android()
+
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+
+ androidMain {
+ dependencies {
+ implementation("androidx.collection:collection:1.4.3")
+ implementation(project(":core:core"))
+ implementation(project(":ink:ink-nativeloader"))
+ implementation(project(":ink:ink-geometry"))
+ implementation(project(":ink:ink-brush"))
+ implementation(project(":ink:ink-strokes"))
+ }
+ }
+
+ androidInstrumentedTest {
+ dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.espressoCore)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ implementation(project(":test:screenshot:screenshot"))
+ }
+ }
+ }
+}
+
+android {
+ namespace = "androidx.ink.rendering"
+ compileSdk = 35
+ sourceSets.androidTest.assets.srcDirs +=
+ project.rootDir.absolutePath + "/../../golden/ink/ink-rendering"
+}
+
+androidx {
+ name = "Ink Rendering"
+ type = LibraryType.PUBLISHED_LIBRARY
+ inceptionYear = "2024"
+ description = "Display beautiful strokes"
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/AndroidManifest.xml b/ink/ink-rendering/src/androidInstrumentedTest/AndroidManifest.xml
new file mode 100644
index 0000000..648a857
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2024 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="androidx.ink.rendering.android.canvas.CanvasStrokeRendererTestActivity"/>
+ <activity
+ android:name="androidx.ink.rendering.android.canvas.internal.CanvasMeshRendererScreenshotTestActivity"/>
+ <activity
+ android:name="androidx.ink.rendering.android.view.ViewStrokeRendererTestActivity"/>
+ </application>
+</manifest>
+
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt
new file mode 100644
index 0000000..14c0c57
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTest.kt
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas
+
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils
+import androidx.ink.brush.Brush
+import androidx.ink.brush.BrushBehavior
+import androidx.ink.brush.BrushCoat
+import androidx.ink.brush.BrushFamily
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.BrushPaint.BlendMode
+import androidx.ink.brush.BrushPaint.TextureOrigin
+import androidx.ink.brush.BrushPaint.TextureSizeUnit
+import androidx.ink.brush.BrushTip
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.InputToolType
+import androidx.ink.brush.StockBrushes
+import androidx.ink.geometry.Angle
+import androidx.ink.rendering.test.R
+import androidx.ink.strokes.ImmutableStrokeInputBatch
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.MutableStrokeInputBatch
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.captureToBitmap
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Emulator-based screenshot test of [CanvasStrokeRenderer] for Stroke and InProgressStroke. */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class CanvasStrokeRendererTest {
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule(CanvasStrokeRendererTestActivity::class.java)
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_DIRECTORY)
+
+ @Test
+ fun drawsSimpleStrokes() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair(
+ "Solid",
+ finishedInProgressStroke(
+ brush(color = TestColors.AVOCADO_GREEN),
+ INPUTS_ZIGZAG
+ ),
+ ),
+ Pair(
+ "Translucent",
+ finishedInProgressStroke(
+ brush(
+ BrushFamily(BrushTip(opacityMultiplier = 1.0F)),
+ TestColors.COBALT_BLUE.withAlpha(0.4),
+ ),
+ INPUTS_TWIST,
+ ),
+ ),
+ Pair(
+ "Tiled",
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSizeUnit = TextureSizeUnit.STROKE_COORDINATES,
+ textureSize = 10f
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ Pair(
+ "Multicoat",
+ finishedInProgressStroke(
+ brush(
+ BrushFamily(
+ listOf(
+ BrushCoat(
+ paint =
+ tiledBrushPaint(
+ textureSizeUnit =
+ TextureSizeUnit.STROKE_COORDINATES,
+ textureSize = 10f,
+ )
+ ),
+ BrushCoat(tip = BrushTip(scaleX = 0.5f, scaleY = 0.5f)),
+ )
+ ),
+ TestColors.RED,
+ ),
+ INPUTS_TWIST,
+ ),
+ ),
+ // TODO: b/330528190 - Add row for atlased textures
+ Pair(
+ """
+ Opacity &
+ HSL Shift
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ brush(
+ BrushFamily(
+ BrushTip(
+ behaviors =
+ listOf(
+ BrushBehavior(
+ source =
+ BrushBehavior.Source
+ .DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ target =
+ BrushBehavior.Target.OPACITY_MULTIPLIER,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 2f,
+ targetModifierRangeLowerBound = 1f,
+ targetModifierRangeUpperBound = 0.25f,
+ sourceOutOfRangeBehavior =
+ BrushBehavior.OutOfRange.MIRROR,
+ ),
+ BrushBehavior(
+ source =
+ BrushBehavior.Source
+ .DISTANCE_TRAVELED_IN_MULTIPLES_OF_BRUSH_SIZE,
+ target =
+ BrushBehavior.Target.HUE_OFFSET_IN_RADIANS,
+ sourceValueRangeLowerBound = 0f,
+ sourceValueRangeUpperBound = 3f,
+ targetModifierRangeLowerBound = 0f,
+ targetModifierRangeUpperBound =
+ Angle.FULL_TURN_RADIANS,
+ sourceOutOfRangeBehavior =
+ BrushBehavior.OutOfRange.REPEAT,
+ ),
+ )
+ )
+ ),
+ TestColors.AVOCADO_GREEN,
+ ),
+ INPUTS_TWIST,
+ ),
+ ),
+ // TODO: b/274461578 - Add row for winding textures
+ )
+ )
+ }
+ assertScreenshot("SimpleStrokes")
+ }
+
+ @Test
+ fun supportsTextureOrigins() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair(
+ "STROKE_SPACE_ORIGIN",
+ finishedInProgressStroke(
+ tiledBrush(
+ textureUri = CanvasStrokeRendererTestActivity.TEXTURE_URI_CIRCLE,
+ textureSize = 1f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ textureOrigin = TextureOrigin.STROKE_SPACE_ORIGIN,
+ textureOffsetX = 0.5f,
+ textureOffsetY = 0.5f,
+ brushSize = 25f,
+ ),
+ INPUTS_ZAGZIG,
+ ),
+ ),
+ Pair(
+ "FIRST_STROKE_INPUT",
+ finishedInProgressStroke(
+ tiledBrush(
+ textureUri = CanvasStrokeRendererTestActivity.TEXTURE_URI_CIRCLE,
+ textureSize = 1f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ textureOrigin = TextureOrigin.FIRST_STROKE_INPUT,
+ textureOffsetX = 0.5f,
+ textureOffsetY = 0.5f,
+ brushSize = 25f,
+ ),
+ INPUTS_ZAGZIG,
+ ),
+ ),
+ Pair(
+ "LAST_STROKE_INPUT",
+ finishedInProgressStroke(
+ tiledBrush(
+ textureUri = CanvasStrokeRendererTestActivity.TEXTURE_URI_CIRCLE,
+ textureSize = 1f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ textureOrigin = TextureOrigin.LAST_STROKE_INPUT,
+ textureOffsetX = 0.5f,
+ textureOffsetY = 0.5f,
+ brushSize = 25f,
+ ),
+ INPUTS_ZAGZIG,
+ ),
+ ),
+ )
+ )
+ }
+ assertScreenshot("TextureOrigins")
+ }
+
+ @Test
+ fun supportsTextureSizeUnits() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair(
+ """
+ textureSize=
+ BRUSH_SIZE*1
+ brushSize=15
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 1f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ brushSize = 15f,
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ Pair(
+ """
+ textureSize=
+ BRUSH_SIZE*1
+ brushSize=30
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 1f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ brushSize = 30f,
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ Pair(
+ """
+ textureSize=
+ BRUSH_SIZE/2
+ brushSize=30
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 0.5f,
+ textureSizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ brushSize = 30f,
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ // TODO: b/336835642 - add row for STROKE_SIZE
+ Pair(
+ """
+ textureSize=
+ STROKE_COORDS*5
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 5f,
+ textureSizeUnit = TextureSizeUnit.STROKE_COORDINATES
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ Pair(
+ """
+ textureSize=
+ STROKE_COORDS*10
+ """
+ .trimIndent(),
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 10f,
+ textureSizeUnit = TextureSizeUnit.STROKE_COORDINATES
+ ),
+ INPUTS_ZIGZAG,
+ ),
+ ),
+ )
+ )
+ }
+ assertScreenshot("TextureSizeUnits")
+ }
+
+ @Test
+ fun supportsBlendModesWithBrushColor() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair(
+ """
+ MODULATE
+ WHITE
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.MODULATE, TestColors.WHITE),
+ ),
+ Pair(
+ """
+ MODULATE
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.MODULATE, TestColors.RED.withAlpha(0.5)),
+ ),
+ Pair(
+ """
+ DST_IN
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.DST_IN, TestColors.RED.withAlpha(0.5)),
+ ),
+ Pair(
+ """
+ DST_OUT
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.DST_OUT, TestColors.RED.withAlpha(0.5)),
+ ),
+ Pair(
+ """
+ SRC_ATOP
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.SRC_ATOP, TestColors.RED.withAlpha(0.5)),
+ ),
+ Pair(
+ """
+ SRC_IN
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.SRC_IN, TestColors.RED.withAlpha(0.5)),
+ ),
+ Pair(
+ """
+ SRC
+ RED.withAlpha(0.5)
+ """
+ .trimIndent(),
+ colorBlendedStroke(BlendMode.SRC, TestColors.RED.withAlpha(0.5)),
+ ),
+ )
+ )
+ }
+ assertScreenshot("BlendWithBrushColor")
+ }
+
+ @Test
+ fun supportsBlendModesWithTwoTextures() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair("SRC", textureBlendedStroke(BlendMode.SRC)),
+ Pair("DST", textureBlendedStroke(BlendMode.DST)),
+ Pair("SRC_OVER", textureBlendedStroke(BlendMode.SRC_OVER)),
+ Pair("DST_OVER", textureBlendedStroke(BlendMode.DST_OVER)),
+ Pair("SRC_OUT", textureBlendedStroke(BlendMode.SRC_OUT)),
+ Pair("DST_ATOP", textureBlendedStroke(BlendMode.DST_ATOP)),
+ Pair("XOR", textureBlendedStroke(BlendMode.XOR)),
+ )
+ )
+ }
+ assertScreenshot("BlendTwoTextures")
+ }
+
+ @Test
+ fun supportsTextureOffset() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ activity.addStrokeRows(
+ listOf(
+ Pair(
+ """
+ offsetX=0.0
+ offsetY=0.0
+ """
+ .trimIndent(),
+ textureTransformStroke(offsetX = 0.0f, offsetY = 0.0f),
+ ),
+ Pair(
+ """
+ offsetX=0.25
+ offsetY=0.0
+ """
+ .trimIndent(),
+ textureTransformStroke(offsetX = 0.25f, offsetY = 0.0f),
+ ),
+ Pair(
+ """
+ offsetX=0.5
+ offsetY=0.0
+ """
+ .trimIndent(),
+ textureTransformStroke(offsetX = 0.5f, offsetY = 0.0f),
+ ),
+ Pair(
+ """
+ offsetX=0.75
+ offsetY=0.0
+ """
+ .trimIndent(),
+ textureTransformStroke(offsetX = 0.75f, offsetY = 0.0f),
+ ),
+ Pair(
+ """
+ offsetX=0.25
+ offsetY=0.25
+ """
+ .trimIndent(),
+ textureTransformStroke(offsetX = 0.25f, offsetY = 0.25f),
+ ),
+ )
+ )
+ }
+ assertScreenshot("TextureOffset")
+ }
+
+ private fun assertScreenshot(filename: String) {
+ onView(withId(R.id.stroke_grid))
+ .perform(
+ captureToBitmap() {
+ it.assertAgainstGolden(screenshotRule, "${this::class.simpleName}_$filename")
+ }
+ )
+ }
+
+ private companion object {
+ val NO_PREDICTION = ImmutableStrokeInputBatch.EMPTY
+
+ val INPUTS_ZIGZAG =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 0F, elapsedTimeMillis = 100)
+ .addOrThrow(InputToolType.UNKNOWN, x = 40F, y = 40F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 70F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 30F, y = 100F, elapsedTimeMillis = 250)
+ .asImmutable()
+
+ val INPUTS_ZAGZIG =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 30F, y = 0F, elapsedTimeMillis = 100)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 40F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 40F, y = 70F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 5F, y = 90F, elapsedTimeMillis = 250)
+ .asImmutable()
+
+ val INPUTS_TWIST =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 0F, elapsedTimeMillis = 100)
+ .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 100F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 100F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 0F, elapsedTimeMillis = 250)
+ .asImmutable()
+
+ fun brush(
+ family: BrushFamily = StockBrushes.markerLatest,
+ @ColorInt color: Int = TestColors.BLACK,
+ size: Float = 15F,
+ epsilon: Float = 0.1F,
+ ) = Brush.createWithColorIntArgb(family, color, size, epsilon)
+
+ fun tiledBrush(
+ textureUri: String = CanvasStrokeRendererTestActivity.TEXTURE_URI_CHECKERBOARD,
+ textureSizeUnit: TextureSizeUnit,
+ textureSize: Float,
+ textureOrigin: TextureOrigin = TextureOrigin.STROKE_SPACE_ORIGIN,
+ textureOffsetX: Float = 0f,
+ textureOffsetY: Float = 0f,
+ @ColorInt brushColor: Int = TestColors.BLACK,
+ brushSize: Float = 15f,
+ ): Brush {
+ val paint =
+ tiledBrushPaint(
+ textureUri = textureUri,
+ textureSizeUnit = textureSizeUnit,
+ textureSize = textureSize,
+ textureOrigin = textureOrigin,
+ textureOffsetX = textureOffsetX,
+ textureOffsetY = textureOffsetY,
+ )
+ return brush(BrushFamily(paint = paint), brushColor, brushSize)
+ }
+
+ fun tiledBrushPaint(
+ textureUri: String = CanvasStrokeRendererTestActivity.TEXTURE_URI_CHECKERBOARD,
+ textureSizeUnit: TextureSizeUnit,
+ textureSize: Float,
+ textureOrigin: TextureOrigin = TextureOrigin.STROKE_SPACE_ORIGIN,
+ textureOffsetX: Float = 0f,
+ textureOffsetY: Float = 0f,
+ ): BrushPaint {
+ val textureLayer =
+ BrushPaint.TextureLayer(
+ colorTextureUri = textureUri,
+ sizeX = textureSize,
+ sizeY = textureSize,
+ offsetX = textureOffsetX,
+ offsetY = textureOffsetY,
+ sizeUnit = textureSizeUnit,
+ origin = textureOrigin,
+ )
+ return BrushPaint(listOf(textureLayer))
+ }
+
+ fun textureTransformStroke(offsetX: Float, offsetY: Float): InProgressStroke =
+ finishedInProgressStroke(
+ tiledBrush(
+ textureSize = 30f,
+ textureOffsetX = offsetX,
+ textureOffsetY = offsetY,
+ textureSizeUnit = TextureSizeUnit.STROKE_COORDINATES,
+ brushSize = 30f,
+ ),
+ INPUTS_ZIGZAG,
+ )
+
+ fun finishedInProgressStroke(brush: Brush, inputs: ImmutableStrokeInputBatch) =
+ InProgressStroke().apply {
+ start(brush)
+ enqueueInputs(inputs, NO_PREDICTION).getOrThrow()
+ finishInput()
+ updateShape(inputs.getDurationMillis()).getOrThrow()
+ }
+
+ fun colorBlendedStroke(blendMode: BlendMode, @ColorInt color: Int): InProgressStroke {
+ val textureLayer =
+ BrushPaint.TextureLayer(
+ CanvasStrokeRendererTestActivity.TEXTURE_URI_POOP_EMOJI,
+ sizeX = 1f,
+ sizeY = 1f,
+ sizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ blendMode = blendMode,
+ )
+ val paint = BrushPaint(listOf(textureLayer))
+ val brush = brush(BrushFamily(paint = paint), color, size = 30f)
+ return finishedInProgressStroke(brush, INPUTS_TWIST)
+ }
+
+ fun textureBlendedStroke(blendMode: BlendMode): InProgressStroke {
+ val textureLayer1 =
+ BrushPaint.TextureLayer(
+ CanvasStrokeRendererTestActivity.TEXTURE_URI_AIRPLANE_EMOJI,
+ sizeX = 1f,
+ sizeY = 1f,
+ sizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ blendMode = blendMode,
+ )
+ val textureLayer2 =
+ BrushPaint.TextureLayer(
+ CanvasStrokeRendererTestActivity.TEXTURE_URI_POOP_EMOJI,
+ sizeX = 1f,
+ sizeY = 1f,
+ sizeUnit = TextureSizeUnit.BRUSH_SIZE,
+ )
+ val paint = BrushPaint(listOf(textureLayer1, textureLayer2))
+ val brush = brush(BrushFamily(paint = paint), color = TestColors.WHITE, size = 40f)
+ return finishedInProgressStroke(brush, INPUTS_ZIGZAG)
+ }
+
+ @ColorInt
+ fun Int.withAlpha(alpha: Double): Int {
+ return ColorUtils.setAlphaComponent(this, (alpha * 255).toInt())
+ }
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTestActivity.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTestActivity.kt
new file mode 100644
index 0000000..be6d493
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRendererTestActivity.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.os.Build
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.widget.GridLayout
+import android.widget.TextView
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.geometry.AffineTransform
+import androidx.ink.geometry.BoxAccumulator
+import androidx.ink.geometry.ImmutableAffineTransform
+import androidx.ink.geometry.ImmutableBox
+import androidx.ink.geometry.ImmutableVec
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.rendering.test.R
+import androidx.ink.strokes.InProgressStroke
+
+/** An [Activity] to support [CanvasStrokeRendererTest]. */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+class CanvasStrokeRendererTestActivity : Activity() {
+ private val textureStore = TextureBitmapStore { uri ->
+ when (uri) {
+ TEXTURE_URI_AIRPLANE_EMOJI -> R.drawable.airplane_emoji
+ TEXTURE_URI_CHECKERBOARD -> R.drawable.checkerboard_black_and_transparent
+ TEXTURE_URI_CIRCLE -> R.drawable.circle
+ TEXTURE_URI_POOP_EMOJI -> R.drawable.poop_emoji
+ else -> null
+ }?.let { BitmapFactory.decodeResource(resources, it) }
+ }
+ private val meshRenderer =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ CanvasStrokeRenderer.create(textureStore)
+ } else {
+ null
+ }
+ private val pathRenderer = CanvasStrokeRenderer.create(textureStore, forcePathRendering = true)
+ private val defaultRenderer = CanvasStrokeRenderer.create()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.canvas_stroke_renderer_test)
+ }
+
+ fun addStrokeRows(labelsAndStrokes: List<Pair<String, InProgressStroke>>) {
+ val grid = findViewById<GridLayout>(R.id.stroke_grid)
+ for ((label, stroke) in labelsAndStrokes) {
+ val row = grid.rowCount
+ grid.rowCount = row + 1
+ grid.addView(
+ TextView(this).apply {
+ text = label
+ setTextSize(10.0F)
+ },
+ gridLayoutParams(row, col = 0, Gravity.CENTER_VERTICAL),
+ )
+ if (meshRenderer != null) {
+ grid.addView(
+ StrokeView(this, meshRenderer, stroke),
+ gridLayoutParams(row, col = 1, Gravity.FILL),
+ )
+ } else {
+ grid.addView(
+ TextView(this).apply { text = "N/A" },
+ gridLayoutParams(row, col = 1, Gravity.CENTER),
+ )
+ }
+ grid.addView(
+ StrokeView(this, pathRenderer, stroke),
+ gridLayoutParams(row, col = 2, Gravity.FILL),
+ )
+ grid.addView(
+ StrokeView(this, defaultRenderer, stroke),
+ gridLayoutParams(row, col = 3, Gravity.FILL),
+ )
+ }
+ }
+
+ private fun gridLayoutParams(row: Int, col: Int, gravity: Int): GridLayout.LayoutParams {
+ val params =
+ GridLayout.LayoutParams(
+ GridLayout.spec(row, /* weight= */ 1f),
+ GridLayout.spec(col, /* weight= */ 0f),
+ )
+ if (gravity == Gravity.FILL) {
+ params.width = 0
+ params.height = 0
+ }
+ params.setGravity(gravity)
+ return params
+ }
+
+ private class StrokeView(
+ context: Context,
+ val renderer: CanvasStrokeRenderer,
+ val inProgressStroke: InProgressStroke,
+ ) : View(context) {
+
+ val bounds = BoxAccumulator().also { inProgressStroke.populateMeshBounds(0, it) }.box!!
+ val finishedStrokeTranslateX = 20 + (bounds.xMax - bounds.xMin)
+ val scaledStrokeTranslateY = 10 + (bounds.yMax - bounds.yMin)
+ val scaleValueY = 0.5f
+ val totalGridBounds =
+ ImmutableBox.fromTwoPoints(
+ ImmutableVec(bounds.xMin, bounds.yMin),
+ ImmutableVec(
+ finishedStrokeTranslateX + (bounds.xMax - bounds.xMin),
+ scaledStrokeTranslateY + scaleValueY * (bounds.yMax - bounds.yMin),
+ ),
+ )
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ canvas.translate(
+ (width - (totalGridBounds.xMax - totalGridBounds.xMin)) / 2,
+ (height - (totalGridBounds.yMax - totalGridBounds.yMin)) / 2,
+ )
+ renderer.draw(canvas, inProgressStroke, AffineTransform.IDENTITY)
+
+ // Draw Stroke next to InProgressStroke, with a small gap between them.
+ val stroke = inProgressStroke.toImmutable()
+ renderer.draw(
+ canvas,
+ stroke,
+ ImmutableAffineTransform.translate(ImmutableVec(finishedStrokeTranslateX, 0f)),
+ )
+
+ // Draw the InProgressStroke and Stroke again in a second row with a non-trivial
+ // transform
+ // and using android.graphics.Matrix instead of AffineTransform.
+ renderer.draw(
+ canvas,
+ inProgressStroke,
+ Matrix().apply {
+ setSkew(0.5f, 0f)
+ postScale(1f, scaleValueY)
+ postTranslate(0f, scaledStrokeTranslateY)
+ },
+ )
+ renderer.draw(
+ canvas,
+ stroke,
+ Matrix().apply {
+ setSkew(0.5f, 0f)
+ postScale(1f, scaleValueY)
+ postTranslate(finishedStrokeTranslateX, scaledStrokeTranslateY)
+ },
+ )
+ }
+ }
+
+ companion object {
+ const val TEXTURE_URI_AIRPLANE_EMOJI = "ink://ink/texture:airplane-emoji"
+ const val TEXTURE_URI_CHECKERBOARD = "ink://ink/texture:checkerboard-overlay-pen"
+ const val TEXTURE_URI_CIRCLE = "ink://ink/texture:circle"
+ const val TEXTURE_URI_POOP_EMOJI = "ink://ink/texture:poop-emoji"
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestColors.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestColors.kt
new file mode 100644
index 0000000..fd7c886
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestColors.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas
+
+import androidx.annotation.ColorInt
+
+/**
+ * [ColorInt] constants for use in tests.
+ *
+ * Channels are in ARGB order, per the definition of [ColorInt]. Use the helper functions defined
+ * below to convert to other channel orders.
+ *
+ * These colors have different values for all RGB channels and at least one channel with a value
+ * strictly between 0.0 (0x00) and 1.0 (0xff). These properties help check for channel order
+ * scrambling (for example, incorrect mixing of RGB and BGR formats) and gamma correction errors.
+ */
+object TestColors {
+ /**
+ * Near-white color for backgrounds and elements without textures. For textured elements that
+ * need a 100% white base color, use [WHITE_FOR_TEXTURE].
+ */
+ @ColorInt const val WHITE = 0xfff5f8ff.toInt()
+ // Gray and black are not pure desaturated tones, because we need different values in the
+ // different channels.
+ @ColorInt const val LIGHT_GRAY = 0xffbaccc0.toInt()
+ @ColorInt const val DARK_GRAY = 0xff4d4239.toInt()
+ @ColorInt const val BLACK = 0xff290e1c.toInt()
+ @ColorInt const val RED = 0xfff7251e.toInt()
+ @ColorInt const val ORANGE = 0xffff6e40.toInt()
+ @ColorInt const val LIGHT_ORANGE = 0xffffccbc.toInt()
+ @ColorInt const val YELLOW = 0xfff7f12d.toInt()
+ @ColorInt const val AVOCADO_GREEN = 0xff558b2f.toInt()
+ @ColorInt const val GREEN = 0xff00c853.toInt()
+ @ColorInt const val CYAN = 0xff2be3f0.toInt()
+ @ColorInt const val LIGHT_BLUE = 0xff4fb5e8.toInt()
+ @ColorInt const val BLUE = 0xff304ffe.toInt()
+ @ColorInt const val COBALT_BLUE = 0xff01579b.toInt()
+ @ColorInt const val DEEP_PURPLE = 0xff8e24aa.toInt()
+ @ColorInt const val MAGENTA = 0xffed26e0.toInt()
+ @ColorInt const val HOT_PINK = 0xffff4081.toInt()
+
+ /** White base color for elements that have a texture applied. */
+ @ColorInt const val WHITE_FOR_TEXTURE = 0xffffffff.toInt()
+
+ @ColorInt const val TRANSLUCENT_ORANGE = 0x80ffbf00.toInt()
+
+ @JvmStatic
+ fun colorIntToRgba(@ColorInt argb: Int): Int = (argb shl 8) or ((argb shr 24) and 0xff)
+}
diff --git a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestConstants.kt
similarity index 67%
copy from biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
copy to ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestConstants.kt
index 233dbe6..b481afb 100644
--- a/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/AuthPromptFailureException.kt
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/TestConstants.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,6 @@
* limitations under the License.
*/
-package androidx.biometric.auth
+package androidx.ink.rendering.android.canvas
-/**
- * Thrown when an authentication attempt by the user has been rejected, e.g., the user's biometrics
- * were not recognized.
- */
-public class AuthPromptFailureException : Exception()
+internal const val SCREENSHOT_GOLDEN_DIRECTORY = "ink/ink-rendering"
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCacheTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCacheTest.kt
new file mode 100644
index 0000000..d646160
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCacheTest.kt
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.createBitmap
+import android.graphics.BitmapShader
+import android.graphics.Color
+import android.graphics.ComposeShader
+import android.graphics.Matrix
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.strokes.StrokeInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BrushPaintCacheTest {
+
+ private fun nestedArrayToMatrix(values: Array<Array<Float>>) =
+ Matrix().apply { setValues(values.flatten().toFloatArray()) }
+
+ @Test
+ fun obtain_positionOnlyWithTexture() {
+ var uriLoaded: String? = null
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ uriLoaded = it
+ createBitmap(10, 20, Bitmap.Config.ARGB_8888)
+ }
+ )
+ val fakeTextureUri = "ink://ink/texture:test-texture-one"
+ val brushPaint =
+ BrushPaint(listOf(BrushPaint.TextureLayer(fakeTextureUri, sizeX = 30F, sizeY = 40F)))
+ val brushSize = 10f
+ val internalToStrokeTransform = Matrix().apply { preTranslate(50F, 60F) }
+
+ val paint =
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+
+ assertThat(uriLoaded).isEqualTo(fakeTextureUri)
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isInstanceOf(BitmapShader::class.java)
+ val expectedLocalMatrix =
+ nestedArrayToMatrix(
+ arrayOf(arrayOf(3F, 0F, -50F), arrayOf(0F, 2F, -60F), arrayOf(0F, 0F, 1.0F))
+ )
+ with(Matrix()) {
+ assertThat(paint.shader.getLocalMatrix(this)).isTrue()
+ assertThat(this).isEqualTo(expectedLocalMatrix)
+ }
+
+ val newInternalToStrokeTransform = Matrix().apply { preTranslate(-50F, -60F) }
+ val expectedUpdatedMatrix =
+ nestedArrayToMatrix(
+ arrayOf(arrayOf(3F, 0F, 50F), arrayOf(0F, 2F, 60F), arrayOf(0F, 0F, 1.0F))
+ )
+ assertThat(
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ newInternalToStrokeTransform,
+ )
+ )
+ .isSameInstanceAs(paint)
+ with(Matrix()) {
+ assertThat(paint.shader.getLocalMatrix(this)).isTrue()
+ assertThat(expectedUpdatedMatrix).isNotEqualTo(expectedLocalMatrix)
+ assertThat(this).isEqualTo(expectedUpdatedMatrix)
+ }
+
+ assertThat(
+ cache.obtain(
+ brushPaint,
+ Color.BLUE,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ newInternalToStrokeTransform,
+ )
+ )
+ .isSameInstanceAs(paint)
+ assertThat(paint.color).isEqualTo(Color.BLUE)
+ }
+
+ @Test
+ fun obtain_forBrushPaintWithSizeUnitBrushSize() {
+ val cache =
+ BrushPaintCache(TextureBitmapStore { createBitmap(1, 1, Bitmap.Config.ARGB_8888) })
+ val textureUri = "ink://ink/texture:test-texture-one"
+ val brushPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(
+ textureUri,
+ sizeX = 2f,
+ sizeY = 3f,
+ sizeUnit = BrushPaint.TextureSizeUnit.BRUSH_SIZE,
+ )
+ )
+ )
+ val internalToStrokeTransform = Matrix().apply { preTranslate(7f, 5f) }
+
+ val paint =
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize = 10f,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+
+ val expectedLocalMatrix =
+ nestedArrayToMatrix(
+ arrayOf(arrayOf(20F, 0F, -7F), arrayOf(0F, 30F, -5F), arrayOf(0F, 0F, 1F))
+ )
+ with(Matrix()) {
+ assertThat(paint.shader.getLocalMatrix(this)).isTrue()
+ assertThat(this).isEqualTo(expectedLocalMatrix)
+ }
+
+ val expectedUpdatedMatrix =
+ nestedArrayToMatrix(
+ arrayOf(arrayOf(40F, 0F, -7F), arrayOf(0F, 60F, -5F), arrayOf(0F, 0F, 1F))
+ )
+ assertThat(
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize = 20f,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+ )
+ .isSameInstanceAs(paint)
+ with(Matrix()) {
+ assertThat(paint.shader.getLocalMatrix(this)).isTrue()
+ assertThat(expectedUpdatedMatrix).isNotEqualTo(expectedLocalMatrix)
+ assertThat(this).isEqualTo(expectedUpdatedMatrix)
+ }
+ }
+
+ @Test
+ fun obtain_multipleTextureLayers() {
+ val urisLoaded: MutableList<String> = mutableListOf()
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ urisLoaded.add(it)
+ createBitmap(/* width= */ 10, /* height= */ 20, Bitmap.Config.ARGB_8888)
+ }
+ )
+ val fakeTextureUri1 = "ink://ink/texture:test-texture-one"
+ val fakeTextureUri2 = "ink://ink/texture:test-texture-two"
+ val brushPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(fakeTextureUri1, sizeX = 30F, sizeY = 40F),
+ BrushPaint.TextureLayer(fakeTextureUri2, sizeX = 30F, sizeY = 40F),
+ )
+ )
+
+ val paint =
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize = 1f,
+ StrokeInput(),
+ StrokeInput(),
+ Matrix()
+ )
+
+ assertThat(urisLoaded).containsExactly(fakeTextureUri1, fakeTextureUri2).inOrder()
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isInstanceOf(ComposeShader::class.java)
+ // Can't really assert in more detail because ComposeShader's fields are not readable.
+ }
+
+ @Test
+ fun obtain_textureLayersThatDoNotLoadAreIgnored() {
+ val urisLoaded: MutableList<String> = mutableListOf()
+ val fakeBrokenTextureUri1 = "//fake/texture:broken:1"
+ val fakeWorkingTextureUri = "ink://ink/texture:test-texture-one"
+ val fakeBrokenTextureUri2 = "//fake/texture:broken:2"
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ urisLoaded.add(it)
+ if (it == fakeWorkingTextureUri) {
+ createBitmap(/* width= */ 10, /* height= */ 20, Bitmap.Config.ARGB_8888)
+ } else {
+ null
+ }
+ }
+ )
+ val brushPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(fakeBrokenTextureUri1, sizeX = 30F, sizeY = 40F),
+ BrushPaint.TextureLayer(fakeWorkingTextureUri, sizeX = 30F, sizeY = 40F),
+ BrushPaint.TextureLayer(fakeBrokenTextureUri2, sizeX = 30F, sizeY = 40F),
+ )
+ )
+
+ val paint =
+ cache.obtain(brushPaint, Color.RED, brushSize = 1f, StrokeInput(), StrokeInput())
+
+ assertThat(urisLoaded)
+ .containsExactly(fakeBrokenTextureUri1, fakeWorkingTextureUri, fakeBrokenTextureUri2)
+ .inOrder()
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isInstanceOf(BitmapShader::class.java)
+ }
+
+ @Test
+ fun obtain_textureLoadingDisabled() {
+ var uriLoaded: String? = null
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ uriLoaded = it
+ null
+ }
+ )
+ val fakeTextureUri = "ink://ink/texture:test-texture-one"
+ val brushPaint =
+ BrushPaint(listOf(BrushPaint.TextureLayer(fakeTextureUri, sizeX = 30F, sizeY = 40F)))
+ val brushSize = 5f
+ val internalToStrokeTransform = Matrix().apply { preTranslate(50F, 60F) }
+
+ val paint =
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+
+ assertThat(uriLoaded).isEqualTo(fakeTextureUri)
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isNull()
+
+ assertThat(
+ cache.obtain(
+ brushPaint,
+ Color.BLUE,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+ )
+ .isSameInstanceAs(paint)
+ assertThat(paint.color).isEqualTo(Color.BLUE)
+ }
+
+ @Test
+ fun obtain_textureLoadingDisabledMultipleLayers() {
+ val urisLoaded: MutableList<String> = mutableListOf()
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ urisLoaded.add(it)
+ null
+ }
+ )
+ val textureLayerWidth = 30F
+ val textureLayerHeight = 40F
+ val fakeTextureUri1 = "ink://ink/texture:test-one"
+ val fakeTextureUri2 = "ink://ink/texture:test-two"
+ val brushPaint =
+ BrushPaint(
+ listOf(
+ BrushPaint.TextureLayer(fakeTextureUri1, textureLayerWidth, textureLayerHeight),
+ BrushPaint.TextureLayer(fakeTextureUri2, textureLayerWidth, textureLayerHeight),
+ )
+ )
+ val internalToStrokeTransform =
+ Matrix().apply { preTranslate(/* dx= */ 50F, /* dy= */ 60F) }
+
+ val paint =
+ cache.obtain(
+ brushPaint,
+ Color.RED,
+ brushSize = 1f,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+
+ assertThat(urisLoaded).containsExactly(fakeTextureUri1, fakeTextureUri2).inOrder()
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isNull()
+ }
+
+ @Test
+ fun obtain_noTexture() {
+ val cache = BrushPaintCache(TextureBitmapStore { null })
+ val brushSize = 15f
+ val internalToStrokeTransform = Matrix().apply { preTranslate(50F, 60F) }
+
+ val paint =
+ cache.obtain(
+ BrushPaint(),
+ Color.RED,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isNull()
+
+ // BrushPaint() is a different instance, but is equal.
+ assertThat(
+ cache.obtain(
+ BrushPaint(),
+ Color.BLUE,
+ brushSize,
+ StrokeInput(),
+ StrokeInput(),
+ internalToStrokeTransform,
+ )
+ )
+ .isSameInstanceAs(paint)
+ assertThat(paint.color).isEqualTo(Color.BLUE)
+ }
+
+ @Test
+ fun obtain_defaultInternalToStrokeTransform() {
+ var uriLoaded: String? = null
+ val cache =
+ BrushPaintCache(
+ TextureBitmapStore {
+ uriLoaded = it
+ createBitmap(10, 20, Bitmap.Config.ARGB_8888)
+ }
+ )
+ val fakeTextureUri = "ink://ink/texture:test-texture-one"
+ val brushPaint =
+ BrushPaint(
+ // Same size as the Bitmap.
+ listOf(BrushPaint.TextureLayer(fakeTextureUri, sizeX = 10F, sizeY = 20F))
+ )
+
+ val paint =
+ cache.obtain(brushPaint, Color.RED, brushSize = 1f, StrokeInput(), StrokeInput())
+
+ assertThat(uriLoaded).isEqualTo(fakeTextureUri)
+ assertThat(paint.color).isEqualTo(Color.RED)
+ assertThat(paint.shader).isInstanceOf(BitmapShader::class.java)
+ Matrix().let {
+ // Set the matrix to garbage data to make sure it gets overwritten.
+ it.preScale(55555F, 7777777F)
+
+ // getLocalMatrix indicates identity either by returning false or overwriting the result
+ // to
+ // the identity, but it has slightly different behavior on different API versions. The
+ // code
+ // under test doesn't use getLocalMatrix, we're just confirming that our call to
+ // setLocalMatrix matches what we expect.
+ val result = paint.shader.getLocalMatrix(it)
+ // Don't check it.isIdentity, that seems to be incorrect on earlier API levels.
+ assertThat(!result || it == Matrix()).isTrue()
+ }
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererRobolectricTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererRobolectricTest.kt
new file mode 100644
index 0000000..07d6c58
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererRobolectricTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.os.Build
+import androidx.ink.brush.Brush
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.InputToolType
+import androidx.ink.brush.StockBrushes
+import androidx.ink.strokes.ImmutableStrokeInputBatch
+import androidx.ink.strokes.MutableStrokeInputBatch
+import androidx.ink.strokes.Stroke
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Non-emulator logic test of [CanvasMeshRenderer].
+ *
+ * Code in this test cannot create an [android.graphics.MeshSpecification] or
+ * [android.graphics.Mesh], but it allows a limited subset of tests to run much more quickly.
+ *
+ * Note that in AndroidX, this test runs on the emulator rather than Robolectric, so it doesn't have
+ * a speed benefit.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class CanvasMeshRendererRobolectricTest {
+ private val brush = Brush(family = StockBrushes.markerLatest, size = 10f, epsilon = 0.1f)
+
+ private val stroke =
+ Stroke(
+ brush = brush,
+ inputs =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 10F, y = 10F, elapsedTimeMillis = 100)
+ .asImmutable(),
+ )
+
+ @OptIn(ExperimentalInkCustomBrushApi::class) private val meshRenderer = CanvasMeshRenderer()
+
+ @Test
+ fun canDraw_withRenderableMesh_returnsTrue() {
+ assertThat(meshRenderer.canDraw(stroke)).isTrue()
+ }
+
+ @Test
+ fun canDraw_withEmptyStroke_returnsTrue() {
+ val emptyStroke = Stroke(brush, ImmutableStrokeInputBatch.EMPTY)
+
+ assertThat(meshRenderer.canDraw(emptyStroke)).isTrue()
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTest.kt
new file mode 100644
index 0000000..07058a0
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.os.Build
+import androidx.ink.rendering.android.canvas.SCREENSHOT_GOLDEN_DIRECTORY
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.captureToBitmap
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Emulator-based screenshot test of [CanvasMeshRenderer]. */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class CanvasMeshRendererScreenshotTest {
+
+ @get:Rule
+ val activityScenarioRule =
+ ActivityScenarioRule(CanvasMeshRendererScreenshotTestActivity::class.java)
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_DIRECTORY)
+
+ @Test
+ fun onView_showsSimpleStroke() {
+ assertScreenshot("app_steady_state")
+ }
+
+ private fun assertScreenshot(filename: String) {
+ onView(isRoot())
+ .perform(
+ captureToBitmap() {
+ it.assertAgainstGolden(screenshotRule, "${this::class.simpleName}_$filename")
+ }
+ )
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt
new file mode 100644
index 0000000..7f68973
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererScreenshotTestActivity.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.ink.brush.Brush
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.InputToolType
+import androidx.ink.brush.StockBrushes
+import androidx.ink.strokes.ImmutableStrokeInputBatch
+import androidx.ink.strokes.MutableStrokeInputBatch
+import androidx.ink.strokes.Stroke
+
+/**
+ * An [Activity] to support [CanvasStrokeUnifiedRendererLegacyTest] by rendering a simple stroke.
+ */
+@SuppressLint("UseSdkSuppress") // SdkSuppress is on the test class.
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CanvasMeshRendererScreenshotTestActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(StrokeView(this))
+ }
+
+ private inner class StrokeView(context: Context) : View(context) {
+
+ private val inputs =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 0F, elapsedTimeMillis = 100)
+ .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 100F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 100F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 80F, y = 0F, elapsedTimeMillis = 250)
+ .asImmutable()
+
+ // Pink twist stroke.
+ private val brush =
+ Brush.createWithColorIntArgb(
+ family = StockBrushes.markerLatest,
+ colorIntArgb = 0x80CC1A99.toInt(),
+ size = 10F,
+ epsilon = 0.1F,
+ )
+ private val stroke = Stroke(brush, inputs)
+ private val transform = Matrix.IDENTITY_MATRIX
+
+ // Green twist stroke, rotated and scaled up.
+ private val brush2 = brush.copyWithColorIntArgb(colorIntArgb = 0xCC33E666.toInt())
+ private val stroke2 = stroke.copy(brush2)
+ private val transform2 =
+ Matrix().apply {
+ postRotate(/* degrees= */ 30F)
+ postScale(7F, 7F)
+ }
+
+ // Stroke with no inputs, and therefore an empty [ModeledShape].
+ private val emptyStroke = Stroke(brush, ImmutableStrokeInputBatch.EMPTY)
+
+ @OptIn(ExperimentalInkCustomBrushApi::class) private val renderer = CanvasMeshRenderer()
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ val xBetweenStrokes = 115F
+ val y = 100F
+ canvas.translate(20F, y)
+
+ // The empty stroke should of course not be visible, but the [draw] call should succeed.
+ renderer.draw(canvas, emptyStroke, Matrix.IDENTITY_MATRIX)
+ // Expected result: pink stroke on left, large green rotated stroke on right.
+ renderer.draw(canvas, stroke, transform)
+ canvas.translate(xBetweenStrokes, 0F)
+ renderer.draw(canvas, stroke2, transform2)
+ }
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererTest.kt
new file mode 100644
index 0000000..4e89280
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRendererTest.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.Matrix
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.core.os.BuildCompat
+import androidx.ink.brush.Brush
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.InputToolType
+import androidx.ink.brush.StockBrushes
+import androidx.ink.brush.color.Color
+import androidx.ink.brush.color.toArgb
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.MutableStrokeInputBatch
+import androidx.ink.strokes.Stroke
+import androidx.ink.strokes.testing.buildStrokeInputBatchFromPoints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Emulator-based logic test of [CanvasMeshRenderer].
+ *
+ * TODO(b/293163827) Move this to [CanvasMeshRendererRobolectricTest] once a shadow exists for
+ * [android.graphics.MeshSpecification].
+ */
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CanvasMeshRendererTest {
+
+ private val brush =
+ Brush.createWithColorIntArgb(
+ family = StockBrushes.markerLatest,
+ colorIntArgb = Color.Black.toArgb(),
+ size = 10F,
+ epsilon = 0.1F,
+ )
+
+ private val stroke =
+ Stroke(
+ brush = brush,
+ inputs =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 10F, y = 10F, elapsedTimeMillis = 100)
+ .asImmutable(),
+ )
+
+ private val clock = FakeClock()
+
+ @OptIn(ExperimentalInkCustomBrushApi::class)
+ private val meshRenderer = CanvasMeshRenderer(getDurationTimeMillis = clock::currentTimeMillis)
+
+ @Test
+ fun obtainShaderMetadata_whenCalledTwiceWithSamePackedInstance_returnsCachedValue() {
+ assertThat(stroke.shape.getRenderGroupCount()).isEqualTo(1)
+ val meshFormat = stroke.shape.renderGroupFormat(0)
+
+ assertThat(meshRenderer.obtainShaderMetadata(meshFormat, isPacked = true))
+ .isSameInstanceAs(meshRenderer.obtainShaderMetadata(meshFormat, isPacked = true))
+ }
+
+ @Test
+ fun obtainShaderMetadata_whenCalledTwiceWithEquivalentPackedFormat_returnsCachedValue() {
+ val anotherStroke =
+ Stroke(
+ brush = brush,
+ inputs =
+ MutableStrokeInputBatch()
+ .addOrThrow(
+ InputToolType.UNKNOWN,
+ x = 99F,
+ y = 99F,
+ elapsedTimeMillis = 100
+ )
+ .asImmutable(),
+ )
+
+ assertThat(stroke.shape.getRenderGroupCount()).isEqualTo(1)
+ val strokeFormat = stroke.shape.renderGroupFormat(0)
+ assertThat(anotherStroke.shape.getRenderGroupCount()).isEqualTo(1)
+ val anotherStrokeFormat = anotherStroke.shape.renderGroupFormat(0)
+
+ assertThat(meshRenderer.obtainShaderMetadata(anotherStrokeFormat, isPacked = true))
+ .isSameInstanceAs(meshRenderer.obtainShaderMetadata(strokeFormat, isPacked = true))
+ }
+
+ @Test
+ fun createAndroidMesh_fromInProgressStroke_returnsMesh() {
+ val inProgressStroke =
+ InProgressStroke().apply {
+ start(
+ Brush.createWithColorIntArgb(StockBrushes.markerLatest, 0x44112233, 10f, 0.25f)
+ )
+ assertThat(
+ enqueueInputs(
+ buildStrokeInputBatchFromPoints(
+ floatArrayOf(10f, 20f, 100f, 120f),
+ startTime = 0L
+ ),
+ MutableStrokeInputBatch(),
+ )
+ .isSuccess
+ )
+ .isTrue()
+ assertThat(updateShape(3L).isSuccess).isTrue()
+ }
+ assertThat(meshRenderer.createAndroidMesh(inProgressStroke, coatIndex = 0, meshIndex = 0))
+ .isNotNull()
+ }
+
+ @Test
+ fun obtainShaderMetadata_whenCalledTwiceWithSameUnpackedInstance_returnsCachedValue() {
+ val inProgressStroke = InProgressStroke()
+ inProgressStroke.start(brush)
+ assertThat(inProgressStroke.getBrushCoatCount()).isEqualTo(1)
+ assertThat(inProgressStroke.getMeshPartitionCount(0)).isEqualTo(1)
+ val meshFormat = inProgressStroke.getMeshFormat(0, 0)
+
+ assertThat(meshRenderer.obtainShaderMetadata(meshFormat, isPacked = false))
+ .isSameInstanceAs(meshRenderer.obtainShaderMetadata(meshFormat, isPacked = false))
+ }
+
+ @Test
+ fun obtainShaderMetadata_whenCalledTwiceWithEquivalentUnpackedFormat_returnsCachedValue() {
+ val inProgressStroke = InProgressStroke()
+ inProgressStroke.start(brush)
+ assertThat(inProgressStroke.getBrushCoatCount()).isEqualTo(1)
+ assertThat(inProgressStroke.getMeshPartitionCount(0)).isEqualTo(1)
+
+ val anotherInProgressStroke = InProgressStroke()
+ anotherInProgressStroke.start(brush)
+ assertThat(anotherInProgressStroke.getBrushCoatCount()).isEqualTo(1)
+ assertThat(anotherInProgressStroke.getMeshPartitionCount(0)).isEqualTo(1)
+
+ assertThat(
+ meshRenderer.obtainShaderMetadata(
+ inProgressStroke.getMeshFormat(0, 0),
+ isPacked = false
+ )
+ )
+ .isSameInstanceAs(
+ meshRenderer.obtainShaderMetadata(
+ anotherInProgressStroke.getMeshFormat(0, 0),
+ isPacked = false,
+ )
+ )
+ }
+
+ @Test
+ fun drawStroke_whenAndroidU_shouldSaveRecentlyDrawnMesh() {
+ if (BuildCompat.isAtLeastV()) {
+ return
+ }
+ val renderNode = RenderNode("test")
+ val canvas = renderNode.beginRecording()
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ meshRenderer.draw(canvas, stroke, Matrix())
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(1)
+
+ // New uniform value for transform scale, new mesh is created and drawn.
+ meshRenderer.draw(canvas, stroke, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(2)
+
+ // Same uniform value for transform scale, same mesh is drawn again.
+ meshRenderer.draw(canvas, stroke, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(2)
+
+ // Transform is the same but color is different, new mesh is created and drawn.
+ val strokeNewColor =
+ stroke.copy(stroke.brush.copyWithColorIntArgb(colorIntArgb = Color.White.toArgb()))
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(3)
+
+ // Move forward just a little bit of time, the same meshes should be saved.
+ clock.currentTimeMillis += 3500
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(3)
+
+ // Entirely different Ink mesh, so a new Android mesh is created and drawn.
+ val strokeNewMesh = stroke.copy(brush = stroke.brush.copy(size = 33F))
+ meshRenderer.draw(canvas, strokeNewMesh, Matrix())
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(4)
+
+ // Move forward enough time that older meshes would be cleaned up, but not enough time to
+ // actually trigger a cleanup. This confirms that cleanup isn't attempted on every draw
+ // call,
+ // which would significantly degrade performance.
+ clock.currentTimeMillis += 1999
+ meshRenderer.draw(canvas, strokeNewMesh, Matrix())
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(4)
+
+ // The next draw after enough time has passed should clean up the (no longer) recently drawn
+ // meshes.
+ clock.currentTimeMillis += 1
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(2)
+ }
+
+ /**
+ * Same set of steps as [drawStroke_whenAndroidU_shouldSaveRecentlyDrawnMesh], but there should
+ * never be any saved meshes.
+ */
+ @Test
+ fun drawStroke_whenAndroidVPlus_shouldNotSaveRecentlyDrawnMeshes() {
+ if (!BuildCompat.isAtLeastV()) {
+ return
+ }
+ val renderNode = RenderNode("test")
+ val canvas = renderNode.beginRecording()
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ meshRenderer.draw(canvas, stroke, Matrix())
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ meshRenderer.draw(canvas, stroke, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ meshRenderer.draw(canvas, stroke, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ val strokeNewColor =
+ stroke.copy(stroke.brush.copyWithColorIntArgb(colorIntArgb = Color.White.toArgb()))
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ clock.currentTimeMillis += 2500
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ val strokeNewMesh = stroke.copy(brush = stroke.brush.copy(size = 33F))
+ meshRenderer.draw(canvas, strokeNewMesh, Matrix())
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+
+ clock.currentTimeMillis += 3000
+ meshRenderer.draw(canvas, strokeNewColor, Matrix().apply { setScale(3F, 4F) })
+ assertThat(meshRenderer.getRecentlyDrawnAndroidMeshesCount()).isEqualTo(0)
+ }
+
+ private class FakeClock(var currentTimeMillis: Long = 1000L)
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTest.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTest.kt
new file mode 100644
index 0000000..438538a
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.view
+
+import androidx.ink.rendering.android.canvas.SCREENSHOT_GOLDEN_DIRECTORY
+import androidx.ink.rendering.test.R
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.captureToBitmap
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class ViewStrokeRendererTest {
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule(ViewStrokeRendererTestActivity::class.java)
+
+ @get:Rule val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_DIRECTORY)
+
+ @Test
+ fun drawWithStrokes_strokesAreAntialiased() {
+ assertScreenshot("StrokesAreAntialiased")
+ }
+
+ private fun assertScreenshot(filename: String) {
+ onView(withId(R.id.activity_root))
+ .perform(
+ captureToBitmap() {
+ it.assertAgainstGolden(screenshotRule, "${this::class.simpleName}_$filename")
+ }
+ )
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTestActivity.kt b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTestActivity.kt
new file mode 100644
index 0000000..a27f8df
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/kotlin/androidx/ink/rendering/android/view/ViewStrokeRendererTestActivity.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.view
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Canvas
+import android.os.Bundle
+import android.view.View
+import android.widget.RelativeLayout
+import androidx.ink.brush.Brush
+import androidx.ink.brush.InputToolType
+import androidx.ink.brush.StockBrushes
+import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
+import androidx.ink.rendering.android.canvas.TestColors
+import androidx.ink.rendering.test.R
+import androidx.ink.strokes.MutableStrokeInputBatch
+import androidx.ink.strokes.Stroke
+
+/** An [Activity] to support [ViewStrokeRendererTest]. */
+class ViewStrokeRendererTestActivity : Activity() {
+ private val strokeRenderer = CanvasStrokeRenderer.create()
+
+ private val viewToScreenScaleX = 0.5F
+ private val viewToScreenRotation = -45F
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.view_stroke_renderer_test)
+
+ // Use a non-trivial view -> screen transform to check that it is correctly applied.
+ val layout = findViewById<RelativeLayout>(R.id.stroke_view_parent)
+ layout.scaleX = viewToScreenScaleX
+ layout.rotation = viewToScreenRotation
+ layout.addView(StrokeView(this, strokeRenderer))
+ }
+
+ /** A [View] that draws multiple transformed strokes using [CanvasStrokeRenderer]. */
+ private inner class StrokeView(context: Context, val strokeRenderer: CanvasStrokeRenderer) :
+ View(context) {
+
+ private val viewStrokeRenderer = ViewStrokeRenderer(strokeRenderer, this)
+
+ private val inputsTwist =
+ MutableStrokeInputBatch()
+ .addOrThrow(InputToolType.UNKNOWN, x = 30F, y = 0F, elapsedTimeMillis = 100)
+ .addOrThrow(InputToolType.UNKNOWN, x = 0F, y = 40F, elapsedTimeMillis = 150)
+ .addOrThrow(InputToolType.UNKNOWN, x = 40F, y = 70F, elapsedTimeMillis = 200)
+ .addOrThrow(InputToolType.UNKNOWN, x = 5F, y = 90F, elapsedTimeMillis = 250)
+ .asImmutable()
+
+ private val stroke =
+ Stroke(
+ Brush.createWithColorIntArgb(
+ family = StockBrushes.markerLatest,
+ colorIntArgb = TestColors.BLACK,
+ size = 10f,
+ epsilon = 0.1f,
+ ),
+ inputsTwist,
+ )
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ canvas.drawColor(TestColors.YELLOW)
+
+ viewStrokeRenderer.drawWithStrokes(canvas) { scope ->
+ canvas.translate(300F, 300F)
+ canvas.scale(9F, 3F)
+
+ canvas.save()
+ canvas.rotate(-45F)
+ scope.drawStroke(stroke)
+ canvas.restore()
+
+ canvas.translate(0F, 50F)
+ scope.drawStroke(stroke)
+ }
+ }
+ }
+}
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/airplane_emoji.png b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/airplane_emoji.png
new file mode 100644
index 0000000..8407cb6
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/airplane_emoji.png
Binary files differ
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/checkerboard_black_and_transparent.png b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/checkerboard_black_and_transparent.png
new file mode 100644
index 0000000..e26d8a4
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/checkerboard_black_and_transparent.png
Binary files differ
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/circle.png b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/circle.png
new file mode 100644
index 0000000..a613d10
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/circle.png
Binary files differ
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/poop_emoji.png b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/poop_emoji.png
new file mode 100644
index 0000000..73a4dc8
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/drawable-mdpi/poop_emoji.png
Binary files differ
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/layout/canvas_stroke_renderer_test.xml b/ink/ink-rendering/src/androidInstrumentedTest/res/layout/canvas_stroke_renderer_test.xml
new file mode 100644
index 0000000..a2baef9
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/layout/canvas_stroke_renderer_test.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Copyright (C) 2024 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<GridLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/stroke_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:columnCount="4"
+ android:rowCount="1"
+ tools:ignore="HardcodedText"
+ >
+
+ <!-- Labels for Canvas.draw* API along top of screen. -->
+
+ <TextView
+ android:layout_column="1"
+ android:layout_row="0"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="0"
+ android:layout_gravity="center_horizontal"
+ android:text="Mesh"
+ />
+
+ <TextView
+ android:layout_column="2"
+ android:layout_row="0"
+ android:layout_columnWeight="1"
+ android:layout_gravity="center_horizontal"
+ android:text="Path"
+ />
+
+ <TextView
+ android:layout_column="3"
+ android:layout_row="0"
+ android:layout_columnWeight="1"
+ android:layout_gravity="center_horizontal"
+ android:text="Default"
+ />
+
+</GridLayout>
diff --git a/ink/ink-rendering/src/androidInstrumentedTest/res/layout/view_stroke_renderer_test.xml b/ink/ink-rendering/src/androidInstrumentedTest/res/layout/view_stroke_renderer_test.xml
new file mode 100644
index 0000000..27cdf55
--- /dev/null
+++ b/ink/ink-rendering/src/androidInstrumentedTest/res/layout/view_stroke_renderer_test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Copyright (C) 2024 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/activity_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <RelativeLayout
+ android:id="@+id/stroke_view_parent"
+ android:layout_width="400dp"
+ android:layout_height="400dp"
+ />
+</FrameLayout>
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/TextureBitmapStore.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/TextureBitmapStore.kt
new file mode 100644
index 0000000..62c1efe
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/TextureBitmapStore.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android
+
+import android.graphics.Bitmap
+import androidx.annotation.RestrictTo
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+
+/**
+ * Interface for a callback to allow the caller to provide a particular [Bitmap] for a texture URI.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+@ExperimentalInkCustomBrushApi
+public fun interface TextureBitmapStore {
+ /**
+ * Retrieve a [Bitmap] for the given texture URI. This may be called synchronously during
+ * `onDraw`, so loading of texture files from disk and decoding them into [Bitmap] objects
+ * should be done on init. The result may be cached by consumers, so this should return a
+ * deterministic result for a given input.
+ *
+ * Textures can be disabled by having load always return null. null should also be returned when
+ * a texture can not be loaded. If null is returned, the texture layer in question should be
+ * ignored, allowing for graceful fallback. It's recommended that implementations log when a
+ * texture can not be loaded.
+ *
+ * @return The texture bitmap, if any, associated with the given URI.
+ */
+ public fun get(textureImageUri: String): Bitmap?
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt
new file mode 100644
index 0000000..dbb3d0a
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/CanvasStrokeRenderer.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import androidx.annotation.Px
+import androidx.annotation.RestrictTo
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.geometry.AffineTransform
+import androidx.ink.nativeloader.NativeLoader
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.rendering.android.canvas.internal.CanvasPathRenderer
+import androidx.ink.rendering.android.canvas.internal.CanvasStrokeUnifiedRenderer
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.Stroke
+
+/**
+ * Renders strokes to a [Canvas].
+ *
+ * In almost all cases, a developer should use an implementation of this interface obtained from
+ * [CanvasStrokeRenderer.create].
+ *
+ * However, some developers may find it helpful to use their own implementation of this interface,
+ * possibly to draw other effects to the [Canvas], typically delegating to a renderer from
+ * [CanvasStrokeRenderer.create] for part of the custom rendering behavior to have the additional
+ * effects add to or modify the standard stroke rendering behavior. Custom [CanvasStrokeRenderer]
+ * implementations are generally less efficient than effects that can be achieved with a custom
+ * [androidx.ink.brush.BrushFamily]. If a custom implementation draws to different screen locations
+ * than the standard implementation, for example surrounding a stroke with additional content, then
+ * that additional content will not be taken into account in geometry operations like
+ * [androidx.ink.geometry.Intersection] or [androidx.ink.geometry.PartitionedMesh.computeCoverage].
+ *
+ * If custom rendering is needed during live authoring of in-progress strokes and that custom
+ * rendering involves drawing content outside the stroke boundaries, then be sure to override
+ * [strokeModifiedRegionOutsetPx].
+ */
+public interface CanvasStrokeRenderer {
+
+ /**
+ * Render a single [stroke] on the provided [canvas], with its positions transformed by
+ * [strokeToCanvasTransform].
+ *
+ * To avoid needing to calculate and maintain [strokeToCanvasTransform], consider using
+ * [androidx.ink.rendering.android.view.ViewStrokeRenderer] instead.
+ *
+ * The [strokeToCanvasTransform] should represent the complete transformation from stroke
+ * coordinates to the canvas, modulo translation. Any existing transforms applied to [canvas]
+ * should be undone prior to calling [draw].
+ */
+ // TODO: b/353561141 - Reference ComposeStrokeRenderer above once implemented.
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: AffineTransform)
+
+ /**
+ * Render a single [stroke] on the provided [canvas], with its positions transformed by
+ * [strokeToCanvasTransform].
+ *
+ * To avoid needing to calculate and maintain [strokeToCanvasTransform], consider using
+ * [androidx.ink.rendering.android.view.ViewStrokeRenderer] instead.
+ *
+ * The [strokeToCanvasTransform] must be affine. It should represent the complete transformation
+ * from stroke coordinates to the canvas, modulo translation. Any existing transforms applied to
+ * [canvas] should be undone prior to calling [draw].
+ */
+ // TODO: b/353561141 - Reference ComposeStrokeRenderer above once implemented.
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: Matrix)
+
+ /**
+ * Render a single [inProgressStroke] on the provided [canvas], with its positions transformed
+ * by [strokeToCanvasTransform].
+ *
+ * The [strokeToCanvasTransform] should represent the complete transformation from stroke
+ * coordinates to the canvas, modulo translation. Any existing transforms applied to [canvas]
+ * should be undone prior to calling [draw].
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: AffineTransform,
+ )
+
+ /**
+ * Render a single [inProgressStroke] on the provided [canvas], with its positions transformed
+ * by [strokeToCanvasTransform].
+ *
+ * The [strokeToCanvasTransform] must be affine. It should represent the complete transformation
+ * from stroke coordinates to the canvas, modulo translation. Any existing transforms applied to
+ * [canvas] should be undone prior to calling [draw].
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ public fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: Matrix,
+ )
+
+ /**
+ * The distance beyond a stroke geometry's bounds that rendering might affect. This is currently
+ * only applicable to in-progress stroke rendering, where the smallest possible region of the
+ * screen is redrawn to optimize performance. But with a custom [CanvasStrokeRenderer], certain
+ * effects like drop shadows or blurs may render beyond the stroke's geometry, and setting a
+ * higher value here can ensure that artifacts are not left on screen after an in-progress
+ * stroke has moved on from a particular region of the screen. This value should be set to the
+ * lowest value that avoids the artifacts, as larger values will be less performant, and effects
+ * that rely on larger values will be less compatible with stroke geometry operations.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+ @Px
+ public fun strokeModifiedRegionOutsetPx(): Int = 3
+
+ public companion object {
+
+ init {
+ NativeLoader.load()
+ }
+
+ /** Create a [CanvasStrokeRenderer] that is appropriate to the device's API version. */
+ @JvmStatic
+ public fun create(): CanvasStrokeRenderer {
+ @OptIn(ExperimentalInkCustomBrushApi::class)
+ return create(textureStore = TextureBitmapStore { null })
+ }
+
+ /**
+ * Create a [CanvasStrokeRenderer] that is appropriate to the device's API version.
+ *
+ * @param textureStore The [TextureBitmapStore] that will be called to retrieve image data
+ * for drawing textured strokes.
+ * @param forcePathRendering Overrides the drawing strategy selected based on API version to
+ * always draw strokes using [Canvas.drawPath] instead of [Canvas.drawMesh].
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
+ @ExperimentalInkCustomBrushApi
+ @JvmOverloads
+ @JvmStatic
+ public fun create(
+ textureStore: TextureBitmapStore,
+ forcePathRendering: Boolean = false,
+ ): CanvasStrokeRenderer {
+ if (!forcePathRendering) return CanvasStrokeUnifiedRenderer(textureStore)
+ return CanvasPathRenderer(textureStore)
+ }
+ }
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/StrokeDrawScope.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/StrokeDrawScope.kt
new file mode 100644
index 0000000..a4d1a05
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/StrokeDrawScope.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import androidx.annotation.RestrictTo
+import androidx.ink.strokes.Stroke
+
+/**
+ * A utility to simplify usage of [CanvasStrokeRenderer] by automatically calculating the
+ * `strokeToCanvasTransform` parameter of [CanvasStrokeRenderer.draw]. Obtain an instance of this
+ * class using [androidx.ink.rendering.android.view.ViewStrokeRenderer], if using
+ * [android.view.View]. Use this scope by calling its [drawStroke] function.
+ */
+// TODO: b/353561141 - Reference ComposeStrokeRenderer above once implemented.
+public class StrokeDrawScope
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(private val renderer: CanvasStrokeRenderer) {
+
+ /**
+ * Pre-allocated value updated in [onDrawStart] holding a transform from the
+ * implementation-defined "initial" transform state of the canvas to screen coordinates as
+ * described below.
+ *
+ * We want to be able to calculate the complete (Local -> Screen) transform inside [drawStroke].
+ * The only options to track changes made by client code to the [Canvas] transform state are:
+ * 1. Require the user to explicitly pass the transform as in [CanvasStrokeRenderer],
+ * 2. Create and require clients to use a complete [Canvas] wrapper type, or
+ * 3. Make use of the deprecated [Canvas.getMatrix] method.
+ *
+ * Option 1 is provided, but cumbersome for clients. We are avoiding option 2, because it would
+ * also be cumbersome client code, and we would need to track every API adding or breaking
+ * change in [Canvas].
+ *
+ * Part of the reason for the deprecation documented on [Canvas.getMatrix] is that hardware
+ * accelerated canvases have an implementation-defined matrix value when passed to a `View`,
+ * because they may be anywhere in the `View` hierarchy. However, we can use the delta between
+ * the values returned by two calls to [Canvas.getMatrix] to find the relative change in
+ * transformations.
+ *
+ * We assume that any non-identity value of the matrix at the start of [onDrawStart] is already
+ * part of the `canvasToScreenTransform` passed to [onDrawStart] as shown in the following
+ * diagram:
+ *
+ * |- [canvasToScreenTransform] -|
+ * | |
+ * | |- canvas.getMatrix() -|
+ * | | in [onDrawStart] |
+ *
+ * (Local -> Screen) = (Initial -> Screen) * (Canvas ---> Initial) * (Local -> Canvas)
+ *
+ * | canvas.getMatrix() |
+ * |- in [drawStroke] -|
+ */
+ private val initialCanvasToScreenTransform = Matrix()
+ private lateinit var canvas: Canvas
+
+ /**
+ * Pre-allocated total transform from drawn object local coordinates to screen coordinates
+ * calculated once per call to [drawStroke].
+ */
+ private val localToScreenTransform = Matrix()
+
+ /**
+ * Pre-allocated inverse of [localToScreenTransform] calculated once per call to [drawStroke].
+ *
+ * TODO: b/353302113 - Delete once the renderer can draw without modifying canvas transform
+ * state.
+ */
+ private val screenToLocalTransform = Matrix()
+
+ /** Overwrite this object for reuse. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun onDrawStart(canvasToScreenTransform: Matrix, newCanvas: Canvas) {
+ canvas = newCanvas
+ with(initialCanvasToScreenTransform) {
+ // (Canvas -> Initial)
+ @Suppress("DEPRECATION") canvas.getMatrix(this)
+ // (Initial -> Canvas)
+ invert(this)
+ // (Initial -> Screen) = (Canvas -> Screen) * (Initial -> Canvas)
+ postConcat(canvasToScreenTransform)
+ }
+ }
+
+ /** Draw the given [Stroke] to the [Canvas] represented by this scope. */
+ public fun drawStroke(stroke: Stroke) {
+ // First, calculate (Local -> Screen). That is the transform that the renderer needs.
+ with(localToScreenTransform) {
+ // (Local -> Initial)
+ @Suppress("DEPRECATION") canvas.getMatrix(this)
+ // (Local -> Screen) = (Initial -> Screen) * (Local -> Initial)
+ postConcat(initialCanvasToScreenTransform)
+ }
+
+ // Second, apply the inverse of (Local -> Screen) to the [Canvas], since the renderer will
+ // apply the provided transform to the [Canvas]. This cancels the two out.
+ // TODO: b/353302113 - Do not modify Canvas transform when new draw API is available.
+ canvas.save()
+ localToScreenTransform.invert(screenToLocalTransform)
+ canvas.concat(screenToLocalTransform)
+
+ renderer.draw(canvas, stroke, localToScreenTransform)
+
+ canvas.restore()
+ }
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BlendModeConversions.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BlendModeConversions.kt
new file mode 100644
index 0000000..bac46ad
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BlendModeConversions.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalInkCustomBrushApi::class)
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.BlendMode
+import android.graphics.PorterDuff
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+
+/** Returns the Android [PorterDuff.Mode] that is equivalent to this Ink [BrushPaint.BlendMode]. */
+internal fun BrushPaint.BlendMode.toPorterDuffMode() =
+ when (this) {
+ // Note that the MODULATE behavior is incorrectly called MULTIPLY in [PorterDuff.Mode].
+ BrushPaint.BlendMode.MODULATE -> PorterDuff.Mode.MULTIPLY
+ BrushPaint.BlendMode.DST_IN -> PorterDuff.Mode.DST_IN
+ BrushPaint.BlendMode.DST_OUT -> PorterDuff.Mode.DST_OUT
+ BrushPaint.BlendMode.SRC_ATOP -> PorterDuff.Mode.SRC_ATOP
+ BrushPaint.BlendMode.SRC_IN -> PorterDuff.Mode.SRC_IN
+ BrushPaint.BlendMode.SRC_OVER -> PorterDuff.Mode.SRC_OVER
+ BrushPaint.BlendMode.DST_OVER -> PorterDuff.Mode.DST_OVER
+ BrushPaint.BlendMode.SRC -> PorterDuff.Mode.SRC
+ BrushPaint.BlendMode.DST -> PorterDuff.Mode.DST
+ BrushPaint.BlendMode.SRC_OUT -> PorterDuff.Mode.SRC_OUT
+ BrushPaint.BlendMode.DST_ATOP -> PorterDuff.Mode.DST_ATOP
+ BrushPaint.BlendMode.XOR -> PorterDuff.Mode.XOR
+ else -> {
+ Log.e(
+ "BlendModeConversion",
+ "Unsupported BlendMode: $this. Using PorterDuff.Mode.MULTIPLY instead.",
+ )
+ PorterDuff.Mode.MULTIPLY
+ }
+ }
+
+/** Like [toPorterDuffMode], but with SRC and DST swapped. */
+internal fun BrushPaint.BlendMode.toReversePorterDuffMode() =
+ when (this) {
+ // Note that the MODULATE behavior is incorrectly called MULTIPLY in [PorterDuff.Mode].
+ BrushPaint.BlendMode.MODULATE -> PorterDuff.Mode.MULTIPLY
+ BrushPaint.BlendMode.DST_IN -> PorterDuff.Mode.SRC_IN
+ BrushPaint.BlendMode.DST_OUT -> PorterDuff.Mode.SRC_OUT
+ BrushPaint.BlendMode.SRC_ATOP -> PorterDuff.Mode.DST_ATOP
+ BrushPaint.BlendMode.SRC_IN -> PorterDuff.Mode.DST_IN
+ BrushPaint.BlendMode.SRC_OVER -> PorterDuff.Mode.DST_OVER
+ BrushPaint.BlendMode.DST_OVER -> PorterDuff.Mode.SRC_OVER
+ BrushPaint.BlendMode.SRC -> PorterDuff.Mode.DST
+ BrushPaint.BlendMode.DST -> PorterDuff.Mode.SRC
+ BrushPaint.BlendMode.SRC_OUT -> PorterDuff.Mode.DST_OUT
+ BrushPaint.BlendMode.DST_ATOP -> PorterDuff.Mode.SRC_ATOP
+ BrushPaint.BlendMode.XOR -> PorterDuff.Mode.XOR
+ else -> {
+ Log.e(
+ "BlendModeConversion",
+ "Unsupported TextureBlendMode: $this. Using PorterDuff.Mode.MULTIPLY instead.",
+ )
+ PorterDuff.Mode.MULTIPLY
+ }
+ }
+
+/** Returns the Android [BlendMode] that is equivalent to this Ink [BrushPaint.BlendMode]. */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+internal fun BrushPaint.BlendMode.toBlendMode() =
+ when (this) {
+ BrushPaint.BlendMode.MODULATE -> BlendMode.MODULATE
+ BrushPaint.BlendMode.DST_IN -> BlendMode.DST_IN
+ BrushPaint.BlendMode.DST_OUT -> BlendMode.DST_OUT
+ BrushPaint.BlendMode.SRC_ATOP -> BlendMode.SRC_ATOP
+ BrushPaint.BlendMode.SRC_IN -> BlendMode.SRC_IN
+ BrushPaint.BlendMode.SRC_OVER -> BlendMode.SRC_OVER
+ BrushPaint.BlendMode.DST_OVER -> BlendMode.DST_OVER
+ BrushPaint.BlendMode.SRC -> BlendMode.SRC
+ BrushPaint.BlendMode.DST -> BlendMode.DST
+ BrushPaint.BlendMode.SRC_OUT -> BlendMode.SRC_OUT
+ BrushPaint.BlendMode.DST_ATOP -> BlendMode.DST_ATOP
+ BrushPaint.BlendMode.XOR -> BlendMode.XOR
+ else -> {
+ Log.e(
+ "BlendModeConversion",
+ "Unsupported BlendMode: $this. Using BlendMode.MODULATE instead.",
+ )
+ BlendMode.MODULATE
+ }
+ }
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCache.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCache.kt
new file mode 100644
index 0000000..fd15111
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/BrushPaintCache.kt
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.Color
+import android.graphics.ComposeShader
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Shader
+import android.os.Build
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.strokes.StrokeInput
+import java.util.WeakHashMap
+
+/**
+ * Helper class for obtaining [Paint] from [BrushPaint].
+ *
+ * @param paintFlags Used to set [Paint.flags] for all [Paint] objects it creates.
+ * @param applyColorFilterToTexture If true, the [BrushPaint] and the provided color are used to
+ * configure [Paint.colorFilter] to apply a color to the paint's shader. This should generally be
+ * set when using an API that expects a color to be uniformly applied by the Paint, instead of
+ * providing per-vertex-modified colors to the draw call.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+internal class BrushPaintCache(
+ val textureStore: TextureBitmapStore,
+ val additionalPaintFlags: Int = 0,
+ val applyColorFilterToTexture: Boolean = false,
+) {
+
+ /** Holds onto the [Paint] for each [BrushPaint] for efficiency. */
+ private val paintCache = WeakHashMap<BrushPaint, PaintCacheData>()
+
+ /** Used to construct and update a shader, holding on to data that's needed for later update. */
+ private inner class ShaderHelper(
+ private val textureLayers: List<BrushPaint.TextureLayer>,
+ private val bitmaps: List<Bitmap?>,
+ private val bitmapShaders: List<Shader?>,
+ ) {
+ private val scratchMatrix = Matrix()
+
+ private val bitmapShaderLocalMatrices: List<Matrix?>? =
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ // `Shader.setLocalMatrix` saves the `Matrix` instance rather than copying its data
+ // to an
+ // internal instance before API 26, so allocate dedicated `Matrix` instances for
+ // those
+ // shaders to avoid accidentally clobbering data. After API 26, `scratchMatrix` can
+ // be used
+ // for all layers to save allocations.
+ bitmapShaders.map { if (it == null) null else Matrix() }
+ } else {
+ null
+ }
+
+ init {
+ require(
+ bitmaps.size == textureLayers.size && bitmapShaders.size == textureLayers.size
+ ) {
+ "textureLayers, bitmaps, and bitmapShaders should be parallel lists."
+ }
+ for (i in 0 until textureLayers.size) {
+ require(bitmapShaders[i] == null || bitmaps[i] != null) {
+ "bitmap[$i] should be non-null if bitmapShaders[$i] is non-null."
+ }
+ }
+ }
+
+ fun updateInternalToStrokeTransform(
+ @FloatRange(from = 0.0) brushSize: Float,
+ firstInput: StrokeInput,
+ lastInput: StrokeInput,
+ internalToStrokeTransform: Matrix?,
+ ) {
+ for (i in 0 until textureLayers.size) {
+ val bitmapShader = bitmapShaders[i] ?: continue
+ val textureLayer = textureLayers[i]
+ val bitmap =
+ checkNotNull(bitmaps[i]) {
+ "bitmap[$i] should be non-null if bitmapShaders[$i] is non-null."
+ }
+ val scratchShaderLocalMatrix =
+ if (bitmapShaderLocalMatrices != null) {
+ checkNotNull(bitmapShaderLocalMatrices[i]) {
+ "bitmapShaderLocalMatrices[$i] shouldbe non-null if bitmapShader[$i] is non-null."
+ }
+ } else {
+ scratchMatrix
+ }
+ // The texture coordinates being drawn are in the mesh's "internal" coordinate space
+ // (which
+ // for legacy strokes may be different than the publicly facing stroke coordinate
+ // space). However, [BitmapShader] assumes we're working with texel coordinates, so
+ // we need
+ // to compute the combined chain of transforms from that coordinate space to
+ // "internal" mesh
+ // space.
+ val texelToInternalTransform =
+ scratchShaderLocalMatrix.also {
+ // At the end of this chain of transforms, we'll need to go from stroke
+ // space to
+ // "internal" mesh space. Start by computing that, then we'll work
+ // backwards.
+ //
+ // Compute (stroke -> internal) = (internal -> stroke)^-1
+ //
+ // Note that internalToStrokeTransform is nullable; if null, we treat it as
+ // an identity
+ // matrix, but skip the needless call to [invert].
+ it.reset()
+ internalToStrokeTransform?.invert(it)
+
+ // While we're in stroke space, shift the origin to the position specified
+ // by the
+ // [TextureLayer].
+ when (textureLayer.origin) {
+ BrushPaint.TextureOrigin.STROKE_SPACE_ORIGIN -> {}
+ BrushPaint.TextureOrigin.FIRST_STROKE_INPUT -> {
+ it.preTranslate(firstInput.x, firstInput.y)
+ }
+ BrushPaint.TextureOrigin.LAST_STROKE_INPUT -> {
+ it.preTranslate(lastInput.x, lastInput.y)
+ }
+ }
+
+ // To get to stroke space, we first need to scale from the coordinate space
+ // where
+ // distance is measured in the chosen SizeUnit for this particular texture
+ // layer.
+ //
+ // Compute (SizeUnit -> internal) = (stroke -> internal) * (SizeUnit ->
+ // stroke)
+ when (textureLayer.sizeUnit) {
+ BrushPaint.TextureSizeUnit.BRUSH_SIZE ->
+ it.preScale(brushSize, brushSize)
+ BrushPaint.TextureSizeUnit.STROKE_SIZE -> {
+ // TODO: b/336835642 - Implement BrushPaintCache support for
+ // TextureSizeUnit.STROKE_SIZE.
+ }
+ BrushPaint.TextureSizeUnit.STROKE_COORDINATES -> {
+ // Nothing to do, since stroke space and SizeUnit space are
+ // identical.
+ }
+ }
+
+ // To get to SizeUnit space, we first need to scale from the texture UV
+ // coordinate
+ // space; that is, the coordinate space where the texture image is a unit
+ // square.
+ //
+ // Compute (UV -> internal) = (SizeUnit -> internal) * (UV -> SizeUnit)
+ it.preScale(textureLayer.sizeX, textureLayer.sizeY)
+
+ // The texture offset is specified as fractions of the texture size; in
+ // other words, it
+ // should be applied within texture UV space.
+ it.preTranslate(textureLayer.offsetX, textureLayer.offsetY)
+
+ // To get to texture UV space, we first need to scale from the coordinate
+ // space where
+ // distance is measured in texels; that is, where each texel is a unit
+ // square.
+ //
+ // Compute (texel -> internal) = (UV -> internal) * (texel -> UV)
+ it.preScale(1f / bitmap.width, 1f / bitmap.height)
+ }
+ // Do not use Matrix.isIdentity - it returns false for the identity matrix on
+ // earlier API
+ // levels.
+ val localMatrix =
+ if (texelToInternalTransform == IDENTITY_MATRIX) {
+ null
+ } else {
+ texelToInternalTransform
+ }
+ bitmapShader.setLocalMatrix(localMatrix)
+ }
+ }
+ }
+
+ private class ColorFilterHelper {
+ private @ColorInt var colorFilterColor: Int = 0
+
+ fun updateColorFilterColor(
+ paint: Paint,
+ brushPaint: BrushPaint,
+ @ColorInt paintColor: Int
+ ) {
+ if (paint.colorFilter != null && colorFilterColor == paintColor) return
+ val lastTextureLayer =
+ requireNotNull(brushPaint.textureLayers.lastOrNull()) {
+ "Paint.colorFilter should only be used when Paint.shader is set, which should only " +
+ "happen when there is at least one item in BrushPaint.textureLayers."
+ }
+ // In [CanvasMeshRenderer], when we call [Canvas.drawMesh] with the last texture layer's
+ // blend
+ // mode, that method treats the mesh color as the DST, and the shader texture as the SRC
+ // (which matches how we've specified the meaning of [BrushPaint.BlendMode]). Here, we
+ // are
+ // using a color filter to emulate that behavior for the sake of [CanvasPathRenderer],
+ // but the
+ // color filter treats [paintColor] as the SRC, and the path texture as the DST. So we
+ // need
+ // to use [toReversePorterDuffMode] here so as to swap SRC and DST from what the
+ // [BrushPaint.BlendMode] says.
+ val colorBlendMode = lastTextureLayer.blendMode.toReversePorterDuffMode()
+ paint.colorFilter = PorterDuffColorFilter(paintColor, colorBlendMode)
+ colorFilterColor = paintColor
+ }
+ }
+
+ private fun createCacheData(brushPaint: BrushPaint): PaintCacheData {
+ val paint =
+ Paint(additionalPaintFlags).apply {
+ // This sets Paint.FILTER_BITMAP_FLAG for consistency. For Android versions <= O,
+ // bilinear
+ // sampling is always used on scaled bitmaps when hardware acceleration is available
+ // and
+ // the behavior depends on this flag otherwise. Starting at Android Q, this flag is
+ // set by
+ // default. So setting it results in consistent behavior for Android P and for <= O
+ // when
+ // hardware acceleration is not available.
+ setFilterBitmap(true)
+ }
+ val textureLayers = brushPaint.textureLayers
+ if (textureLayers.isEmpty()) {
+ // Early exit for efficiency.
+ return PaintCacheData(paint)
+ }
+ val bitmaps = textureLayers.map { textureStore.get(it.colorTextureUri) }
+ val bitmapShaders =
+ bitmaps.map { bitmap ->
+ if (bitmap == null) return@map null
+ BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
+ }
+ // Each layer is combined with the result of combining all of the previous layers, using the
+ // immediately previous layer's blend mode. (Effectively, ComposeShader acts as the non-leaf
+ // nodes in a binary tree; more like a linked-list in this case because the destination side
+ // is
+ // always a leaf.) No layers reduce to null, a single layer reduces to the single
+ // BitmapShader.
+ paint.shader =
+ bitmapShaders.reduceIndexedOrNull<Shader?, Shader?> { i, acc, shader ->
+ when {
+ // TextureLayers that fail to resolve to a texture Bitmap are ignored. This
+ // seems like
+ // clearer behavior than refusing to apply the whole texture, and a more gentle
+ // fallback
+ // than crashing. It also allows textures to be disabled with a
+ // TextureBitmapStore whose
+ // load method returns null.
+ acc == null -> shader
+ shader == null -> acc
+ // The constructor arguments are destination, source, blend mode.
+ else ->
+ ComposeShader(
+ shader,
+ acc,
+ textureLayers[i - 1].blendMode.toPorterDuffMode()
+ )
+ }
+ }
+ return PaintCacheData(
+ paint,
+ // Only construct the ShaderHelper if we actually loaded some texture bitmaps and
+ // generated
+ // a shader.
+ if (paint.shader != null) ShaderHelper(textureLayers, bitmaps, bitmapShaders) else null,
+ if (applyColorFilterToTexture && paint.shader != null) ColorFilterHelper() else null,
+ )
+ }
+
+ private fun PaintCacheData.update(
+ brushPaint: BrushPaint,
+ @ColorInt paintColor: Int,
+ @FloatRange(from = 0.0) brushSize: Float,
+ firstInput: StrokeInput,
+ lastInput: StrokeInput,
+ internalToStrokeTransform: Matrix?,
+ ) {
+ shaderHelper?.updateInternalToStrokeTransform(
+ brushSize,
+ firstInput,
+ lastInput,
+ internalToStrokeTransform,
+ )
+ if (colorFilterHelper != null) {
+ colorFilterHelper.updateColorFilterColor(paint, brushPaint, paintColor)
+ paint.color = Color.WHITE
+ } else {
+ paint.color = paintColor
+ }
+ }
+
+ /**
+ * Obtains a [Paint] for the [BrushPaint] from the cache, creating it if necessary and updating
+ * it with the current [internalToStrokeTransform]. If [BrushPaint.TextureLayer.colorTextureUri]
+ * can't be resolved to a bitmap for any layer, that layer is ignored.
+ *
+ * @param brushPaint Used to configure [Paint.shader].
+ * @param paintColor Used to set [Paint.color].
+ * @param brushSize Used for supporting [BrushPaint.TextureSizeUnit.BRUSH_SIZE].
+ * @param firstInput Used for supporting [BrushPaint.TextureOrigin.FIRST_STROKE_INPUT].
+ * @param lastInput Used for supporting [BrushPaint.TextureOrigin.LAST_STROKE_INPUT].
+ * @param internalToStrokeTransform Used to update the local matrix of [Paint.shader] if
+ * applicable. Defaults to null, which is treated equivalently to the identity matrix.
+ */
+ fun obtain(
+ brushPaint: BrushPaint,
+ @ColorInt paintColor: Int,
+ @FloatRange(from = 0.0) brushSize: Float,
+ firstInput: StrokeInput,
+ lastInput: StrokeInput,
+ internalToStrokeTransform: Matrix? = null,
+ ): Paint {
+ val cached = paintCache.getOrPut(brushPaint) { createCacheData(brushPaint) }
+ cached.update(
+ brushPaint,
+ paintColor,
+ brushSize,
+ firstInput,
+ lastInput,
+ internalToStrokeTransform,
+ )
+ return cached.paint
+ }
+
+ private class PaintCacheData(
+ val paint: Paint,
+ val shaderHelper: ShaderHelper? = null,
+ val colorFilterHelper: ColorFilterHelper? = null,
+ )
+
+ private companion object {
+ // Would be better to use the immutable [Matrix.IDENTITY_MATRIX], but that's in API version
+ // 31.
+ val IDENTITY_MATRIX = Matrix()
+ }
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt
new file mode 100644
index 0000000..ebd2194
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasMeshRenderer.kt
@@ -0,0 +1,995 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color as AndroidColor
+import android.graphics.ColorSpace as AndroidColorSpace
+import android.graphics.Matrix
+import android.graphics.Mesh as AndroidMesh
+import android.graphics.MeshSpecification
+import android.graphics.Paint
+import android.graphics.RectF
+import android.os.Build
+import android.os.SystemClock
+import androidx.annotation.RequiresApi
+import androidx.annotation.Size
+import androidx.annotation.VisibleForTesting
+import androidx.collection.MutableObjectLongMap
+import androidx.core.os.BuildCompat
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.color.Color as ComposeColor
+import androidx.ink.brush.color.colorspace.ColorSpaces as ComposeColorSpaces
+import androidx.ink.geometry.AffineTransform
+import androidx.ink.geometry.BoxAccumulator
+import androidx.ink.geometry.Mesh as InkMesh
+import androidx.ink.geometry.MeshAttributeUnpackingParams
+import androidx.ink.geometry.MeshFormat
+import androidx.ink.geometry.populateMatrix
+import androidx.ink.nativeloader.NativeLoader
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.Stroke
+import androidx.ink.strokes.StrokeInput
+import java.util.WeakHashMap
+
+/**
+ * Renders Ink objects using [Canvas.drawMesh]. This is the most fully-featured and performant
+ * [Canvas] Ink renderer.
+ *
+ * This is not thread safe, so if it must be used from multiple threads, the caller is responsible
+ * for synchronizing access. If it is being used in two very different contexts where there are
+ * unlikely to be cached mesh data in common, the easiest solution to thread safety is to have two
+ * different instances of this object.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+internal class CanvasMeshRenderer(
+ textureStore: TextureBitmapStore = TextureBitmapStore { null },
+ /** Monotonic time with a non-epoch zero time. */
+ private val getDurationTimeMillis: () -> Long = SystemClock::elapsedRealtime,
+) : CanvasStrokeRenderer {
+
+ /** Caches [Paint] objects so these can be reused between strokes with the same [BrushPaint]. */
+ private val paintCache = BrushPaintCache(textureStore)
+
+ /**
+ * Caches [android.graphics.Mesh] instances so that they can be reused between calls to
+ * [Canvas.drawMesh], greatly improving performance.
+ *
+ * On Android U, a bug in [android.graphics.Mesh] uniform handling causes a mesh rendered twice
+ * with different uniform values to overwrite the first draw's uniform values with the second
+ * draw's values. Therefore, [MeshData] tracks the most recent uniform data that a mesh has been
+ * drawn with, and if the next draw differs from the previous draw, a new
+ * [android.graphics.Mesh] will be created to satisfy it. This allows the typical use case,
+ * where a stroke is drawn the same way frame to frame, to remain fast and reuse cached
+ * [android.graphics.Mesh] instances. But less typical use cases, like animations that change
+ * the color or scale/rotation of strokes, will still work but will be a little slower.
+ *
+ * On Android V+, this bug has been fixed, so the same [android.graphics.Mesh] can be reused
+ * with different uniform values even within the same frame. Therefore, the extra data in
+ * [MeshData] is ignored, and it will just contain the values that were first used when
+ * rendering the associated [InkMesh].
+ */
+ private val inkMeshToAndroidMesh = WeakHashMap<InkMesh, MeshData>()
+
+ /**
+ * On Android U, this holds strong references to [android.graphics.Mesh] instances that were
+ * recently used in [draw], so that they aren't garbage collected too soon causing their
+ * underlying memory to be freed before the render thread can use it. Otherwise, the render
+ * thread may use the memory after it is freed, leading to undefined behavior (typically a
+ * crash). The values of this map are the last time (according to [getDurationTimeMillis]) that
+ * the corresponding mesh has been drawn, so that its contents can be periodically evicted to
+ * keep memory usage under control.
+ *
+ * On Android V+, this bug has been fixed, so this map will remain empty.
+ */
+ private val recentlyDrawnMeshesToLastDrawTimeMillis = MutableObjectLongMap<AndroidMesh>()
+
+ /**
+ * The last time that [recentlyDrawnMeshesToLastDrawTimeMillis] was checked for old meshes that
+ * can be cleaned up.
+ */
+ private var recentlyDrawnMeshesLastCleanupTimeMillis = Long.MIN_VALUE
+
+ /**
+ * Cached [android.graphics.Mesh]es so that they can be reused between calls to
+ * [Canvas.drawMesh], greatly improving performance. Each [InProgressStroke] maps to a list of
+ * [InProgressMeshData] objects, one for each brush coat. Because [InProgressStroke] is mutable,
+ * this cache is based not just on the existence of data, but whether that data's version number
+ * matches that of the [InProgressStroke].
+ */
+ private val inProgressStrokeToAndroidMeshes =
+ WeakHashMap<InProgressStroke, List<InProgressMeshData>>()
+
+ /**
+ * Caches [ShaderMetadata]s so that when two [MeshFormat] objects have equivalent packed
+ * representations (see [MeshFormat.isPackedEquivalent]), the same [ShaderMetadata] object can
+ * be used instead of reconstructed. This is a list instead of a map because:
+ * 1. There should never be more than ~9 unique values of [MeshFormat], so a linear scan is not
+ * an undue cost when constructing a new [android.graphics.Mesh].
+ * 2. [MeshFormat] does not implement `hashCode` and `equals` in a way that would be relevant
+ * here, since we only care about the packed representation for this use case. This could be
+ * worked around with a wrapper type of [MeshFormat] that is specific to the packed
+ * representation, but it didn't seem worth the extra effort.
+ *
+ * @See [obtainShaderMetadata] for the management of this cache.
+ */
+ private val meshFormatToPackedShaderMetadata = ArrayList<Pair<MeshFormat, ShaderMetadata>>()
+
+ /**
+ * Holds a mapping from [MeshFormat] to [ShaderMetadata], so that when two [MeshFormat] objects
+ * are equivalent when it comes to their unpacked representation (see
+ * [MeshFormat.isUnpackedEquivalent]), then the same [MeshSpecification] object can be used
+ * instead of reconstructed. This is a list instead of a map because
+ * 1. There should never be more than ~9 unique values of [MeshFormat], so a linear scan is not
+ * an undue cost when constructing a new [android.graphics.Mesh].
+ * 2. [MeshFormat] does not implement `hashCode` and `equals` in a way that would be relevant
+ * here, since we only care about the unpacked representation for this use case.
+ *
+ * @See [obtainShaderMetadata] for the management of this cache.
+ */
+ private val meshFormatToUnpackedShaderMetadata = ArrayList<Pair<MeshFormat, ShaderMetadata>>()
+
+ /** Scratch [Matrix] used for draw calls taking an [AffineTransform]. */
+ private val scratchMatrix = Matrix()
+
+ /** Scratch space used as the argument to [Matrix.getValues]. */
+ private val matrixValuesScratchArray = FloatArray(9)
+
+ /** Scratch space used to hold the scale/skew components of a [Matrix] in column-major order. */
+ @Size(4) private val objectToCanvasLinearComponentScratch = FloatArray(4)
+
+ /** Allocated once and reused for performance, passed to [AndroidMesh.setFloatUniform]. */
+ private val colorRgbaScratchArray = FloatArray(4)
+
+ // First and last inputs for the stroke being rendered, reused so that we don't need to allocate
+ // new ones for every stroke.
+ private val scratchFirstInput = StrokeInput()
+ private val scratchLastInput = StrokeInput()
+
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: AffineTransform) {
+ strokeToCanvasTransform.populateMatrix(scratchMatrix)
+ draw(canvas, stroke, scratchMatrix)
+ }
+
+ /**
+ * Draw a [Stroke] to the [Canvas].
+ *
+ * @param canvas The [Canvas] to draw to.
+ * @param stroke The [Stroke] to draw.
+ * @param strokeToCanvasTransform The transform [Matrix] to convert from [Stroke] actual
+ * coordinates to the coordinates of [canvas]. It is important to pass this here to be applied
+ * internally rather than applying it to [canvas] in calling code, to ensure anti-aliasing has
+ * the information it needs to render properly. In addition, any transforms previously applied
+ * to [canvas] must only be translations, or rotations in multiples of 90 degrees. If you are
+ * not transforming [canvas] yourself then this will be correct, as the [android.view.View]
+ * hierarchy applies only translations by default. If you are rendering in a
+ * [android.view.View] where it (or one of its ancestors) is rotated or scaled within its
+ * parent, or if you are applying rotation or scaling transforms to [canvas] yourself, then
+ * care must be taken to undo those transforms before calling this method, and calling this
+ * method with a full stroke-to-screen (modulo translation or multi-90 degree rotation)
+ * transform. Without this, anti-aliasing at the edge of strokes will not render properly.
+ */
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: Matrix) {
+ require(strokeToCanvasTransform.isAffine) { "strokeToCanvasTransform must be affine" }
+ if (stroke.inputs.isEmpty()) return // nothing to draw
+ stroke.inputs.populate(0, scratchFirstInput)
+ stroke.inputs.populate(stroke.inputs.size - 1, scratchLastInput)
+ for (coatIndex in 0 until stroke.brush.family.coats.size) {
+ val meshes = stroke.shape.renderGroupMeshes(coatIndex)
+ if (meshes.isEmpty()) continue
+ val brushPaint = stroke.brush.family.coats[coatIndex].paint
+ val blendMode = finalBlendMode(brushPaint)
+ // A white paint color ensures that the paint color doesn't affect how the paint texture
+ // is blended with the mesh coloring.
+ val androidPaint =
+ paintCache.obtain(
+ brushPaint,
+ AndroidColor.WHITE,
+ stroke.brush.size,
+ scratchFirstInput,
+ scratchLastInput,
+ )
+ for (mesh in meshes) {
+ drawFromStroke(
+ canvas,
+ mesh,
+ strokeToCanvasTransform,
+ stroke.brush.composeColor,
+ blendMode,
+ androidPaint,
+ )
+ }
+ }
+ }
+
+ /** Draw an [InkMesh] as if it is part of a stroke. */
+ private fun drawFromStroke(
+ canvas: Canvas,
+ inkMesh: InkMesh,
+ meshToCanvasTransform: Matrix,
+ brushColor: ComposeColor,
+ blendMode: BlendMode,
+ paint: Paint,
+ ) {
+ fillObjectToCanvasLinearComponent(
+ meshToCanvasTransform,
+ objectToCanvasLinearComponentScratch
+ )
+ val cachedMeshData = inkMeshToAndroidMesh[inkMesh]
+ @OptIn(BuildCompat.PrereleaseSdkCheck::class) val uniformBugFixed = BuildCompat.isAtLeastV()
+ val androidMesh =
+ if (
+ cachedMeshData == null ||
+ (!uniformBugFixed &&
+ !cachedMeshData.areUniformsEquivalent(
+ brushColor,
+ objectToCanvasLinearComponentScratch
+ ))
+ ) {
+ val newMesh =
+ createAndroidMesh(inkMesh) ?: return // Nothing to draw if the mesh is empty.
+ updateAndroidMesh(
+ newMesh,
+ inkMesh.format,
+ objectToCanvasLinearComponentScratch,
+ brushColor,
+ inkMesh.vertexAttributeUnpackingParams,
+ )
+ inkMeshToAndroidMesh[inkMesh] =
+ MeshData.create(newMesh, brushColor, objectToCanvasLinearComponentScratch)
+ newMesh
+ } else {
+ if (uniformBugFixed) {
+ // Update the uniform values unconditionally because it's inexpensive after the
+ // bug fix.
+ // Before the bug fix, there's no need to update the uniforms since changed
+ // uniform values
+ // could have caused the mesh to be recreated above.
+ updateAndroidMesh(
+ cachedMeshData.androidMesh,
+ inkMesh.format,
+ objectToCanvasLinearComponentScratch,
+ brushColor,
+ inkMesh.vertexAttributeUnpackingParams,
+ )
+ }
+ cachedMeshData.androidMesh
+ }
+
+ canvas.save()
+ try {
+ canvas.concat(meshToCanvasTransform)
+ canvas.drawMesh(androidMesh, blendMode, paint)
+ } finally {
+ // If any exceptions occur while drawing, restore the canvas so that restore is always
+ // called
+ // after canvas.save().
+ canvas.restore()
+ }
+
+ if (!uniformBugFixed) {
+ val currentTimeMillis = getDurationTimeMillis()
+ // Before the `androidMesh` variable goes out of scope, save it as a hard reference
+ // (temporarily) as a workaround for the Android U bug where drawMesh would not hand off
+ // or
+ // share ownership of the mesh data properly so data could be used by the render thread
+ // after
+ // being freed and cause a crash.
+ saveRecentlyDrawnAndroidMesh(androidMesh, currentTimeMillis)
+ // Clean up meshes that were previously saved as hard references, but shouldn't be saved
+ // forever otherwise we'll run out of memory. Anything purged by this will only be kept
+ // around
+ // if its associated InkMesh is still referenced, due to their presence in
+ // `inkMeshToAndroidMesh`.
+ cleanUpRecentlyDrawnAndroidMeshes(currentTimeMillis)
+ }
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: AffineTransform,
+ ) {
+ strokeToCanvasTransform.populateMatrix(scratchMatrix)
+ draw(canvas, inProgressStroke, scratchMatrix)
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: Matrix,
+ ) {
+ val brush =
+ checkNotNull(inProgressStroke.brush) {
+ "Attempting to draw an InProgressStroke that has not been started."
+ }
+ require(strokeToCanvasTransform.isAffine) { "strokeToCanvasTransform must be affine" }
+ val inputCount = inProgressStroke.getInputCount()
+ if (inputCount == 0) return // nothing to draw
+ inProgressStroke.populateInput(scratchFirstInput, 0)
+ inProgressStroke.populateInput(scratchLastInput, inputCount - 1)
+ fillObjectToCanvasLinearComponent(
+ strokeToCanvasTransform,
+ objectToCanvasLinearComponentScratch
+ )
+ val brushCoatCount = inProgressStroke.getBrushCoatCount()
+ canvas.save()
+ try {
+ canvas.concat(strokeToCanvasTransform)
+ for (coatIndex in 0 until brushCoatCount) {
+ val brushPaint = brush.family.coats[coatIndex].paint
+ val blendMode = finalBlendMode(brushPaint)
+ val androidPaint =
+ paintCache.obtain(
+ brushPaint,
+ AndroidColor.WHITE,
+ brush.size,
+ scratchFirstInput,
+ scratchLastInput,
+ )
+ val inProgressMeshData = obtainInProgressMeshData(inProgressStroke, coatIndex)
+ for (meshIndex in 0 until inProgressMeshData.androidMeshes.size) {
+ val androidMesh = inProgressMeshData.androidMeshes[meshIndex] ?: continue
+ updateAndroidMesh(
+ androidMesh,
+ inProgressStroke.getMeshFormat(coatIndex, meshIndex),
+ objectToCanvasLinearComponentScratch,
+ brush.composeColor,
+ attributeUnpackingParams = null,
+ )
+ canvas.drawMesh(androidMesh, blendMode, androidPaint)
+ }
+ }
+ } finally {
+ // If any exceptions occur while drawing, restore the canvas so that restore is always
+ // called
+ // after canvas.save().
+ canvas.restore()
+ }
+ }
+
+ /** Create a new [AndroidMesh] for the given [InkMesh]. */
+ private fun createAndroidMesh(inkMesh: InkMesh): AndroidMesh? {
+ val bounds = inkMesh.bounds ?: return null // Nothing to render with an empty mesh.
+ val meshSpec = obtainShaderMetadata(inkMesh.format, isPacked = true).meshSpecification
+ return AndroidMesh(
+ meshSpec,
+ AndroidMesh.TRIANGLES,
+ inkMesh.rawVertexData,
+ inkMesh.vertexCount,
+ inkMesh.rawTriangleIndexData,
+ RectF(bounds.xMin, bounds.yMin, bounds.xMax, bounds.yMax),
+ )
+ }
+
+ /**
+ * Update [androidMesh] with the information that might have changed since the previous call to
+ * [drawFromStroke] with the [InkMesh]. This is intended to be so low cost that it can be called
+ * on every draw call.
+ */
+ private fun updateAndroidMesh(
+ androidMesh: AndroidMesh,
+ meshFormat: MeshFormat,
+ @Size(min = 4) meshToCanvasLinearComponent: FloatArray,
+ brushColor: ComposeColor,
+ attributeUnpackingParams: List<MeshAttributeUnpackingParams>?,
+ ) {
+ val isPacked = attributeUnpackingParams != null
+ var colorUniformName = INVALID_NAME
+ var positionUnpackingParamsUniformName = INVALID_NAME
+ var positionAttributeIndex = INVALID_ATTRIBUTE_INDEX
+ var sideDerivativeUnpackingParamsUniformName = INVALID_NAME
+ var sideDerivativeAttributeIndex = INVALID_ATTRIBUTE_INDEX
+ var forwardDerivativeUnpackingParamsUniformName = INVALID_NAME
+ var forwardDerivativeAttributeIndex = INVALID_ATTRIBUTE_INDEX
+ var objectToCanvasLinearComponentUniformName = INVALID_NAME
+
+ for ((id, _, name, unpackingIndex) in
+ obtainShaderMetadata(meshFormat, isPacked).uniformMetadata) {
+ when (id) {
+ UniformId.OBJECT_TO_CANVAS_LINEAR_COMPONENT ->
+ objectToCanvasLinearComponentUniformName = name
+ UniformId.BRUSH_COLOR -> colorUniformName = name
+ UniformId.POSITION_UNPACKING_TRANSFORM -> {
+ check(isPacked) {
+ "Unpacking transform uniform is only supported for packed meshes."
+ }
+ positionUnpackingParamsUniformName = name
+ positionAttributeIndex = unpackingIndex
+ }
+ UniformId.SIDE_DERIVATIVE_UNPACKING_TRANSFORM -> {
+ check(isPacked) {
+ "Unpacking transform uniform is only supported for packed meshes."
+ }
+ sideDerivativeUnpackingParamsUniformName = name
+ sideDerivativeAttributeIndex = unpackingIndex
+ }
+ UniformId.FORWARD_DERIVATIVE_UNPACKING_TRANSFORM -> {
+ check(isPacked) {
+ "Unpacking transform uniform is only supported for packed meshes."
+ }
+ forwardDerivativeUnpackingParamsUniformName = name
+ forwardDerivativeAttributeIndex = unpackingIndex
+ }
+ }
+ }
+ // Color and object-to-canvas uniforms are required for all meshes.
+ check(objectToCanvasLinearComponentUniformName != INVALID_NAME)
+ check(colorUniformName != INVALID_NAME)
+ // Unpacking transform uniforms are required for and only for packed meshes.
+ check(
+ !isPacked ||
+ (positionUnpackingParamsUniformName != INVALID_NAME &&
+ sideDerivativeUnpackingParamsUniformName != INVALID_NAME &&
+ forwardDerivativeUnpackingParamsUniformName != INVALID_NAME)
+ )
+
+ androidMesh.setFloatUniform(
+ objectToCanvasLinearComponentUniformName,
+ meshToCanvasLinearComponent[0],
+ meshToCanvasLinearComponent[1],
+ meshToCanvasLinearComponent[2],
+ meshToCanvasLinearComponent[3],
+ )
+
+ // Don't use setColorUniform because it does some color space conversion that we don't want.
+ // Instead, set the uniform as an array of 4 floats, but ensure that the color is in the
+ // same
+ // color space that the MeshSpecification is configured to operate in. In
+ // LinearExtendedSrgb,
+ // "linear" refers to the format, "extended" means that the channel values are not clamped
+ // to
+ // [0, 1], and "sRGB" is the color space itself.
+ androidMesh.setFloatUniform(
+ colorUniformName,
+ colorRgbaScratchArray.also {
+ brushColor
+ .convert(ComposeColorSpaces.LinearExtendedSrgb)
+ .fillFloatArray(colorRgbaScratchArray)
+ },
+ )
+
+ if (!isPacked) return
+
+ attributeUnpackingParams!!.let {
+ val positionParams = it[positionAttributeIndex]
+ androidMesh.setFloatUniform(
+ positionUnpackingParamsUniformName,
+ positionParams.xOffset,
+ positionParams.xScale,
+ positionParams.yOffset,
+ positionParams.yScale,
+ )
+
+ val sideDerivativeParams = it[sideDerivativeAttributeIndex]
+ androidMesh.setFloatUniform(
+ sideDerivativeUnpackingParamsUniformName,
+ sideDerivativeParams.xOffset,
+ sideDerivativeParams.xScale,
+ sideDerivativeParams.yOffset,
+ sideDerivativeParams.yScale,
+ )
+
+ val forwardDerivativeParams = it[forwardDerivativeAttributeIndex]
+ androidMesh.setFloatUniform(
+ forwardDerivativeUnpackingParamsUniformName,
+ forwardDerivativeParams.xOffset,
+ forwardDerivativeParams.xScale,
+ forwardDerivativeParams.yOffset,
+ forwardDerivativeParams.yScale,
+ )
+ }
+ }
+
+ private fun fillObjectToCanvasLinearComponent(
+ objectToCanvasTransform: Matrix,
+ @Size(min = 4) objectToCanvasLinearComponent: FloatArray,
+ ) {
+ require(objectToCanvasTransform.isAffine) { "objectToCanvasTransform must be affine" }
+ objectToCanvasTransform.getValues(matrixValuesScratchArray)
+ objectToCanvasLinearComponent.let {
+ it[0] = matrixValuesScratchArray[Matrix.MSCALE_X]
+ it[1] = matrixValuesScratchArray[Matrix.MSKEW_Y]
+ it[2] = matrixValuesScratchArray[Matrix.MSKEW_X]
+ it[3] = matrixValuesScratchArray[Matrix.MSCALE_Y]
+ }
+ }
+
+ private fun obtainInProgressMeshData(
+ inProgressStroke: InProgressStroke,
+ coatIndex: Int,
+ ): InProgressMeshData {
+ val cachedMeshDatas = inProgressStrokeToAndroidMeshes[inProgressStroke]
+ if (
+ cachedMeshDatas != null && cachedMeshDatas.size == inProgressStroke.getBrushCoatCount()
+ ) {
+ val inProgressMeshData = cachedMeshDatas[coatIndex]
+ if (inProgressMeshData.version == inProgressStroke.version) {
+ return inProgressMeshData
+ }
+ }
+ val inProgressMeshDatas = computeInProgressMeshDatas(inProgressStroke)
+ inProgressStrokeToAndroidMeshes[inProgressStroke] = inProgressMeshDatas
+ return inProgressMeshDatas[coatIndex]
+ }
+
+ private fun computeInProgressMeshDatas(
+ inProgressStroke: InProgressStroke
+ ): List<InProgressMeshData> =
+ buildList() {
+ for (coatIndex in 0 until inProgressStroke.getBrushCoatCount()) {
+ val androidMeshes =
+ buildList() {
+ for (meshIndex in
+ 0 until inProgressStroke.getMeshPartitionCount(coatIndex)) {
+ add(createAndroidMesh(inProgressStroke, coatIndex, meshIndex))
+ }
+ }
+ add(InProgressMeshData(inProgressStroke.version, androidMeshes))
+ }
+ }
+
+ /**
+ * Create a new [AndroidMesh] for the unpacked mesh at [meshIndex] in brush coat [coatIndex] of
+ * the given [inProgressStroke].
+ */
+ @VisibleForTesting
+ internal fun createAndroidMesh(
+ inProgressStroke: InProgressStroke,
+ coatIndex: Int,
+ meshIndex: Int,
+ ): AndroidMesh? {
+ val vertexCount = inProgressStroke.getVertexCount(coatIndex, meshIndex)
+ if (vertexCount < 3) {
+ // Fail gracefully when mesh doesn't contain enough vertices for a full triangle.
+ return null
+ }
+ val bounds = BoxAccumulator().apply { inProgressStroke.populateMeshBounds(coatIndex, this) }
+ if (bounds.isEmpty()) return null // Empty mesh; nothing to render.
+ return AndroidMesh(
+ obtainShaderMetadata(
+ inProgressStroke.getMeshFormat(coatIndex, meshIndex),
+ isPacked = false
+ )
+ .meshSpecification,
+ AndroidMesh.TRIANGLES,
+ inProgressStroke.getRawVertexBuffer(coatIndex, meshIndex),
+ vertexCount,
+ inProgressStroke.getRawTriangleIndexBuffer(coatIndex, meshIndex),
+ bounds.box?.let { RectF(it.xMin, it.yMin, it.xMax, it.yMax) } ?: return null,
+ )
+ }
+
+ /**
+ * Returns a [ShaderMetadata] compatible with the [isPacked] state of the given [MeshFormat]'s
+ * vertex format. This may be newly created, or an internally cached value.
+ *
+ * This method manages read and write access to both [meshFormatToPackedShaderMetadata] and
+ * [meshFormatToUnpackedShaderMetadata]
+ */
+ @VisibleForTesting
+ internal fun obtainShaderMetadata(meshFormat: MeshFormat, isPacked: Boolean): ShaderMetadata {
+ val meshFromatToShaderMetaData =
+ if (isPacked) meshFormatToPackedShaderMetadata else meshFormatToUnpackedShaderMetadata
+ // Check the cache first.
+ return getCachedValue(meshFormat, meshFromatToShaderMetaData, isPacked)
+ ?: createShaderMetadata(meshFormat, isPacked).also {
+ // Populate the cache before returning the newly-created ShaderMetadata.
+ meshFromatToShaderMetaData.add(Pair(meshFormat, it))
+ }
+ }
+
+ /**
+ * Returns true when the [stroke]'s [inputs] are empty, or [MeshFormat] is compatible with the
+ * native Skia `MeshSpecificationData`.
+ */
+ internal fun canDraw(stroke: Stroke): Boolean {
+ for (groupIndex in 0 until stroke.shape.getRenderGroupCount()) {
+ if (stroke.shape.renderGroupMeshes(groupIndex).isEmpty()) continue
+ val format = stroke.shape.renderGroupFormat(groupIndex)
+ if (!nativeIsMeshFormatRenderable(format.getNativeAddress(), isPacked = true)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ private fun createShaderMetadata(meshFormat: MeshFormat, isPacked: Boolean): ShaderMetadata {
+ // Fill "out" parameter arrays with invalid data, to fail fast in case anything goes wrong.
+ val attributeTypesOut = IntArray(MAX_ATTRIBUTES) { Type.INVALID_NATIVE_VALUE }
+ val attributeOffsetsBytesOut = IntArray(MAX_ATTRIBUTES) { INVALID_OFFSET }
+ val attributeNamesOut = Array(MAX_ATTRIBUTES) { INVALID_NAME }
+ val vertexStrideBytesOut = intArrayOf(INVALID_VERTEX_STRIDE)
+ val varyingTypesOut = IntArray(MAX_VARYINGS) { Type.INVALID_NATIVE_VALUE }
+ val varyingNamesOut = Array(MAX_VARYINGS) { INVALID_NAME }
+ val uniformIdsOut = IntArray(MAX_UNIFORMS) { UniformId.INVALID_NATIVE_VALUE }
+ val uniformTypesOut = IntArray(MAX_UNIFORMS) { Type.INVALID_NATIVE_VALUE }
+ val uniformUnpackingIndicesOut = IntArray(MAX_UNIFORMS) { INVALID_ATTRIBUTE_INDEX }
+ val uniformNamesOut = Array(MAX_UNIFORMS) { INVALID_NAME }
+ val vertexShaderOut = arrayOf("unset vertex shader")
+ val fragmentShaderOut = arrayOf("unset fragment shader")
+ fillSkiaMeshSpecData(
+ meshFormat.getNativeAddress(),
+ isPacked,
+ attributeTypesOut,
+ attributeOffsetsBytesOut,
+ attributeNamesOut,
+ vertexStrideBytesOut,
+ varyingTypesOut,
+ varyingNamesOut,
+ uniformIdsOut,
+ uniformTypesOut,
+ uniformUnpackingIndicesOut,
+ uniformNamesOut,
+ vertexShaderOut,
+ fragmentShaderOut,
+ )
+ val attributes = mutableListOf<MeshSpecification.Attribute>()
+ for (attrIndex in 0 until MAX_ATTRIBUTES) {
+ val type = Type.fromNativeValue(attributeTypesOut[attrIndex]) ?: break
+ val offset = attributeOffsetsBytesOut[attrIndex]
+ val name = attributeNamesOut[attrIndex]
+ attributes.add(MeshSpecification.Attribute(type.meshSpecValue, offset, name))
+ }
+ val varyings = mutableListOf<MeshSpecification.Varying>()
+ for (varyingIndex in 0 until MAX_VARYINGS) {
+ val type = Type.fromNativeValue(varyingTypesOut[varyingIndex]) ?: break
+ val name = varyingNamesOut[varyingIndex]
+ varyings.add(MeshSpecification.Varying(type.meshSpecValue, name))
+ }
+ val uniforms = mutableListOf<UniformMetadata>()
+ for (uniformIndex in 0 until MAX_UNIFORMS) {
+ val id = UniformId.fromNativeValue(uniformIdsOut[uniformIndex]) ?: break
+ val type = Type.fromNativeValue(uniformTypesOut[uniformIndex]) ?: break
+ val name = uniformNamesOut[uniformIndex]
+ val attributeIndex = uniformUnpackingIndicesOut[uniformIndex]
+ uniforms.add(UniformMetadata(id, type, name, attributeIndex))
+ }
+
+ return ShaderMetadata(
+ meshSpecification =
+ MeshSpecification.make(
+ attributes.toTypedArray(),
+ validVertexStrideBytes(vertexStrideBytesOut[0]),
+ varyings.toTypedArray(),
+ vertexShaderOut[0],
+ fragmentShaderOut[0],
+ // The shaders output linear, premultiplied, non-clamped sRGB colors.
+ AndroidColorSpace.get(AndroidColorSpace.Named.LINEAR_EXTENDED_SRGB),
+ MeshSpecification.ALPHA_TYPE_PREMULTIPLIED,
+ ),
+ uniformMetadata = uniforms,
+ )
+ }
+
+ private fun validVertexStrideBytes(vertexStride: Int): Int {
+ // MeshSpecification.make is documented to accept a vertex stride between 1 and 1024 bytes
+ // (inclusive), but its only supported vertex attribute types are in multiples of 4 bytes,
+ // so
+ // its true lower bound is 4 bytes.
+ require(vertexStride in 4..1024)
+ return vertexStride
+ }
+
+ /**
+ * Retrieves data analogous to [MeshSpecification] from native code. It makes use of "out"
+ * parameters to return this data, as it is tedious (and therefore error-prone) to construct and
+ * return complex objects from JNI. These "out" parameters are all arrays, as those are well
+ * supported by JNI, especially primitive arrays.
+ *
+ * @param meshFormatNativeAddress The raw pointer address of a [MeshFormat].
+ * @param isPacked Whether to fill the mesh spec with properties describing a packed format (as
+ * in ink::Mesh) or an unpacked format (as in ink::MutableMesh).
+ * @param attributeTypesOut An array that can hold at least [MAX_ATTRIBUTES] values. It will
+ * contain the resulting attribute types aligning with [Type.nativeValue]. The number of
+ * attributes will be determined by the first index of this array with an invalid value, and
+ * that attribute count will determine the number of entries to look at in
+ * [attributeOffsetsBytesOut] and [attributeNamesOut]. See
+ * [MeshSpecification.Attribute.getType].
+ * @param attributeOffsetsBytesOut An array that can hold at least [MAX_ATTRIBUTES] values.
+ * Specifies the layout of each vertex of the raw data for a mesh, where each vertex is a
+ * contiguous chunk of memory and each attribute is located at a particular number of bytes
+ * (offset) from the beginning of that vertex's chunk of memory.
+ * @param attributeNamesOut The names of each attribute, referenced in the shader code.
+ * @param vertexStrideBytesOut In the raw data of the mesh vertices, the number of bytes between
+ * the start of each vertex. See [attributeOffsetsBytesOut] for how each attribute is laid
+ * out.
+ * @param varyingTypesOut An array that can hold at least [MAX_VARYINGS] values. It will contain
+ * the resulting varying types aligning with [Type.nativeValue]. The number of varyings will
+ * be determined by the first index of this array with an invalid value, and that varying
+ * count will determine the number of entries to look at in [varyingNamesOut]. See
+ * [MeshSpecification.Varying.getType].
+ * @param varyingNamesOut The names of each varying, referenced in the shader code.
+ * @param vertexShaderOut An array with at least one element that will be filled in by the
+ * string vertex shader code.
+ * @param fragmentShaderOut An array with at least one element that will be filled in by the
+ * string fragment shader code.
+ * @throws IllegalArgumentException If an unrecognized format was passed in, i.e. when
+ * [nativeIsMeshFormatRenderable] returns false.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun fillSkiaMeshSpecData(
+ meshFormatNativeAddress: Long,
+ isPacked: Boolean,
+ attributeTypesOut: IntArray,
+ attributeOffsetsBytesOut: IntArray,
+ attributeNamesOut: Array<String>,
+ vertexStrideBytesOut: IntArray,
+ varyingTypesOut: IntArray,
+ varyingNamesOut: Array<String>,
+ uniformIdsOut: IntArray,
+ uniformTypesOut: IntArray,
+ uniformUnpackingIndicesOut: IntArray,
+ uniformNamesOut: Array<String>,
+ vertexShaderOut: Array<String>,
+ fragmentShaderOut: Array<String>,
+ )
+
+ /**
+ * Constructs native [MeshFormat] from [meshFormatNativeAddress] and checks whether it is
+ * compatible with the native Skia `MeshSpecificationData`.
+ *
+ * @param isPacked checks whether [meshFormat] describes a packed format (as in native
+ * ink::Mesh) or an unpacked format (as in native ink::MutableMesh).
+ *
+ * [fillSkiaMeshSpecData] throws IllegalArgumentException when this method returns false.
+ */
+ // TODO: b/355248266 - @Keep must go in Proguard config file instead.
+ private external fun nativeIsMeshFormatRenderable(
+ meshFormatNativeAddress: Long,
+ isPacked: Boolean,
+ ): Boolean
+
+ private fun saveRecentlyDrawnAndroidMesh(androidMesh: AndroidMesh, currentTimeMillis: Long) {
+ recentlyDrawnMeshesToLastDrawTimeMillis[androidMesh] = currentTimeMillis
+ }
+
+ private fun cleanUpRecentlyDrawnAndroidMeshes(currentTimeMillis: Long) {
+ if (
+ recentlyDrawnMeshesLastCleanupTimeMillis + EVICTION_SCAN_PERIOD_MS > currentTimeMillis
+ ) {
+ return
+ }
+ recentlyDrawnMeshesToLastDrawTimeMillis.removeIf { _, lastDrawTimeMillis ->
+ lastDrawTimeMillis + MESH_STRONG_REFERENCE_DURATION_MS < currentTimeMillis
+ }
+ recentlyDrawnMeshesLastCleanupTimeMillis = currentTimeMillis
+ }
+
+ @VisibleForTesting
+ internal fun getRecentlyDrawnAndroidMeshesCount(): Int {
+ return recentlyDrawnMeshesToLastDrawTimeMillis.size
+ }
+
+ private fun ComposeColor.fillFloatArray(@Size(min = 4) outRgba: FloatArray) {
+ outRgba[0] = this.red
+ outRgba[1] = this.green
+ outRgba[2] = this.blue
+ outRgba[3] = this.alpha
+ }
+
+ private class MeshData
+ private constructor(
+ val androidMesh: AndroidMesh,
+ val brushColor: ComposeColor,
+ /** Do not modify! */
+ @Size(4) val objectToCanvasLinearComponent: FloatArray,
+ ) {
+
+ fun areUniformsEquivalent(
+ otherBrushColor: ComposeColor,
+ @Size(4) otherObjectToCanvasLinearComponent: FloatArray,
+ ): Boolean =
+ otherBrushColor == brushColor &&
+ otherObjectToCanvasLinearComponent.contentEquals(objectToCanvasLinearComponent)
+
+ companion object {
+ fun create(
+ androidMesh: AndroidMesh,
+ brushColor: ComposeColor,
+ @Size(4) objectToCanvasLinearComponent: FloatArray,
+ ): MeshData {
+ val copied = FloatArray(4)
+ System.arraycopy(
+ /* src = */ objectToCanvasLinearComponent,
+ /* srcPos = */ 0,
+ /* dest = */ copied,
+ /* destPos = */ 0,
+ /* length = */ 4,
+ )
+ return MeshData(androidMesh, brushColor, copied)
+ }
+ }
+ }
+
+ /**
+ * Contains the [android.graphics.Mesh] data for an [InProgressStroke], along with metadata used
+ * to verify if that data is still valid.
+ */
+ private data class InProgressMeshData(
+ /** If this does not match [InProgressStroke.version], the data is invalid. */
+ val version: Long,
+ /**
+ * At each index, the [android.graphics.Mesh] for the corresponding partition index of the
+ * [InProgressStroke], or `null` if that partition is empty.
+ */
+ val androidMeshes: List<AndroidMesh?>,
+ )
+
+ companion object {
+ init {
+ NativeLoader.load()
+ }
+
+ /**
+ * On Android U, how long to hold a reference to an [android.graphics.Mesh] after it has
+ * been drawn with [Canvas.drawMesh]. This is an imperfect workaround for a bug in the
+ * native layer where the render thread is not given ownership of the mesh data to prevent
+ * it from being freed before the render thread uses it for drawing.
+ */
+ private const val MESH_STRONG_REFERENCE_DURATION_MS = 5000
+ private const val EVICTION_SCAN_PERIOD_MS = 2000
+
+ /** All the metadata about values sent to the shader for a given mesh. Used for caching. */
+ internal data class ShaderMetadata(
+ val meshSpecification: MeshSpecification,
+ val uniformMetadata: List<UniformMetadata>,
+ )
+
+ internal data class UniformMetadata(
+ val id: UniformId,
+ val type: Type,
+ val name: String,
+ val unpackingAttributeIndex: Int,
+ )
+
+ internal enum class UniformId(val nativeValue: Int) {
+ /**
+ * The 2x2 linear component of the affine transformation from mesh / "object"
+ * coordinates to the canvas. This requires that the [meshToCanvasTransform] matrix used
+ * during drawing is an affine transform. Set it with [AndroidMesh.setFloatUniform]. It
+ * is a `float4` with the following expected entries:
+ * - `[0]`: `matrixValues[Matrix.MSCALE_X]`
+ * - `[1]`: `matrixValues[Matrix.MSKEW_X]`
+ * - `[2]`: `matrixValues[Matrix.MSKEW_Y]`
+ * - `[3]`: `matrixValues[Matrix.MSCALE_Y]`
+ */
+ OBJECT_TO_CANVAS_LINEAR_COMPONENT(0),
+
+ /**
+ * The [Color] of the Stroke's brush, which will be combined with per-vertex color
+ * shifts in the shaders. Set it with [AndroidMesh.setColorUniform]. Must be specified
+ * for every format.
+ */
+ BRUSH_COLOR(1),
+
+ /**
+ * The transform parameters to convert packed [InkMesh] coordinates into actual
+ * ("object") coordinates. Set it with [AndroidMesh.setFloatUniform]. Must be specified
+ * for packed meshes only. It is a `float4` with the following entries:
+ * - `[0]`: x offset
+ * - `[1]`: x scale
+ * - `[2]`: y offset
+ * - `[3]`: y scale
+ */
+ POSITION_UNPACKING_TRANSFORM(2),
+
+ /**
+ * The transform parameters to convert packed [InkMesh] side-derivative attribute values
+ * into their unpacked values. Set it with [AndroidMesh.setFloatUniform]. Must be
+ * specified for packed meshes only. It is a `float4` with the following entries:
+ * - `[0]`: x offset
+ * - `[1]`: x scale
+ * - `[2]`: y offset
+ * - `[3]`: y scale
+ */
+ SIDE_DERIVATIVE_UNPACKING_TRANSFORM(3),
+
+ /**
+ * The transform parameters to convert packed [InkMesh] forward-derivative attribute
+ * values into their unpacked values. Set it with [AndroidMesh.setFloatUniform]. Must be
+ * specified for packed meshes only. It is a `float4` with the following entries:
+ * - `[0]`: x offset
+ * - `[1]`: x scale
+ * - `[2]`: y offset
+ * - `[3]`: y scale
+ */
+ FORWARD_DERIVATIVE_UNPACKING_TRANSFORM(4);
+
+ companion object {
+ const val INVALID_NATIVE_VALUE = -1
+
+ fun fromNativeValue(nativeValue: Int): UniformId? {
+ for (type in UniformId.values()) {
+ if (type.nativeValue == nativeValue) return type
+ }
+ return null
+ }
+ }
+ }
+
+ private const val MAX_ATTRIBUTES = 8
+ private const val MAX_VARYINGS = 6
+ private const val MAX_UNIFORMS = 6
+
+ private const val INVALID_OFFSET = -1
+ private const val INVALID_VERTEX_STRIDE = -1
+ private const val INVALID_NAME = ")"
+ private const val INVALID_ATTRIBUTE_INDEX = -1
+
+ internal enum class Type(val nativeValue: Int, val meshSpecValue: Int) {
+ FLOAT(0, MeshSpecification.TYPE_FLOAT),
+ FLOAT2(1, MeshSpecification.TYPE_FLOAT2),
+ FLOAT3(2, MeshSpecification.TYPE_FLOAT3),
+ FLOAT4(3, MeshSpecification.TYPE_FLOAT4),
+ UBYTE4(4, MeshSpecification.TYPE_UBYTE4);
+
+ companion object {
+ const val INVALID_NATIVE_VALUE = -1
+
+ fun fromNativeValue(nativeValue: Int): Type? {
+ for (type in Type.values()) {
+ if (type.nativeValue == nativeValue) return type
+ }
+ return null
+ }
+ }
+ }
+
+ /**
+ * Returns the [T] associated with [key] in [cache]. If [isPacked] is false, keys are
+ * considered equivalent if their unpacked format is the same; if true, if their packed
+ * format is the same. This provides a map-like getter interface for a cache implemented as
+ * a list of key-value pairs. Returns `null` if no equivalent key is found.
+ */
+ private fun <T> getCachedValue(
+ key: MeshFormat,
+ cache: ArrayList<Pair<MeshFormat, T>>,
+ isPacked: Boolean,
+ ): T? {
+ for ((format, item) in cache) {
+ if (isPacked && format.isPackedEquivalent(key)) {
+ return item
+ } else if (!isPacked && format.isUnpackedEquivalent(key)) {
+ return item
+ }
+ }
+ return null
+ }
+
+ private fun finalBlendMode(brushPaint: BrushPaint): BlendMode =
+ brushPaint.textureLayers.lastOrNull()?.let { it.blendMode.toBlendMode() }
+ ?: BlendMode.MODULATE
+
+ private val MeshAttributeUnpackingParams.xOffset
+ get() = components[0].offset
+
+ private val MeshAttributeUnpackingParams.xScale
+ get() = components[0].scale
+
+ private val MeshAttributeUnpackingParams.yOffset
+ get() = components[1].offset
+
+ private val MeshAttributeUnpackingParams.yScale
+ get() = components[1].scale
+ }
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasPathRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasPathRenderer.kt
new file mode 100644
index 0000000..7f185a0
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasPathRenderer.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import androidx.annotation.FloatRange
+import androidx.ink.brush.BrushPaint
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.brush.color.Color as ComposeColor
+import androidx.ink.brush.color.toArgb
+import androidx.ink.geometry.AffineTransform
+import androidx.ink.geometry.MutableVec
+import androidx.ink.geometry.PartitionedMesh
+import androidx.ink.geometry.populateMatrix
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.Stroke
+import androidx.ink.strokes.StrokeInput
+import java.util.WeakHashMap
+
+/**
+ * Renders Ink objects using [Canvas.drawPath]. This is the best [Canvas] Ink renderer to use before
+ * [android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE] for both quality (anti-aliasing) and
+ * performance compared to a solution built on [Canvas.drawVertices], and even on higher OS versions
+ * when the desired behavior for self-intersection of translucent strokes is to discard the extra
+ * layers.
+ *
+ * This is not thread safe, so if it must be used from multiple threads, the caller is responsible
+ * for synchronizing access. If it is being used in two very different contexts where there are
+ * unlikely to be cached mesh data in common, the easiest solution to thread safety is to have two
+ * different instances of this object.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+internal class CanvasPathRenderer(
+ private val textureStore: TextureBitmapStore = TextureBitmapStore { null }
+) : CanvasStrokeRenderer {
+
+ /**
+ * Holds onto rendering data for each [PartitionedMesh] (the shape of a [Stroke]) so the data
+ * can be created once and then reused on each call to [draw]. The [WeakHashMap] ensures that
+ * this renderer does not hold onto [PartitionedMesh] instances that would otherwise be garbage
+ * collected.
+ */
+ private val strokePathCache = WeakHashMap<PartitionedMesh, List<Path>>()
+
+ /**
+ * Holds onto rendering data for each [InProgressStroke], so the data can be created once and
+ * then reused on each call to [draw]. Because [InProgressStroke] is mutable, this cache is
+ * based not just on the existence of data, but whether that data's version number matches that
+ * of the [InProgressStroke]. The [WeakHashMap] ensures that this renderer does not hold onto
+ * [InProgressStroke] instances that would otherwise be garbage collected.
+ */
+ private val inProgressStrokePathCache = WeakHashMap<InProgressStroke, InProgressPathData>()
+
+ private val paintCache =
+ BrushPaintCache(
+ textureStore,
+ additionalPaintFlags = Paint.ANTI_ALIAS_FLAG,
+ applyColorFilterToTexture = true,
+ )
+
+ private val scratchPoint = MutableVec()
+
+ /** Scratch [Matrix] used for draw calls taking an [AffineTransform]. */
+ private val scratchMatrix = Matrix()
+
+ // First and last inputs for the stroke being rendered, reused so that we don't need to allocate
+ // new ones for every stroke.
+ private val scratchFirstInput = StrokeInput()
+ private val scratchLastInput = StrokeInput()
+
+ private fun draw(
+ canvas: Canvas,
+ path: Path,
+ brushPaint: BrushPaint,
+ color: ComposeColor,
+ @FloatRange(from = 0.0) brushSize: Float,
+ firstInput: StrokeInput,
+ lastInput: StrokeInput,
+ strokeToCanvasTransform: Matrix,
+ ) {
+ val paint = paintCache.obtain(brushPaint, color.toArgb(), brushSize, firstInput, lastInput)
+ canvas.save()
+ try {
+ canvas.concat(strokeToCanvasTransform)
+ canvas.drawPath(path, paint)
+ } finally {
+ canvas.restore()
+ }
+ }
+
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: AffineTransform) {
+ strokeToCanvasTransform.populateMatrix(scratchMatrix)
+ draw(canvas, stroke, scratchMatrix)
+ }
+
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: Matrix) {
+ if (stroke.inputs.isEmpty()) return // nothing to draw
+ stroke.inputs.populate(0, scratchFirstInput)
+ stroke.inputs.populate(stroke.inputs.size - 1, scratchLastInput)
+ for (groupIndex in 0 until stroke.shape.getRenderGroupCount()) {
+ draw(
+ canvas,
+ obtainPath(stroke.shape, groupIndex),
+ stroke.brush.family.coats[groupIndex].paint,
+ stroke.brush.composeColor,
+ stroke.brush.size,
+ scratchFirstInput,
+ scratchLastInput,
+ strokeToCanvasTransform,
+ )
+ }
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: AffineTransform,
+ ) {
+ strokeToCanvasTransform.populateMatrix(scratchMatrix)
+ draw(canvas, inProgressStroke, scratchMatrix)
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: Matrix,
+ ) {
+ val brush =
+ checkNotNull(inProgressStroke.brush) {
+ "Attempting to draw an InProgressStroke that has not been started."
+ }
+ val inputCount = inProgressStroke.getInputCount()
+ if (inputCount == 0) return // nothing to draw
+ inProgressStroke.populateInput(scratchFirstInput, 0)
+ inProgressStroke.populateInput(scratchLastInput, inputCount - 1)
+ for (coatIndex in 0 until inProgressStroke.getBrushCoatCount()) {
+ draw(
+ canvas,
+ obtainPath(inProgressStroke, coatIndex),
+ brush.family.coats[coatIndex].paint,
+ brush.composeColor,
+ brush.size,
+ scratchFirstInput,
+ scratchLastInput,
+ strokeToCanvasTransform,
+ )
+ }
+ }
+
+ /**
+ * Obtain a [Path] for the specified render group of the given [PartitionedMesh], which may be
+ * cached or new.
+ */
+ private fun obtainPath(shape: PartitionedMesh, groupIndex: Int): Path {
+ val paths =
+ strokePathCache[shape] ?: createPaths(shape).also { strokePathCache[shape] = it }
+ return paths[groupIndex]
+ }
+
+ /** Create new [Path]s for the given [PartitionedMesh], one for each render group. */
+ private fun createPaths(shape: PartitionedMesh): List<Path> =
+ buildList() {
+ val point = MutableVec()
+ for (groupIndex in 0 until shape.getRenderGroupCount()) {
+ val path = Path()
+ for (outlineIndex in 0 until shape.getOutlineCount(groupIndex)) {
+ val outlineVertexCount = shape.getOutlineVertexCount(groupIndex, outlineIndex)
+ if (outlineVertexCount == 0) continue
+
+ shape.populateOutlinePosition(groupIndex, outlineIndex, 0, point)
+ path.moveTo(point.x, point.y)
+
+ for (outlineVertexIndex in 1 until outlineVertexCount) {
+ shape.populateOutlinePosition(
+ groupIndex,
+ outlineIndex,
+ outlineVertexIndex,
+ point
+ )
+ path.lineTo(point.x, point.y)
+ }
+
+ path.close()
+ }
+ add(path)
+ }
+ }
+
+ /**
+ * Obtain a [Path] for brush coat [coatIndex] of the given [InProgressStroke], which may be
+ * cached or new.
+ */
+ private fun obtainPath(inProgressStroke: InProgressStroke, coatIndex: Int): Path {
+ val cachedPathData = inProgressStrokePathCache[inProgressStroke]
+ if (cachedPathData != null && cachedPathData.version == inProgressStroke.version) {
+ return cachedPathData.paths[coatIndex]
+ }
+ val inProgressPathData = computeInProgressPathData(inProgressStroke)
+ inProgressStrokePathCache[inProgressStroke] = inProgressPathData
+ return inProgressPathData.paths[coatIndex]
+ }
+
+ private fun computeInProgressPathData(inProgressStroke: InProgressStroke): InProgressPathData {
+ val paths =
+ buildList() {
+ for (coatIndex in 0 until inProgressStroke.getBrushCoatCount()) {
+ val path = Path()
+ path.fillFrom(inProgressStroke, coatIndex)
+ add(path)
+ }
+ }
+ return InProgressPathData(inProgressStroke.version, paths)
+ }
+
+ /** Create a new [Path] for the given [InProgressStroke]. */
+ private fun Path.fillFrom(inProgressStroke: InProgressStroke, coatIndex: Int) {
+ rewind()
+ for (outlineIndex in 0 until inProgressStroke.getOutlineCount(coatIndex)) {
+ val outlineVertexCount = inProgressStroke.getOutlineVertexCount(coatIndex, outlineIndex)
+ if (outlineVertexCount == 0) continue
+
+ inProgressStroke.populateOutlinePosition(
+ coatIndex,
+ outlineIndex,
+ outlineVertexIndex = 0,
+ scratchPoint,
+ )
+ moveTo(scratchPoint.x, scratchPoint.y)
+
+ for (outlineVertexIndex in 1 until outlineVertexCount) {
+ inProgressStroke.populateOutlinePosition(
+ coatIndex,
+ outlineIndex,
+ outlineVertexIndex,
+ scratchPoint,
+ )
+ lineTo(scratchPoint.x, scratchPoint.y)
+ }
+
+ close()
+ }
+ }
+
+ /**
+ * A snapshot of the outline(s) of [InProgressStroke] at a particular
+ * [InProgressStroke.version], with one [Path] object for each brush coat.
+ */
+ private class InProgressPathData(val version: Long, val paths: List<Path>)
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt
new file mode 100644
index 0000000..62fc86c
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/canvas/internal/CanvasStrokeUnifiedRenderer.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.canvas.internal
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.os.Build
+import androidx.ink.brush.ExperimentalInkCustomBrushApi
+import androidx.ink.geometry.AffineTransform
+import androidx.ink.rendering.android.TextureBitmapStore
+import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
+import androidx.ink.strokes.InProgressStroke
+import androidx.ink.strokes.Stroke
+
+/**
+ * Renders Ink objects using [CanvasMeshRenderer], but falls back to using [CanvasPathRenderer] when
+ * mesh rendering is not possible.
+ */
+@OptIn(ExperimentalInkCustomBrushApi::class)
+internal class CanvasStrokeUnifiedRenderer(
+ private val textureStore: TextureBitmapStore = TextureBitmapStore { null }
+) : CanvasStrokeRenderer {
+
+ private val meshRenderer by lazy {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ CanvasMeshRenderer(textureStore)
+ } else {
+ null
+ }
+ }
+ private val pathRenderer by lazy { CanvasPathRenderer(textureStore) }
+
+ private fun getDelegateRendererOrThrow(stroke: Stroke): CanvasStrokeRenderer {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val renderer = checkNotNull(meshRenderer)
+ if (renderer.canDraw(stroke)) {
+ return renderer
+ }
+ }
+ for (groupIndex in 0 until stroke.shape.getRenderGroupCount()) {
+ if (stroke.shape.getOutlineCount(groupIndex) > 0) {
+ return pathRenderer
+ }
+ }
+ throw IllegalArgumentException("Cannot draw $stroke")
+ }
+
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: AffineTransform) {
+ getDelegateRendererOrThrow(stroke).draw(canvas, stroke, strokeToCanvasTransform)
+ }
+
+ override fun draw(canvas: Canvas, stroke: Stroke, strokeToCanvasTransform: Matrix) {
+ getDelegateRendererOrThrow(stroke).draw(canvas, stroke, strokeToCanvasTransform)
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: AffineTransform,
+ ) {
+ val delegateRenderer = meshRenderer ?: pathRenderer
+ delegateRenderer.draw(canvas, inProgressStroke, strokeToCanvasTransform)
+ }
+
+ override fun draw(
+ canvas: Canvas,
+ inProgressStroke: InProgressStroke,
+ strokeToCanvasTransform: Matrix,
+ ) {
+ val delegateRenderer = meshRenderer ?: pathRenderer
+ delegateRenderer.draw(canvas, inProgressStroke, strokeToCanvasTransform)
+ }
+}
diff --git a/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/view/ViewStrokeRenderer.kt b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/view/ViewStrokeRenderer.kt
new file mode 100644
index 0000000..77df307
--- /dev/null
+++ b/ink/ink-rendering/src/androidMain/kotlin/androidx/ink/rendering/android/view/ViewStrokeRenderer.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ink.rendering.android.view
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.os.Build
+import android.view.View
+import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
+import androidx.ink.rendering.android.canvas.StrokeDrawScope
+
+/**
+ * Helps developers using Android Views to draw [androidx.ink.strokes.Stroke] objects in their UI,
+ * in an easier way than using [CanvasStrokeRenderer] directly. Construct this once for your [View]
+ * and reuse it during each [View.onDraw] call.
+ *
+ * This utility is valid as long as [View.onDraw]
+ * 1. Does not call [Canvas.setMatrix].
+ * 2. Does not modify [Canvas] transform state prior to calling [drawWithStrokes].
+ * 3. Does not use [android.graphics.RenderEffect], either setting it on this [View] or a subview
+ * using [View.setRenderEffect], or by calling [Canvas.drawRenderNode] using a
+ * [android.graphics.RenderNode] that has been configured with
+ * [android.graphics.RenderNode.setRenderEffect]. Developers who want to use
+ * [android.graphics.RenderEffect] in conjunction with [androidx.ink.strokes.Stroke] rendering
+ * must use [CanvasStrokeRenderer.draw] directly.
+ *
+ * Example:
+ * ```
+ * class MyView(context: Context) : View(context) {
+ * private val viewStrokeRenderer = ViewStrokeRenderer(myCanvasStrokeRenderer, this)
+ *
+ * override fun onDraw(canvas: Canvas) {
+ * viewStrokeRenderer.drawWithStrokes(canvas) { scope ->
+ * canvas.scale(myZoomLevel)
+ * canvas.rotate(myRotation)
+ * canvas.translate(myPanX, myPanY)
+ * scope.drawStroke(myStroke)
+ * // Draw other objects including more strokes, apply more transformations, etc.
+ * }
+ * }
+ * }
+ * ```
+ */
+public class ViewStrokeRenderer(
+ private val canvasStrokeRenderer: CanvasStrokeRenderer,
+ private val view: View,
+) {
+
+ private val scratchMatrix = Matrix()
+ private val recycledDrawScopes = mutableListOf<StrokeDrawScope>()
+
+ /**
+ * Kotlin developers should call this at the beginning of [View.onDraw] and perform their
+ * [Canvas] manipulations within its scope.
+ *
+ * For example:
+ * ```
+ * viewStrokeRenderer.drawWithStrokes(canvas) { scope ->
+ * canvas.scale(...) // or concat, or translate, or rotate, etc.
+ * scope.drawStroke(stroke)
+ * // Repeat with other strokes, draw other things to the canvas, etc.
+ * }
+ * ```
+ *
+ * Java callers should prefer to use the non-inline overload of [drawWithStrokes] with a
+ * non-capturing lambda or an object that is allocated once and reused.
+ */
+ public inline fun drawWithStrokes(canvas: Canvas, block: (StrokeDrawScope) -> Unit) {
+ val scope = obtainDrawScope(canvas)
+ block(scope)
+ recycleDrawScope(scope)
+ }
+
+ /**
+ * Java developers should call this at the beginning of [View.onDraw] and perform their [Canvas]
+ * manipulations within its scope.
+ *
+ * The structure of the callback in this non-inline version makes it easier for Java callers to
+ * write performant code, since forwarding the value of [canvas] allows a lambda to be
+ * non-capturing, thereby avoiding an allocation of the lambda on every frame.
+ *
+ * For example:
+ * ```java
+ * viewStrokeRenderer.drawWithStrokes(canvas, (scopedCanvas, scope) -> {
+ * // Make sure to use `scopedCanvas` rather than `canvas` in this lambda!
+ * scopedCanvas.scale(...); // or concat, or translate, or rotate, etc.
+ * scope.drawStroke(stroke);
+ * // Repeat with other strokes, draw other things to the canvas, etc.
+ * });
+ * ```
+ *
+ * Alternatively, the callback could be an object that is allocated once and reused.
+ *
+ * Kotlin callers should prefer to use the inline overload of [drawWithStrokes], as it better
+ * guarantees that the lambda argument will not cause an allocation.
+ */
+ public fun drawWithStrokes(
+ canvas: Canvas,
+ block: (scopedCanvas: Canvas, StrokeDrawScope) -> Unit
+ ) {
+ val scope = obtainDrawScope(canvas)
+ block(canvas, scope)
+ recycleDrawScope(scope)
+ }
+
+ /**
+ * Manually obtain a scope to draw into the given [canvas]. Make sure to call [recycleDrawScope]
+ * when finished drawing. Prefer to use the public [drawWithStrokes] function instead.
+ */
+ @PublishedApi
+ internal fun obtainDrawScope(canvas: Canvas): StrokeDrawScope {
+ val viewToScreenTransform =
+ scratchMatrix.also {
+ it.reset()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ view.transformMatrixToGlobal(it)
+ } else {
+ transformMatrixToGlobalFallback(view, it)
+ }
+ }
+ require(viewToScreenTransform.isAffine) { "View to screen transform must be affine." }
+ val scope = recycledDrawScopes.removeFirstOrNull() ?: StrokeDrawScope(canvasStrokeRenderer)
+ scope.onDrawStart(viewToScreenTransform, canvas)
+ return scope
+ }
+
+ /**
+ * Recycle a [scope] for future use, that was previously obtained with [obtainDrawScope]. Prefer
+ * to use the public [drawWithStrokes] function instead.
+ */
+ @PublishedApi
+ internal fun recycleDrawScope(scope: StrokeDrawScope) {
+ recycledDrawScopes.add(scope)
+ }
+}
+
+/**
+ * Modify [matrix] such that it maps from view-local to on-screen coordinates when
+ * [View.transformMatrixToGlobal] is not available.
+ */
+private fun transformMatrixToGlobalFallback(view: View, matrix: Matrix) {
+ (view.parent as? View)?.let {
+ transformMatrixToGlobalFallback(it, matrix)
+ matrix.preTranslate(-it.scrollX.toFloat(), -it.scrollY.toFloat())
+ }
+ matrix.preTranslate(view.left.toFloat(), view.top.toFloat())
+ matrix.preConcat(view.matrix)
+}
diff --git a/ink/ink-strokes/api/current.txt b/ink/ink-strokes/api/current.txt
index e6f50d0..a81e872 100644
--- a/ink/ink-strokes/api/current.txt
+++ b/ink/ink-strokes/api/current.txt
@@ -1 +1,157 @@
// Signature format: 4.0
+package androidx.ink.strokes {
+
+ public final class ImmutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch.Companion Companion;
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch EMPTY;
+ }
+
+ public static final class ImmutableStrokeInputBatch.Companion {
+ }
+
+ public final class InProgressStroke {
+ ctor public InProgressStroke();
+ method public Object enqueueInputs(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method public void enqueueInputsOrThrow(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method protected void finalize();
+ method public void finishInput();
+ method public androidx.ink.brush.Brush? getBrush();
+ method @IntRange(from=0L) public int getBrushCoatCount();
+ method @IntRange(from=0L) public int getInputCount();
+ method public boolean getNeedsUpdate();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int coatIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getPredictedInputCount();
+ method @IntRange(from=0L) public int getRealInputCount();
+ method public boolean isInputFinished();
+ method public androidx.ink.strokes.StrokeInput populateInput(androidx.ink.strokes.StrokeInput out, @IntRange(from=0L) int index);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from, optional @IntRange(from=0L) int to);
+ method public androidx.ink.geometry.BoxAccumulator populateMeshBounds(@IntRange(from=0L) int coatIndex, androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void populateOutlinePosition(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ method public androidx.ink.geometry.BoxAccumulator populateUpdatedRegion(androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void resetUpdatedRegion();
+ method public void start(androidx.ink.brush.Brush brush);
+ method public androidx.ink.strokes.Stroke toImmutable();
+ method public Object updateShape(optional long currentElapsedTimeMillis);
+ method public void updateShapeOrThrow(optional long currentElapsedTimeMillis);
+ property public final androidx.ink.brush.Brush? brush;
+ field public static final androidx.ink.strokes.InProgressStroke.Companion Companion;
+ }
+
+ public static final class InProgressStroke.Companion {
+ }
+
+ public final class MutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ ctor public MutableStrokeInputBatch();
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public void clear();
+ }
+
+ public final class Stroke {
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs);
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.geometry.PartitionedMesh shape);
+ method public androidx.ink.strokes.Stroke copy(androidx.ink.brush.Brush brush);
+ method protected void finalize();
+ method public androidx.ink.brush.Brush getBrush();
+ method public androidx.ink.strokes.ImmutableStrokeInputBatch getInputs();
+ method public androidx.ink.geometry.PartitionedMesh getShape();
+ property public final androidx.ink.brush.Brush brush;
+ property public final androidx.ink.strokes.ImmutableStrokeInputBatch inputs;
+ property public final androidx.ink.geometry.PartitionedMesh shape;
+ field public static final androidx.ink.strokes.Stroke.Companion Companion;
+ }
+
+ public static final class Stroke.Companion {
+ }
+
+ public final class StrokeInput {
+ ctor public StrokeInput();
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public long getElapsedTimeMillis();
+ method public float getOrientationRadians();
+ method public float getPressure();
+ method public float getStrokeUnitLengthCm();
+ method public float getTiltRadians();
+ method public androidx.ink.brush.InputToolType getToolType();
+ method public float getX();
+ method public float getY();
+ method public boolean hasOrientation();
+ method public boolean hasPressure();
+ method public boolean hasTilt();
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ property public final long elapsedTimeMillis;
+ property public final boolean hasOrientation;
+ property public final boolean hasPressure;
+ property public final boolean hasTilt;
+ property public final float orientationRadians;
+ property public final float pressure;
+ property public final float strokeUnitLengthCm;
+ property public final float tiltRadians;
+ property public final androidx.ink.brush.InputToolType toolType;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.ink.strokes.StrokeInput.Companion Companion;
+ field public static final float NO_ORIENTATION = -1.0f;
+ field public static final float NO_PRESSURE = -1.0f;
+ field public static final float NO_STROKE_UNIT_LENGTH = 0.0f;
+ field public static final float NO_TILT = -1.0f;
+ }
+
+ public static final class StrokeInput.Companion {
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ }
+
+ public abstract class StrokeInputBatch {
+ method protected final void finalize();
+ method public final operator androidx.ink.strokes.StrokeInput get(int index);
+ method public final long getDurationMillis();
+ method public final int getSize();
+ method public final float getStrokeUnitLengthCm();
+ method public final androidx.ink.brush.InputToolType getToolType();
+ method public final boolean hasOrientation();
+ method public final boolean hasPressure();
+ method public final boolean hasStrokeUnitLength();
+ method public final boolean hasTilt();
+ method public final boolean isEmpty();
+ method public final androidx.ink.strokes.StrokeInput populate(int index, androidx.ink.strokes.StrokeInput outStrokeInput);
+ property public final int size;
+ field public static final androidx.ink.strokes.StrokeInputBatch.Companion Companion;
+ }
+
+ public static final class StrokeInputBatch.Companion {
+ }
+
+}
+
diff --git a/ink/ink-strokes/api/restricted_current.txt b/ink/ink-strokes/api/restricted_current.txt
index e6f50d0..a81e872 100644
--- a/ink/ink-strokes/api/restricted_current.txt
+++ b/ink/ink-strokes/api/restricted_current.txt
@@ -1 +1,157 @@
// Signature format: 4.0
+package androidx.ink.strokes {
+
+ public final class ImmutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch.Companion Companion;
+ field public static final androidx.ink.strokes.ImmutableStrokeInputBatch EMPTY;
+ }
+
+ public static final class ImmutableStrokeInputBatch.Companion {
+ }
+
+ public final class InProgressStroke {
+ ctor public InProgressStroke();
+ method public Object enqueueInputs(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method public void enqueueInputsOrThrow(androidx.ink.strokes.StrokeInputBatch realInputs, androidx.ink.strokes.StrokeInputBatch predictedInputs);
+ method protected void finalize();
+ method public void finishInput();
+ method public androidx.ink.brush.Brush? getBrush();
+ method @IntRange(from=0L) public int getBrushCoatCount();
+ method @IntRange(from=0L) public int getInputCount();
+ method public boolean getNeedsUpdate();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int coatIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getPredictedInputCount();
+ method @IntRange(from=0L) public int getRealInputCount();
+ method public boolean isInputFinished();
+ method public androidx.ink.strokes.StrokeInput populateInput(androidx.ink.strokes.StrokeInput out, @IntRange(from=0L) int index);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from);
+ method public androidx.ink.strokes.MutableStrokeInputBatch populateInputs(androidx.ink.strokes.MutableStrokeInputBatch out, optional @IntRange(from=0L) int from, optional @IntRange(from=0L) int to);
+ method public androidx.ink.geometry.BoxAccumulator populateMeshBounds(@IntRange(from=0L) int coatIndex, androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void populateOutlinePosition(@IntRange(from=0L) int coatIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ method public androidx.ink.geometry.BoxAccumulator populateUpdatedRegion(androidx.ink.geometry.BoxAccumulator outBoxAccumulator);
+ method public void resetUpdatedRegion();
+ method public void start(androidx.ink.brush.Brush brush);
+ method public androidx.ink.strokes.Stroke toImmutable();
+ method public Object updateShape(optional long currentElapsedTimeMillis);
+ method public void updateShapeOrThrow(optional long currentElapsedTimeMillis);
+ property public final androidx.ink.brush.Brush? brush;
+ field public static final androidx.ink.strokes.InProgressStroke.Companion Companion;
+ }
+
+ public static final class InProgressStroke.Companion {
+ }
+
+ public final class MutableStrokeInputBatch extends androidx.ink.strokes.StrokeInputBatch {
+ ctor public MutableStrokeInputBatch();
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrIgnore(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.brush.InputToolType type, float x, float y, long elapsedTimeMillis, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInput input);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(androidx.ink.strokes.StrokeInputBatch inputBatch);
+ method public androidx.ink.strokes.MutableStrokeInputBatch addOrThrow(java.util.Collection<androidx.ink.strokes.StrokeInput> inputs);
+ method public void clear();
+ }
+
+ public final class Stroke {
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs);
+ ctor public Stroke(androidx.ink.brush.Brush brush, androidx.ink.strokes.StrokeInputBatch inputs, androidx.ink.geometry.PartitionedMesh shape);
+ method public androidx.ink.strokes.Stroke copy(androidx.ink.brush.Brush brush);
+ method protected void finalize();
+ method public androidx.ink.brush.Brush getBrush();
+ method public androidx.ink.strokes.ImmutableStrokeInputBatch getInputs();
+ method public androidx.ink.geometry.PartitionedMesh getShape();
+ property public final androidx.ink.brush.Brush brush;
+ property public final androidx.ink.strokes.ImmutableStrokeInputBatch inputs;
+ property public final androidx.ink.geometry.PartitionedMesh shape;
+ field public static final androidx.ink.strokes.Stroke.Companion Companion;
+ }
+
+ public static final class Stroke.Companion {
+ }
+
+ public final class StrokeInput {
+ ctor public StrokeInput();
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public static androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ method public long getElapsedTimeMillis();
+ method public float getOrientationRadians();
+ method public float getPressure();
+ method public float getStrokeUnitLengthCm();
+ method public float getTiltRadians();
+ method public androidx.ink.brush.InputToolType getToolType();
+ method public float getX();
+ method public float getY();
+ method public boolean hasOrientation();
+ method public boolean hasPressure();
+ method public boolean hasTilt();
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method public void update(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ property public final long elapsedTimeMillis;
+ property public final boolean hasOrientation;
+ property public final boolean hasPressure;
+ property public final boolean hasTilt;
+ property public final float orientationRadians;
+ property public final float pressure;
+ property public final float strokeUnitLengthCm;
+ property public final float tiltRadians;
+ property public final androidx.ink.brush.InputToolType toolType;
+ property public final float x;
+ property public final float y;
+ field public static final androidx.ink.strokes.StrokeInput.Companion Companion;
+ field public static final float NO_ORIENTATION = -1.0f;
+ field public static final float NO_PRESSURE = -1.0f;
+ field public static final float NO_STROKE_UNIT_LENGTH = 0.0f;
+ field public static final float NO_TILT = -1.0f;
+ }
+
+ public static final class StrokeInput.Companion {
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians);
+ method @VisibleForTesting public androidx.ink.strokes.StrokeInput create(float x, float y, @IntRange(from=0L) long elapsedTimeMillis, optional androidx.ink.brush.InputToolType toolType, optional float strokeUnitLengthCm, optional float pressure, optional float tiltRadians, optional float orientationRadians);
+ }
+
+ public abstract class StrokeInputBatch {
+ method protected final void finalize();
+ method public final operator androidx.ink.strokes.StrokeInput get(int index);
+ method public final long getDurationMillis();
+ method public final int getSize();
+ method public final float getStrokeUnitLengthCm();
+ method public final androidx.ink.brush.InputToolType getToolType();
+ method public final boolean hasOrientation();
+ method public final boolean hasPressure();
+ method public final boolean hasStrokeUnitLength();
+ method public final boolean hasTilt();
+ method public final boolean isEmpty();
+ method public final androidx.ink.strokes.StrokeInput populate(int index, androidx.ink.strokes.StrokeInput outStrokeInput);
+ property public final int size;
+ field public static final androidx.ink.strokes.StrokeInputBatch.Companion Companion;
+ }
+
+ public static final class StrokeInputBatch.Companion {
+ }
+
+}
+
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
index 6bfd24e..7af48ff 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/InProgressStroke.kt
@@ -47,7 +47,6 @@
* 6. For best performance, reuse this object and go back to step 1 rather than allocating a new
* instance.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class InProgressStroke {
@@ -208,6 +207,8 @@
/**
* Add the specified range of inputs from this stroke to the output [MutableStrokeInputBatch].
+ *
+ * @return [out]
*/
@JvmOverloads
public fun populateInputs(
@@ -225,6 +226,8 @@
/**
* Gets the value of the i-th input and overwrites [out]. Requires that [index] is positive and
* less than [getInputCount].
+ *
+ * @return [out]
*/
public fun populateInput(out: StrokeInput, @IntRange(from = 0) index: Int): StrokeInput {
val size = getInputCount()
@@ -251,11 +254,13 @@
* Writes to [outBoxAccumulator] the bounding box of the vertex positions of the mesh for brush
* coat [coatIndex].
*
+ * @param coatIndex The index of the coat to obtain the bounding box from.
* @param outBoxAccumulator The pre-allocated [BoxAccumulator] to be filled with the result.
+ * @return [outBoxAccumulator]
*/
public fun populateMeshBounds(
@IntRange(from = 0) coatIndex: Int,
- outBoxAccumulator: BoxAccumulator
+ outBoxAccumulator: BoxAccumulator,
): BoxAccumulator {
require(coatIndex >= 0 && coatIndex < getBrushCoatCount()) {
"coatIndex=$coatIndex must be between 0 and brushCoatCount=${getBrushCoatCount()}"
@@ -269,6 +274,7 @@
* [updateShape] since the most recent call to [start] or [resetUpdatedRegion].
*
* @param outBoxAccumulator The pre-allocated [BoxAccumulator] to be filled with the result.
+ * @return [outBoxAccumulator]
*/
public fun populateUpdatedRegion(outBoxAccumulator: BoxAccumulator): BoxAccumulator {
nativeFillUpdatedRegion(nativePointer, outBoxAccumulator)
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..72c67e7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -33,7 +33,6 @@
* [InProgressStrokesView] or [InProgressStroke], which will ultimately return a [Stroke] when input
* is completed.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@OptIn(ExperimentalInkCustomBrushApi::class)
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class Stroke {
@@ -125,8 +124,8 @@
*/
internal constructor(nativeAddress: Long, brush: Brush) {
val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.nativeAddress = nativeAddress
this.brush = brush
@@ -142,8 +141,8 @@
* [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
*/
public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.brush = brush
this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
index f8abb1e..e3cd9ad 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInput.kt
@@ -17,7 +17,6 @@
package androidx.ink.strokes
import androidx.annotation.IntRange
-import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.ink.brush.InputToolType
@@ -31,7 +30,6 @@
* ones which could introduce unpredictable garbage collection related delays to the time-sensitive
* input path. This class has the [update] method for that purpose, rather than being immutable.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class StrokeInput {
/** The x-coordinate of the input position in stroke space. */
public var x: Float = 0F
@@ -136,13 +134,20 @@
get() = orientationRadians != NO_ORIENTATION
/**
- * Overwrite this instance with new values.
+ * Set new values on this instance, clearing values corresponding to optional parameters that
+ * are not specified.
*
* @param x The `x` position coordinate of the input in the stroke's coordinate space.
* @param y The `y` position coordinate of the input in the stroke's coordinate space.
* @param elapsedTimeMillis Marks the number of milliseconds since the stroke started. It is a
* non-negative timestamp in the [android.os.SystemClock.elapsedRealtime] time base.
* @param toolType The type of tool used to create this input data.
+ * @param strokeUnitLengthCm The physical distance in centimeters that the pointer must travel
+ * in order to produce an input motion of one stroke unit. For stylus/touch, this is the
+ * real-world distance that the stylus/fingertip must move in physical space; for mouse, this
+ * is the visual distance that the mouse pointer must travel along the surface of the display.
+ * A value of [NO_STROKE_UNIT_LENGTH] indicates that the relationship between stroke space and
+ * physical space is unknown or ill-defined.
* @param pressure Should be within [0, 1] but it's not enforced until added to a
* [StrokeInputBatch] object. Absence of [pressure] data is represented with [NO_PRESSURE].
* @param tiltRadians The angle in radians between a stylus and the line perpendicular to the
@@ -153,6 +158,8 @@
* end is along positive x and values increase towards the positive y-axis. Absence of
* [orientationRadians] data is represented with [NO_ORIENTATION].
*/
+ // TODO: b/355248266 - @UsedByNative("stroke_input_jni_helper.cc") must go in Proguard config
+ // file instead.
@JvmOverloads
public fun update(
x: Float,
@@ -174,35 +181,6 @@
this.orientationRadians = orientationRadians
}
- /** @see update */
- // TODO: b/362469375 - Change JNI to use `update` and delete `overwrite`
- // TODO: b/355248266 - @UsedByNative("stroke_input_jni_helper.cc") must go in Proguard config
- // file instead.
- @JvmOverloads
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
- @Deprecated("Renaming to update")
- public fun overwrite(
- x: Float,
- y: Float,
- @IntRange(from = 0) elapsedTimeMillis: Long,
- toolType: InputToolType = InputToolType.UNKNOWN,
- strokeUnitLengthCm: Float = NO_STROKE_UNIT_LENGTH,
- pressure: Float = NO_PRESSURE,
- tiltRadians: Float = NO_TILT,
- orientationRadians: Float = NO_ORIENTATION,
- ) {
- update(
- x,
- y,
- elapsedTimeMillis,
- toolType,
- strokeUnitLengthCm,
- pressure,
- tiltRadians,
- orientationRadians
- )
- }
-
public override fun equals(other: Any?): Boolean {
// NOMUTANTS -- Check the instance first to short circuit faster.
if (this === other) return true
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
index 4f4e2a8..5af5cc5 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/StrokeInputBatch.kt
@@ -29,7 +29,6 @@
* change, and a [MutableStrokeInputBatch] for data that is meant to be modified or incrementally
* built.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public abstract class StrokeInputBatch internal constructor(nativePointer: Long) {
@@ -100,8 +99,8 @@
public operator fun get(index: Int): StrokeInput = populate(index, StrokeInput())
/**
- * Gets the value of the i-th input and overwrites [outStrokeInput]. Requires that [index] is
- * positive and less than [size]. Returns [outStrokeInput].
+ * Gets the value of the i-th input and overwrites [outStrokeInput], which it then returns.
+ * Requires that [index] is positive and less than [size].
*/
public fun populate(index: Int, outStrokeInput: StrokeInput): StrokeInput {
require(index < size && index >= 0) { "index ($index) must be in [0, size=$size)" }
@@ -132,7 +131,6 @@
* An immutable implementation of [StrokeInputBatch]. For a mutable alternative, see
* [MutableStrokeInputBatch].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class ImmutableStrokeInputBatch
/**
* Constructor for Kotlin [ImmutableStrokeInputBatch] objects that are originally created in native
@@ -177,7 +175,6 @@
* [0, 2π) or be [StrokeInput.NO_ORIENTATION].
* 7) The [toolType] and [strokeUnitLengthCm] values must be the same across all inputs.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public class MutableStrokeInputBatch : StrokeInputBatch(StrokeInputBatchNative.createNativePeer()) {
public fun clear(): Unit = MutableStrokeInputBatchNative.clear(nativePointer)
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/InProgressStrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/InProgressStrokeTest.kt
index 0592f8c..ca3a16e 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/InProgressStrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/InProgressStrokeTest.kt
@@ -517,10 +517,6 @@
val triangleIndexBuffer = stroke.getRawTriangleIndexBuffer(0, 0)
- // TODO: b/302535371 - Make this buffer read only
- // assertThat(triangleIndexBuffer.isDirect).isTrue()
- // assertThat(triangleIndexBuffer.isReadOnly).isTrue()
- // assertFailsWith<ReadOnlyBufferException> { triangleIndexBuffer.put(5) }
assertThat(triangleIndexBuffer.limit()).isEqualTo(0)
assertThat(triangleIndexBuffer.capacity()).isEqualTo(0)
}
@@ -533,10 +529,6 @@
val triangleIndexBuffer = stroke.getRawTriangleIndexBuffer(0, 0)
- // TODO: b/302535371 - Make this buffer read only
- // assertThat(triangleIndexBuffer.isDirect).isTrue()
- // assertThat(triangleIndexBuffer.isReadOnly).isTrue()
- // assertFailsWith<ReadOnlyBufferException> { triangleIndexBuffer.put(5) }
assertThat(triangleIndexBuffer.limit()).isNotEqualTo(0)
assertThat(triangleIndexBuffer.capacity()).isNotEqualTo(0)
}
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeInputTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeInputTest.kt
index 15a2f7e..1a1be43 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeInputTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeInputTest.kt
@@ -45,7 +45,7 @@
}
@Test
- fun overwrite_shouldReassignValues() {
+ fun update_shouldReassignValues() {
val input = StrokeInput()
input.update(1f, 2f, 3L, InputToolType.TOUCH)
input.update(2f, 3f, 4L, InputToolType.STYLUS, 0.1f, 0.2f, 0.3f, 0.4f)
@@ -61,7 +61,7 @@
}
@Test
- fun overwrite_withDefaultValues_shouldReassignValues() {
+ fun update_withDefaultValues_shouldReassignValues() {
val input = StrokeInput()
input.update(
x = 1f,
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
// Create a [ModeledShape] with render group.
val inputs = makeTestInputs()
val shape = Stroke(buildTestBrush(), inputs).shape
- assertThat(shape.renderGroupCount).isEqualTo(1)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
// Create a brush with two brush coats.
val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/inspection/inspection-gradle-plugin/lint-baseline.xml b/inspection/inspection-gradle-plugin/lint-baseline.xml
index 603e7bd..ef0ffef 100644
--- a/inspection/inspection-gradle-plugin/lint-baseline.xml
+++ b/inspection/inspection-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
<issue
id="EagerGradleConfiguration"
@@ -38,6 +38,24 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Avoid using method findProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Avoid using method getRootProject"
+ errorLine1=" val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
errorLine1=" GradleRunner.create().withProjectDir(projectSetup.rootDir).withPluginClasspath()"
diff --git a/kruth/kruth/bcv/native/current.txt b/kruth/kruth/bcv/native/current.txt
index c38a4c9..3c17a19 100644
--- a/kruth/kruth/bcv/native/current.txt
+++ b/kruth/kruth/bcv/native/current.txt
@@ -1,5 +1,5 @@
// Klib ABI Dump
-// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64]
+// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
diff --git a/kruth/kruth/build.gradle b/kruth/kruth/build.gradle
index a12d78a..a843fe5 100644
--- a/kruth/kruth/build.gradle
+++ b/kruth/kruth/build.gradle
@@ -38,6 +38,7 @@
mac()
linux()
ios()
+ watchosDeviceArm64()
watchos()
tvos()
androidNative()
diff --git a/leanback/leanback-grid/build.gradle b/leanback/leanback-grid/build.gradle
index f24c785..30fc5af 100644
--- a/leanback/leanback-grid/build.gradle
+++ b/leanback/leanback-grid/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 9eade4e..9e1cfb0 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -27,8 +27,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index 78d8b76..74ab4ba 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -26,8 +26,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 5f18f6e..ed754ce 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -31,8 +31,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(project(":internal-testutils-common"))
diff --git a/libraryversions.toml b/libraryversions.toml
index 3980454..7125ef8 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,13 +1,13 @@
[versions]
ACTIVITY = "1.10.0-alpha02"
-ANNOTATION = "1.9.0-alpha02"
+ANNOTATION = "1.9.0-beta01"
ANNOTATION_EXPERIMENTAL = "1.5.0-alpha01"
APPCOMPAT = "1.8.0-alpha01"
APPSEARCH = "1.1.0-alpha05"
ARCH_CORE = "2.3.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
-AUTOFILL = "1.3.0-alpha02"
-BENCHMARK = "1.3.0-rc01"
+AUTOFILL = "1.3.0-beta01"
+BENCHMARK = "1.4.0-alpha01"
BIOMETRIC = "1.4.0-alpha02"
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
@@ -18,10 +18,10 @@
CAMERA_VIEWFINDER = "1.4.0-alpha08"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-beta02"
-COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.8.0-alpha01"
+COLLECTION = "1.5.0-alpha02"
+COMPOSE = "1.8.0-alpha02"
COMPOSE_MATERIAL3 = "1.4.0-alpha01"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha02"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.1.0-alpha03"
COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
CONSTRAINTLAYOUT = "2.2.0-beta01"
@@ -29,7 +29,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-beta01"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.15.0-alpha02"
+CORE = "1.15.0-alpha03"
CORE_ANIMATION = "1.0.0"
CORE_ANIMATION_TESTING = "1.0.0"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -69,7 +69,7 @@
GRAPHICS_PATH = "1.0.0-rc01"
GRAPHICS_SHAPES = "1.0.0-rc01"
GRIDLAYOUT = "1.1.0-beta02"
-HEALTH_CONNECT = "1.1.0-alpha08"
+HEALTH_CONNECT = "1.1.0-alpha09"
HEALTH_CONNECT_TESTING_QUARANTINE = "1.0.0-alpha01"
HEALTH_SERVICES_CLIENT = "1.1.0-alpha03"
HEIFWRITER = "1.1.0-alpha03"
@@ -89,9 +89,9 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha03"
LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
@@ -99,7 +99,7 @@
NAVIGATION = "2.9.0-alpha01"
PAGING = "3.4.0-alpha01"
PALETTE = "1.1.0-alpha01"
-PDF = "1.0.0-alpha02"
+PDF = "1.0.0-alpha03"
PERCENTLAYOUT = "1.1.0-alpha01"
PREFERENCE = "1.3.0-alpha01"
PRINT = "1.1.0-beta01"
@@ -108,16 +108,16 @@
PRIVACYSANDBOX_PLUGINS = "1.0.0-alpha03"
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha14"
PRIVACYSANDBOX_TOOLS = "1.0.0-alpha09"
-PRIVACYSANDBOX_UI = "1.0.0-alpha09"
+PRIVACYSANDBOX_UI = "1.0.0-alpha10"
PROFILEINSTALLER = "1.4.0-rc01"
RECOMMENDATION = "1.1.0-alpha01"
RECYCLERVIEW = "1.4.0-rc01"
RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
REMOTECALLBACK = "1.0.0-alpha02"
RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.7.0-alpha07"
+ROOM = "2.7.0-alpha08"
SAFEPARCEL = "1.0.0-alpha01"
-SAVEDSTATE = "1.3.0-alpha01"
+SAVEDSTATE = "1.3.0-alpha02"
SECURITY = "1.1.0-alpha07"
SECURITY_APP_AUTHENTICATOR = "1.0.0-rc01"
SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-rc01"
@@ -131,7 +131,7 @@
SLICE_BUILDERS_KTX = "1.0.0-alpha09"
SLICE_REMOTECALLBACK = "1.0.0-alpha01"
SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.5.0-alpha07"
+SQLITE = "2.5.0-alpha08"
SQLITE_INSPECTOR = "2.1.0-alpha01"
STABLE_AIDL = "1.0.0-alpha01"
STARTUP = "1.2.0-rc01"
@@ -153,8 +153,8 @@
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.2.0-alpha01"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.5.0-alpha01"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha24"
+WEAR_COMPOSE = "1.5.0-alpha02"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha25"
WEAR_CORE = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
@@ -165,14 +165,14 @@
WEAR_REMOTE_INTERACTIONS = "1.1.0-beta01"
WEAR_TILES = "1.5.0-alpha01"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
-WEAR_WATCHFACE = "1.3.0-alpha03"
+WEAR_WATCHFACE = "1.3.0-alpha04"
WEBKIT = "1.12.0-beta01"
# Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.4.0-alpha02"
+WINDOW = "1.4.0-alpha03"
WINDOW_EXTENSIONS = "1.4.0-beta01"
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
-WORK = "2.10.0-alpha03"
+WORK = "2.10.0-alpha04"
XR = "1.0.0-alpha01"
[groups]
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 0ffdf85..a802e2f 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -41,9 +41,9 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-runtime"))
api("androidx.annotation:annotation:1.8.1")
- api(project(":compose:runtime:runtime"))
+ api("androidx.compose.runtime:runtime:1.7.1")
}
}
@@ -54,14 +54,14 @@
// `lifecycle-runtime-compose` also updates `lifecycle-runtime-ktx`
// in cases where our constraints fail (e.g., internally in AndroidX
// when using project dependencies).
- api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+ api(project(":lifecycle:lifecycle-runtime-ktx"))
}
}
androidInstrumentedTest {
dependencies {
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
- implementation(projectOrArtifact(":compose:ui:ui-test"))
+ implementation(project(":lifecycle:lifecycle-runtime-testing"))
+ implementation(project(":compose:ui:ui-test"))
implementation(project(":compose:test-utils"))
implementation(libs.testRules)
implementation(libs.testRunner)
diff --git a/lifecycle/lifecycle-runtime-compose/samples/build.gradle b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
index 40a0c51..c352425 100644
--- a/lifecycle/lifecycle-runtime-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/samples/build.gradle
@@ -35,7 +35,7 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation projectOrArtifact(":lifecycle:lifecycle-runtime-compose")
+ implementation project(":lifecycle:lifecycle-runtime-compose")
implementation "androidx.compose.material:material:1.0.1"
}
diff --git a/lifecycle/lifecycle-runtime-testing-lint/build.gradle b/lifecycle/lifecycle-runtime-testing-lint/build.gradle
index 9a253fb..002ec45 100644
--- a/lifecycle/lifecycle-runtime-testing-lint/build.gradle
+++ b/lifecycle/lifecycle-runtime-testing-lint/build.gradle
@@ -35,9 +35,9 @@
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
// Needed for Compose lint util functions
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index 7f3a53e..6f21e36 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -71,7 +71,7 @@
dependencies {
api(libs.kotlinCoroutinesAndroid)
implementation("androidx.arch.core:core-runtime:2.2.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
}
}
diff --git a/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle b/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
index ab1344d..4cceb15 100644
--- a/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose-lint/build.gradle
@@ -35,9 +35,9 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
+ bundleInside(project(":compose:lint:common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 13aebe3..b99d1b3 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -39,8 +39,8 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-common"))
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
api("androidx.annotation:annotation:1.8.1")
api("androidx.compose.runtime:runtime:1.6.0")
implementation(libs.kotlinStdlib)
@@ -54,20 +54,20 @@
androidMain {
dependsOn(commonMain)
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
api("androidx.compose.ui:ui:1.6.0")
// Converting `lifecycle-viewmodel-compose` to KMP and including a transitive
// dependency on `lifecycle-livedata-core` triggered a Gradle bug. Adding the
// `livedata` dependency directly works around the issue.
// See https://github.com/gradle/gradle/issues/14220 for details.
- compileOnly(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+ compileOnly(project(":lifecycle:lifecycle-livedata-core"))
}
}
androidInstrumentedTest {
dependsOn(commonTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
implementation(libs.testRules)
implementation(libs.testRunner)
@@ -80,10 +80,10 @@
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- implementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
- implementation(projectOrArtifact(":activity:activity-compose"))
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+ implementation(project(":activity:activity-compose"))
+ implementation(project(":lifecycle:lifecycle-runtime-testing"))
}
}
}
@@ -100,7 +100,7 @@
description = "Compose integration with Lifecycle ViewModel"
legacyDisableKotlinStrictApiMode = true
metalavaK2UastEnabled = false
- samples(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
+ samples(project(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
}
android {
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
index aecf4ad..a9fb65d 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
@@ -34,9 +34,9 @@
dependencies {
compileOnly(project(":annotation:annotation-sampled"))
implementation(libs.kotlinStdlib)
- implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
- implementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose")
- implementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate")
+ implementation project(":lifecycle:lifecycle-common-java8")
+ implementation project(":lifecycle:lifecycle-viewmodel-compose")
+ implementation project(":lifecycle:lifecycle-viewmodel-savedstate")
}
androidx {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 6f417dd..108a647 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -40,13 +40,13 @@
api("androidx.annotation:annotation:1.8.1")
api("androidx.core:core-ktx:1.2.0")
api("androidx.savedstate:savedstate:1.2.1")
- api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(project(":lifecycle:lifecycle-livedata-core"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesAndroid)
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime")
- androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-livedata-core")
+ androidTestImplementation project(":lifecycle:lifecycle-runtime")
+ androidTestImplementation project(":lifecycle:lifecycle-livedata-core")
androidTestImplementation ("androidx.fragment:fragment:1.3.0")
androidTestImplementation project(":internal-testutils-runtime")
androidTestImplementation(libs.truth)
diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle
index 8060a19..bde7c14 100644
--- a/lint-checks/integration-tests/build.gradle
+++ b/lint-checks/integration-tests/build.gradle
@@ -42,9 +42,6 @@
android {
lintOptions {
- // lint is supposed to detect errors in this project
- // We don't need to see the errors in stdout
- textOutput("${buildDir}/lint-results-debug.txt") // Set to this value for b/189877657
// We don't want errors to cause lint to fail
abortOnError false
}
@@ -76,13 +73,3 @@
}
}
}
-
-// workaround for b/189877657
-afterEvaluate {
- tasks.findByName("copyDebugAndroidLintReports")?.configure {
- enabled = false
- }
- tasks.findByName("copyDebugLintReports")?.configure {
- enabled = false
- }
-}
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index e8680ae..3f129b8 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -140,8 +140,11 @@
Replacement(NAMED_DOMAIN_OBJECT_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"findByName" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "findProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"findProperty" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "hasProperty" to
+ Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"property" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
"iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
@@ -149,8 +152,10 @@
"getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
"getByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"getByName" to Replacement(TASK_CONTAINER, "named", EAGER_CONFIGURATION_ISSUE),
+ "getParent" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"getProperties" to
Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "getRootProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
"matching" to Replacement(TASK_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
"replace" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
"remove" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
diff --git a/loader/loader-ktx/build.gradle b/loader/loader-ktx/build.gradle
index ffb4403..d411d1b 100644
--- a/loader/loader-ktx/build.gradle
+++ b/loader/loader-ktx/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/loader/loader/build.gradle b/loader/loader/build.gradle
index 706fba2..209c8fb 100644
--- a/loader/loader/build.gradle
+++ b/loader/loader/build.gradle
@@ -15,7 +15,7 @@
dependencies {
api("androidx.annotation:annotation:1.8.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.0.0")
- implementation(projectOrArtifact(":core:core"))
+ implementation(project(":core:core"))
implementation("androidx.collection:collection:1.4.2")
implementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
@@ -25,8 +25,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/navigation/integration-tests/testapp/build.gradle b/navigation/integration-tests/testapp/build.gradle
index 4398e0c..030aecf 100644
--- a/navigation/integration-tests/testapp/build.gradle
+++ b/navigation/integration-tests/testapp/build.gradle
@@ -23,8 +23,8 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation("androidx.appcompat:appcompat:1.1.0")
- api(projectOrArtifact(":fragment:fragment-ktx"))
- api(projectOrArtifact(":transition:transition-ktx"))
+ api(project(":fragment:fragment-ktx"))
+ api(project(":transition:transition-ktx"))
implementation(project(":navigation:navigation-fragment-ktx"))
implementation(project(":navigation:navigation-ui-ktx"))
implementation(project(":internal-testutils-navigation"), {
diff --git a/navigation/navigation-common-lint/build.gradle b/navigation/navigation-common-lint/build.gradle
index d1f2801..66c71e5 100644
--- a/navigation/navigation-common-lint/build.gradle
+++ b/navigation/navigation-common-lint/build.gradle
@@ -35,7 +35,7 @@
dependencies {
compileOnly(libs.kotlinStdlib)
compileOnly(libs.androidLintApi)
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index bcde5af..e76f7a7 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -48,7 +48,7 @@
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.collection:collection-ktx:1.4.2")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation(libs.kotlinSerializationCore)
api(libs.kotlinStdlib)
@@ -67,8 +67,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
lintPublish(project(':navigation:navigation-common-lint'))
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
index 897a60c..10b89a5 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
@@ -68,7 +68,7 @@
destination.id = 1
assertThat(destination.route).isEqualTo("route")
assertThat(destination.id).isEqualTo(1)
- assertThat(destination.hasDeepLink(createRoute("route").toUri())).isTrue()
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
destination.route = null
assertThat(destination.route).isNull()
@@ -682,4 +682,44 @@
assertThat(destination.hasRoute<TestClass>()).isFalse()
}
+
+ @Test
+ fun routeNotAddedToDeepLink() {
+ val destination = NoOpNavigator().createDestination()
+ assertThat(destination.route).isNull()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
+ }
+
+ @Test
+ fun matchRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+ }
+
+ @Test
+ fun matchRouteAfterSetNewRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+
+ destination.route = "newRoute"
+ assertThat(destination.route).isEqualTo("newRoute")
+ val match2 = destination.matchRoute("newRoute")
+ assertThat(match2).isNotNull()
+ assertThat(match2!!.destination).isEqualTo(destination)
+ }
}
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 5e1c141..64768f7 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -168,9 +168,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/route can't be used to " +
- "open destination NavDestination(0xa2bd82dc).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"route\" for destination NavDestination(0x0). Following required arguments are missing: [intArg]"
)
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index de01d95..4225f41 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -223,14 +223,36 @@
id = 0
} else {
require(route.isNotBlank()) { "Cannot have an empty route" }
- val internalRoute = createRoute(route)
- id = internalRoute.hashCode()
- addDeepLink(internalRoute)
+
+ // make sure the route contains all required arguments
+ val tempRoute = createRoute(route)
+ val tempDeepLink = NavDeepLink.Builder().setUriPattern(tempRoute).build()
+ val missingRequiredArguments =
+ _arguments.missingRequiredArguments { key ->
+ key !in tempDeepLink.argumentsNames
+ }
+ require(missingRequiredArguments.isEmpty()) {
+ "Cannot set route \"$route\" for destination $this. " +
+ "Following required arguments are missing: $missingRequiredArguments"
+ }
+
+ routeDeepLink = lazy { NavDeepLink.Builder().setUriPattern(tempRoute).build() }
+ id = tempRoute.hashCode()
}
- deepLinks.remove(deepLinks.firstOrNull { it.uriPattern == createRoute(field) })
field = route
}
+ /**
+ * This destination's unique route as a NavDeepLink.
+ *
+ * This deeplink must be kept private and segregated from the explicitly added public deeplinks
+ * to ensure that external users cannot deeplink into this destination with this routeDeepLink.
+ *
+ * This value is reassigned a new lazy value every time [route] is updated to ensure that any
+ * initialized lazy value is overwritten with the latest value.
+ */
+ private var routeDeepLink: Lazy<NavDeepLink>? = null
+
public open val displayName: String
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get() = idName ?: id.toString()
@@ -346,26 +368,28 @@
}
/**
- * Determines if this NavDestination has a deep link of this route.
+ * Determines if this NavDestination's route matches the requested route.
*
* @param [route] The route to match against this [NavDestination.route]
* @return The matching [DeepLinkMatch], or null if no match was found.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun matchDeepLink(route: String): DeepLinkMatch? {
- val request = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
- val matchingDeepLink =
- if (this is NavGraph) {
- matchDeepLinkComprehensive(
- request,
- searchChildren = false,
- searchParent = false,
- lastVisited = this
- )
- } else {
- matchDeepLink(request)
- }
- return matchingDeepLink
+ public fun matchRoute(route: String): DeepLinkMatch? {
+ val routeDeepLink = this.routeDeepLink?.value ?: return null
+
+ val uri = createRoute(route).toUri()
+
+ // includes matching args for path, query, and fragment
+ val matchingArguments = routeDeepLink.getMatchingArguments(uri, _arguments) ?: return null
+ val matchingPathSegments = routeDeepLink.calculateMatchingPathSegments(uri)
+ return DeepLinkMatch(
+ this,
+ matchingArguments,
+ routeDeepLink.isExactDeepLink,
+ matchingPathSegments,
+ false,
+ -1
+ )
}
/**
@@ -481,7 +505,7 @@
// if no match based on routePattern, this means route contains filled in args or query
// params
- val matchingDeepLink = matchDeepLink(route)
+ val matchingDeepLink = matchRoute(route)
// if no matchingDeepLink or mismatching destination, return false directly
if (this != matchingDeepLink?.destination) return false
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 3a34696..5fbe69d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -65,6 +65,50 @@
}
/**
+ * Matches route with all children and parents recursively.
+ *
+ * Does not revisit graphs (whether it's a child or parent) if it has already been visited.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun matchRouteComprehensive(
+ route: String,
+ searchChildren: Boolean,
+ searchParent: Boolean,
+ lastVisited: NavDestination
+ ): DeepLinkMatch? {
+ // First try to match with this graph's route
+ val bestMatch = matchRoute(route)
+ // If searchChildren is true, search through all child destinations for a matching route
+ val bestChildMatch =
+ if (searchChildren) {
+ mapNotNull { child ->
+ when (child) {
+ lastVisited -> null
+ is NavGraph ->
+ child.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = false,
+ lastVisited = this
+ )
+ else -> child.matchRoute(route)
+ }
+ }
+ .maxOrNull()
+ } else null
+
+ // If searchParent is true, search through all parents (and their children) destinations
+ // for a matching route
+ val bestParentMatch =
+ parent?.let {
+ if (searchParent && it != lastVisited)
+ it.matchRouteComprehensive(route, searchChildren, true, this)
+ else null
+ }
+ return listOfNotNull(bestMatch, bestChildMatch, bestParentMatch).maxOrNull()
+ }
+
+ /**
* Matches deeplink with all children and parents recursively.
*
* Does not revisit graphs (whether it's a child or parent) if it has already been visited.
@@ -262,7 +306,7 @@
nodes.valueIterator().asSequence().firstOrNull {
// first try matching with routePattern
// if not found with routePattern, try matching with route args
- it.route.equals(route) || it.matchDeepLink(route) != null
+ it.route.equals(route) || it.matchRoute(route) != null
}
// Search the parent for the NavDestination if it is not a child of this navigation graph
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
index 79035974..96f21f0 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
@@ -83,14 +83,31 @@
"navigation destination $dest is not a direct child of this NavGraph"
)
}
- if (startRoute != null && startRoute != startDestination.route) {
- val matchingArgs = startDestination.matchDeepLink(startRoute)?.matchingArgs
- if (matchingArgs != null && !matchingArgs.isEmpty) {
- val bundle = Bundle()
- // we need to add args from startRoute, but it should not override existing args
- bundle.putAll(matchingArgs)
- args?.let { bundle.putAll(args) }
- args = bundle
+ if (startRoute != null) {
+ // If startRoute contains only placeholders, we fallback to default arg values.
+ // This is to maintain existing behavior of using default value for startDestination
+ // while also adding support for args declared in startRoute.
+ if (startRoute != startDestination.route) {
+ val matchingArgs = startDestination.matchRoute(startRoute)?.matchingArgs
+ if (matchingArgs != null && !matchingArgs.isEmpty) {
+ val bundle = Bundle()
+ // we need to add args from startRoute, but it should not override existing args
+ bundle.putAll(matchingArgs)
+ args?.let { bundle.putAll(args) }
+ args = bundle
+ }
+ }
+ // by this point, the bundle should contain all arguments that don't have
+ // default values (regardless of whether the actual default value is known or not).
+ if (startDestination.arguments.isNotEmpty()) {
+ val missingRequiredArgs =
+ startDestination.arguments.missingRequiredArguments { key ->
+ if (args == null) true else !args.containsKey(key)
+ }
+ require(missingRequiredArgs.isEmpty()) {
+ "Cannot navigate to startDestination $startDestination. " +
+ "Missing required arguments [$missingRequiredArgs]"
+ }
}
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
index 1d4158a..ac7a813 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavType.kt
@@ -880,6 +880,7 @@
override fun valueEquals(value: Array<String>?, other: Array<String>?) =
value.contentDeepEquals(other)
+ // "null" is still serialized as "null"
override fun serializeAsValues(value: Array<String>?): List<String> =
value?.map { Uri.encode(it) } ?: emptyList()
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
index df00b37..2e87bc3 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/NavTypeConverter.kt
@@ -18,7 +18,9 @@
package androidx.navigation.serialization
+import android.net.Uri
import android.os.Bundle
+import androidx.navigation.CollectionNavType
import androidx.navigation.NavType
import java.io.Serializable
import kotlin.reflect.KType
@@ -38,6 +40,7 @@
LONG,
LONG_NULLABLE,
STRING,
+ STRING_NULLABLE,
INT_ARRAY,
BOOL_ARRAY,
FLOAT_ARRAY,
@@ -68,14 +71,19 @@
InternalType.FLOAT_NULLABLE -> InternalNavType.FloatNullableType
InternalType.LONG -> NavType.LongType
InternalType.LONG_NULLABLE -> InternalNavType.LongNullableType
- InternalType.STRING -> NavType.StringType
+ InternalType.STRING -> InternalNavType.StringNonNullableType
+ InternalType.STRING_NULLABLE -> NavType.StringType
InternalType.INT_ARRAY -> NavType.IntArrayType
InternalType.BOOL_ARRAY -> NavType.BoolArrayType
InternalType.FLOAT_ARRAY -> NavType.FloatArrayType
InternalType.LONG_ARRAY -> NavType.LongArrayType
InternalType.ARRAY -> {
val typeParameter = getElementDescriptor(0).toInternalType()
- if (typeParameter == InternalType.STRING) NavType.StringArrayType else UNKNOWN
+ when (typeParameter) {
+ InternalType.STRING -> NavType.StringArrayType
+ InternalType.STRING_NULLABLE -> InternalNavType.StringNullableArrayType
+ else -> UNKNOWN
+ }
}
InternalType.LIST -> {
val typeParameter = getElementDescriptor(0).toInternalType()
@@ -85,6 +93,7 @@
InternalType.FLOAT -> NavType.FloatListType
InternalType.LONG -> NavType.LongListType
InternalType.STRING -> NavType.StringListType
+ InternalType.STRING_NULLABLE -> InternalNavType.StringNullableListType
else -> UNKNOWN
}
}
@@ -120,7 +129,8 @@
if (isNullable) InternalType.FLOAT_NULLABLE else InternalType.FLOAT
serialName == "kotlin.Long" ->
if (isNullable) InternalType.LONG_NULLABLE else InternalType.LONG
- serialName == "kotlin.String" -> InternalType.STRING
+ serialName == "kotlin.String" ->
+ if (isNullable) InternalType.STRING_NULLABLE else InternalType.STRING
serialName == "kotlin.IntArray" -> InternalType.INT_ARRAY
serialName == "kotlin.BooleanArray" -> InternalType.BOOL_ARRAY
serialName == "kotlin.FloatArray" -> InternalType.FLOAT_ARRAY
@@ -268,6 +278,90 @@
}
}
+ val StringNonNullableType =
+ object : NavType<String>(false) {
+ override val name: String
+ get() = "string_non_nullable"
+
+ override fun put(bundle: Bundle, key: String, value: String) {
+ bundle.putString(key, value)
+ }
+
+ @Suppress("DEPRECATION")
+ override fun get(bundle: Bundle, key: String): String = bundle.getString(key) ?: "null"
+
+ // "null" is still parsed as "null"
+ override fun parseValue(value: String): String = value
+
+ // "null" is still serialized as "null"
+ override fun serializeAsValue(value: String): String = Uri.encode(value)
+ }
+
+ val StringNullableArrayType: NavType<Array<String?>?> =
+ object : CollectionNavType<Array<String?>?>(true) {
+ override val name: String
+ get() = "string_nullable[]"
+
+ override fun put(bundle: Bundle, key: String, value: Array<String?>?) {
+ bundle.putStringArray(key, value)
+ }
+
+ @Suppress("UNCHECKED_CAST", "DEPRECATION")
+ override fun get(bundle: Bundle, key: String): Array<String?>? =
+ bundle[key] as Array<String?>?
+
+ // match String? behavior where null -> null, and "null" -> null
+ override fun parseValue(value: String): Array<String?> =
+ arrayOf(StringType.parseValue(value))
+
+ override fun parseValue(
+ value: String,
+ previousValue: Array<String?>?
+ ): Array<String?>? = previousValue?.plus(parseValue(value)) ?: parseValue(value)
+
+ override fun valueEquals(value: Array<String?>?, other: Array<String?>?): Boolean =
+ value.contentDeepEquals(other)
+
+ override fun serializeAsValues(value: Array<String?>?): List<String> =
+ value?.map { Uri.encode(it) } ?: emptyList()
+
+ override fun emptyCollection(): Array<String?>? = arrayOf()
+ }
+
+ val StringNullableListType: NavType<List<String?>?> =
+ object : CollectionNavType<List<String?>?>(true) {
+ override val name: String
+ get() = "List<String?>"
+
+ override fun put(bundle: Bundle, key: String, value: List<String?>?) {
+ bundle.putStringArray(key, value?.toTypedArray())
+ }
+
+ @Suppress("UNCHECKED_CAST", "DEPRECATION")
+ override fun get(bundle: Bundle, key: String): List<String?>? {
+ return (bundle[key] as Array<String?>?)?.toList()
+ }
+
+ override fun parseValue(value: String): List<String?> {
+ return listOf(StringType.parseValue(value))
+ }
+
+ override fun parseValue(value: String, previousValue: List<String?>?): List<String?>? {
+ return previousValue?.plus(parseValue(value)) ?: parseValue(value)
+ }
+
+ override fun valueEquals(value: List<String?>?, other: List<String?>?): Boolean {
+ val valueArray = value?.toTypedArray()
+ val otherArray = other?.toTypedArray()
+ return valueArray.contentDeepEquals(otherArray)
+ }
+
+ override fun serializeAsValues(value: List<String?>?): List<String> =
+ value?.map { Uri.encode(it) } ?: emptyList()
+
+ override fun emptyCollection(): List<String?> = emptyList()
+ }
+
class EnumNullableType<D : Enum<*>?>(type: Class<D?>) : SerializableNullableType<D?>(type) {
private val type: Class<D?>
diff --git a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
index f395dd1..7c00ba2 100644
--- a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
+++ b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavArgumentGeneratorTest.kt
@@ -73,7 +73,7 @@
val converted = serializer<TestClass>().generateNavArguments()
val expected =
navArgument("arg") {
- type = NavType.StringType
+ type = InternalNavType.StringNonNullableType
nullable = false
}
assertThat(converted).containsExactlyInOrder(expected)
@@ -487,22 +487,22 @@
}
@Test
- fun convertToStringList() {
- @Serializable class TestClass(val arg: List<String>)
+ fun convertToStringNullableArrayNullable() {
+ @Serializable class TestClass(val arg: Array<String?>?)
val converted = serializer<TestClass>().generateNavArguments()
val expected =
navArgument("arg") {
- type = NavType.StringListType
- nullable = false
+ type = InternalNavType.StringNullableArrayType
+ nullable = true
}
assertThat(converted).containsExactlyInOrder(expected)
assertThat(converted[0].argument.isDefaultValueUnknown).isFalse()
}
@Test
- fun convertArrayListToStringList() {
- @Serializable class TestClass(val arg: ArrayList<String>)
+ fun convertToStringList() {
+ @Serializable class TestClass(val arg: List<String>)
val converted = serializer<TestClass>().generateNavArguments()
val expected =
@@ -529,6 +529,46 @@
}
@Test
+ fun convertToStringNullableList() {
+ @Serializable class TestClass(val arg: List<String?>)
+ val converted = serializer<TestClass>().generateNavArguments()
+ val expected =
+ navArgument("arg") {
+ type = InternalNavType.StringNullableListType
+ nullable = false
+ }
+ assertThat(converted).containsExactlyInOrder(expected)
+ assertThat(converted[0].argument.isDefaultValueUnknown).isFalse()
+ }
+
+ @Test
+ fun convertToStringNullableListNullable() {
+ @Serializable class TestClass(val arg: List<String?>?)
+ val converted = serializer<TestClass>().generateNavArguments()
+ val expected =
+ navArgument("arg") {
+ type = InternalNavType.StringNullableListType
+ nullable = true
+ }
+ assertThat(converted).containsExactlyInOrder(expected)
+ assertThat(converted[0].argument.isDefaultValueUnknown).isFalse()
+ }
+
+ @Test
+ fun convertArrayListToStringList() {
+ @Serializable class TestClass(val arg: ArrayList<String>)
+
+ val converted = serializer<TestClass>().generateNavArguments()
+ val expected =
+ navArgument("arg") {
+ type = NavType.StringListType
+ nullable = false
+ }
+ assertThat(converted).containsExactlyInOrder(expected)
+ assertThat(converted[0].argument.isDefaultValueUnknown).isFalse()
+ }
+
+ @Test
fun convertToParcelable() {
@Serializable
class TestParcelable : Parcelable {
@@ -864,7 +904,7 @@
val converted = serializer<TestClass>().generateNavArguments()
val expected =
navArgument("arg") {
- type = NavType.StringType
+ type = InternalNavType.StringNonNullableType
nullable = false
unknownDefaultValuePresent = true
}
@@ -1173,7 +1213,7 @@
.generateNavArguments(mapOf(typeOf<ArrayList<Int>>() to CustomIntList))
val expectedString =
navArgument("arg") {
- type = NavType.StringType
+ type = InternalNavType.StringNonNullableType
nullable = false
}
val expectedIntList =
diff --git a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavTypeConverterTest.kt b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavTypeConverterTest.kt
index 5ebbcf0..6b881e0 100644
--- a/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavTypeConverterTest.kt
+++ b/navigation/navigation-common/src/test/java/androidx/navigation/serialization/NavTypeConverterTest.kt
@@ -111,6 +111,9 @@
val descriptor = serializer<String>().descriptor
val kType = typeOf<String>()
assertThat(descriptor.matchKType(kType)).isTrue()
+
+ val nullable = serializer<String?>().descriptor
+ assertThat(nullable.matchKType(kType)).isFalse()
}
@Test
@@ -645,7 +648,7 @@
val longType = serializer<Long>().descriptor.getNavType()
assertThat(longType).isEqualTo(NavType.LongType)
- val stringType = serializer<String>().descriptor.getNavType()
+ val stringType = serializer<String?>().descriptor.getNavType()
assertThat(stringType).isEqualTo(NavType.StringType)
}
diff --git a/navigation/navigation-compose-lint/build.gradle b/navigation/navigation-compose-lint/build.gradle
index 4548102..23b3515 100644
--- a/navigation/navigation-compose-lint/build.gradle
+++ b/navigation/navigation-compose-lint/build.gradle
@@ -34,10 +34,10 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":compose:lint:common"))
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":compose:lint:common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
- testImplementation(projectOrArtifact(":compose:lint:common-test"))
+ testImplementation(project(":compose:lint:common-test"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.kotlinReflect)
testImplementation(libs.kotlinStdlibJdk8)
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 0088065..af843ef 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -36,30 +36,30 @@
api("androidx.compose.runtime:runtime-saveable:1.7.0-rc01")
api("androidx.compose.ui:ui:1.7.0-rc01")
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
- api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
+ api(project(":navigation:navigation-runtime-ktx"))
implementation(libs.kotlinSerializationCore)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
+ androidTestImplementation(project(":compose:material:material"))
androidTestImplementation project(":compose:test-utils")
- androidTestImplementation projectOrArtifact(":compose:ui:ui-tooling")
- androidTestImplementation(projectOrArtifact(":navigation:navigation-testing"))
- androidTestImplementation(projectOrArtifact(":internal-testutils-navigation"), {
+ androidTestImplementation project(":compose:ui:ui-tooling")
+ androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation(project(":internal-testutils-navigation"), {
exclude group: "androidx.navigation", module: "navigation-common"
})
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
- androidTestImplementation(projectOrArtifact(":activity:activity-ktx"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
+ androidTestImplementation(project(":lifecycle:lifecycle-livedata-core"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
+ androidTestImplementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+ androidTestImplementation(project(":activity:activity-ktx"))
androidTestImplementation("androidx.collection:collection-ktx:1.4.2")
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
- lintChecks(projectOrArtifact(":navigation:navigation-compose-lint"))
- lintPublish(projectOrArtifact(":navigation:navigation-compose-lint"))
+ lintChecks(project(":navigation:navigation-compose-lint"))
+ lintPublish(project(":navigation:navigation-compose-lint"))
}
androidx {
@@ -68,7 +68,7 @@
inceptionYear = "2020"
description = "Compose integration with Navigation"
legacyDisableKotlinStrictApiMode = true
- samples(projectOrArtifact(":navigation:navigation-compose:navigation-compose-samples"))
+ samples(project(":navigation:navigation-compose:navigation-compose-samples"))
legacyDisableKotlinStrictApiMode = true
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..7cdaa05 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -42,7 +42,7 @@
implementation(project(":compose:animation:animation"))
implementation("androidx.compose.foundation:foundation:1.0.1")
implementation("androidx.compose.ui:ui-tooling:1.4.0")
- implementation(projectOrArtifact(":navigation:navigation-compose"))
+ implementation(project(":navigation:navigation-compose"))
implementation("androidx.compose.material:material:1.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
@@ -57,10 +57,6 @@
kotlinTarget = KotlinTarget.KOTLIN_2_0
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
SharedTransitionLayout {
val selectFirst = mutableStateOf(true)
NavHost(navController, startDestination = RedBox) {
- composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
- composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+ composable<RedBox> {
+ RedBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.navigate(BlueBox)
+ }
+ }
+ composable<BlueBox> {
+ BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.popBackStack()
+ }
+ }
}
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun RedBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onNavigate: () -> Unit
) {
- Box(
- Modifier.sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onNavigate()
- }
- )
- .background(Color.Red)
- .size(100.dp)
- ) {
- Text("start", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onNavigate()
+ }
+ )
+ .background(Color.Red)
+ .size(100.dp)
+ ) {
+ Text("start", color = Color.White)
+ }
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun BlueBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onPopBack: () -> Unit
) {
- Box(
- Modifier.offset(180.dp, 180.dp)
- .sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = !selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onPopBack()
- }
- )
- .alpha(0.5f)
- .background(Color.Blue)
- .size(180.dp)
- ) {
- Text("finish", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.offset(180.dp, 180.dp)
+ .sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = !selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onPopBack()
+ }
+ )
+ .alpha(0.5f)
+ .background(Color.Blue)
+ .size(180.dp)
+ ) {
+ Text("finish", color = Color.White)
+ }
}
}
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
index 59c08294..a5f5320 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
@@ -243,13 +243,15 @@
@Test
fun testNavigationNestedKClassStart() {
+ @Serializable class TestOuterClass
+
lateinit var navController: TestNavHostController
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
- NavHost(navController, startDestination = TestClassArg::class) {
- navigation<TestClassArg>(startDestination = TestClass::class) {
+ NavHost(navController, startDestination = TestOuterClass::class) {
+ navigation<TestOuterClass>(startDestination = TestClass::class) {
composable<TestClass> {}
}
}
@@ -258,7 +260,7 @@
composeTestRule.runOnUiThread {
assertThat(navController.currentDestination?.route).isEqualTo(TEST_CLASS_ROUTE)
assertWithMessage("Destination should be added to the graph")
- .that(TestClassArg::class in navController.graph)
+ .that(TestOuterClass::class in navController.graph)
.isTrue()
assertThat(navController.graph.findStartDestination().route).isEqualTo(TEST_CLASS_ROUTE)
}
@@ -341,7 +343,7 @@
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
- NavHost(navController, startDestination = TestClassArg::class) {
+ NavHost(navController, startDestination = TestClassArg(1)) {
navigation<TestClassArg>(startDestination = TestClass()) {
composable<TestClass> {}
}
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index 34a65f65..d294b3c 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -51,9 +51,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index 8693621..ba25181 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -54,9 +54,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
diff --git a/navigation/navigation-runtime-lint/build.gradle b/navigation/navigation-runtime-lint/build.gradle
index 91991d3..f50d052 100644
--- a/navigation/navigation-runtime-lint/build.gradle
+++ b/navigation/navigation-runtime-lint/build.gradle
@@ -35,7 +35,7 @@
dependencies {
compileOnly(libs.androidLintMinApi)
compileOnly(libs.kotlinStdlib)
- bundleInside(projectOrArtifact(":navigation:navigation-lint-common"))
+ bundleInside(project(":navigation:navigation-lint-common"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.androidLint)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 2698334..9acb9fd 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -42,7 +42,7 @@
implementation(libs.kotlinSerializationCore)
api(libs.kotlinStdlib)
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(project(":internal-testutils-navigation"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.testExtJunit)
@@ -53,8 +53,8 @@
androidTestImplementation(libs.espressoCore)
androidTestImplementation(libs.espressoIntents)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinTest)
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 986c95e..b15e868 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -29,6 +29,7 @@
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
import androidx.navigation.NavDestination.Companion.createRoute
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.serialization.generateHashCode
@@ -285,11 +286,18 @@
@Serializable @SerialName("test") class TestClass(val arg: Int)
val navController = createNavController()
- navController.graph =
- navController.createGraph(startDestination = TestClass::class) { test<TestClass>() }
- assertThat(navController.currentDestination?.route).isEqualTo("test/{arg}")
- assertThat(navController.currentDestination?.id)
- .isEqualTo(serializer<TestClass>().generateHashCode())
+ val exception =
+ assertFailsWith<IllegalArgumentException> {
+ navController.graph =
+ navController.createGraph(startDestination = TestClass::class) {
+ test<TestClass>()
+ }
+ }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Cannot navigate to startDestination Destination(0x693c804) " +
+ "route=test/{arg}. Missing required arguments [[arg]]"
+ )
}
@UiThreadTest
@@ -424,6 +432,30 @@
@UiThreadTest
@Test
+ fun testStartDestinationMissingRequiredArg() {
+ val navController = createNavController()
+ val exception =
+ assertFailsWith<IllegalArgumentException> {
+ navController.graph =
+ navController.createGraph(
+ route = "graph",
+ startDestination = "start_test/{arg}"
+ ) {
+ test("start_test/{arg}") {
+ // does not have default value to fallback to
+ argument("arg") { type = NavType.IntType }
+ }
+ }
+ }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Cannot navigate to startDestination Destination(0x67775af) " +
+ "route=start_test/{arg}. Missing required arguments [[arg]]"
+ )
+ }
+
+ @UiThreadTest
+ @Test
fun testNestedStartDestinationObjectWithPathArg() {
@Serializable @SerialName("graph") class NestedGraph(val nestedArg: Int)
@@ -814,6 +846,30 @@
@UiThreadTest
@Test
+ fun testNavigateContainsIntent() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start")
+ test("second")
+ }
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ navController.navigate("second")
+ assertThat(navController.currentDestination?.route).isEqualTo("second")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent: Intent? =
+ @Suppress("DEPRECATION")
+ navController.currentBackStackEntry?.arguments?.getParcelable(KEY_DEEP_LINK_INTENT)
+ assertThat(intent).isNotNull()
+ assertThat(intent!!.data).isEqualTo(Uri.parse("android-app://androidx.navigation/second"))
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateNestedSharedDestination() {
val navController = createNavController()
navController.graph =
@@ -1524,7 +1580,6 @@
test<TestClass>()
}
assertThat(navController.currentDestination?.route).isEqualTo("start")
-
// passed in arg
navController.navigate(TestClass(TestTopLevelEnum.TWO))
assertThat(navController.currentDestination?.hasRoute(TestClass::class)).isTrue()
@@ -1725,12 +1780,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -1761,12 +1814,10 @@
}
// fist nested graph
- val deepLink = Uri.parse("android-app://androidx.navigation/graph2/13")
- navController.navigate(deepLink)
+ navController.navigate("graph2/13")
// second nested graph
- val deepLink2 = Uri.parse("android-app://androidx.navigation/graph3/18")
- navController.navigate(deepLink2)
+ navController.navigate("graph3/18")
val navigator = navController.navigatorProvider.getNavigator(NavGraphNavigator::class.java)
// ["graph", "graph2/13", "graph3/18"]
@@ -1786,8 +1837,7 @@
navController.graph = nav_multiArg_graph
// navigate with both args filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -1804,8 +1854,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -1902,8 +1951,7 @@
}
// navigate with query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1937,8 +1985,7 @@
}
// navigate with query params
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null&opt2=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null&opt2=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1952,7 +1999,7 @@
navController.graph =
createNavController().createGraph(
route = "nav_root",
- startDestination = "start_test?{arg}"
+ startDestination = "start_test?myArg"
) {
test("start_test?{arg}") { argument("arg") { type = NavType.StringType } }
}
@@ -1975,7 +2022,7 @@
navController.graph =
createNavController().createGraph(
route = "nav_root",
- startDestination = "start_test?opt={arg}"
+ startDestination = "start_test?opt=myArg"
) {
test("start_test?opt={arg}") { argument("arg") { type = NavType.StringType } }
}
@@ -1998,8 +2045,7 @@
navController.graph = nav_singleArg_graph
// navigate with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13"]
@@ -2024,8 +2070,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2049,8 +2094,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2073,8 +2117,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2446,12 +2489,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2470,12 +2511,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2492,8 +2531,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2510,8 +2548,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2539,8 +2576,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2569,8 +2605,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2596,8 +2631,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2612,8 +2646,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2630,8 +2663,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2648,8 +2680,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2666,8 +2697,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2989,11 +3019,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3012,11 +3040,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3036,11 +3062,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3090,11 +3114,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3111,11 +3133,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3133,11 +3153,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3155,11 +3173,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3179,8 +3195,7 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3201,8 +3216,7 @@
navController.graph = nav_multiArg_graph
// navigate with partial args filled in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3229,8 +3243,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3258,8 +3271,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3287,8 +3299,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3305,8 +3316,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3323,8 +3333,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3341,8 +3350,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3359,8 +3367,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3846,6 +3853,71 @@
assertThat(navigator.backStack.size).isEqualTo(1)
}
+ @UiThreadTest
+ @Test
+ fun testNavigateViaUriOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val deepLink = Uri.parse(createRoute("explicit_start_deeplink"))
+
+ navController.navigate(deepLink)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val deepLink2 = Uri.parse(createRoute("start"))
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(deepLink2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateViaRequestOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val request =
+ NavDeepLinkRequest(Uri.parse(createRoute("explicit_start_deeplink")), null, null)
+
+ navController.navigate(request)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val request2 = NavDeepLinkRequest(Uri.parse(createRoute("start")), null, null)
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(request2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
@LargeTest
@Test
fun testNavigateViaImplicitDeepLink() {
@@ -4650,6 +4722,33 @@
@UiThreadTest
@Test
+ fun testNavigateWithObjectNonNullableString() {
+ @Serializable @SerialName("test") class TestClass(val arg: String)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass("null")) { test<TestClass>() }
+ assertThat(navController.currentDestination?.route).isEqualTo("test/{arg}")
+ val route = navController.currentBackStackEntry?.toRoute<TestClass>()
+ assertThat(route!!.arg).isNotNull()
+ assertThat(route.arg).isEqualTo("null")
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectNullableString() {
+ @Serializable @SerialName("test") class TestClass(val arg: String?)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass("null")) { test<TestClass>() }
+ assertThat(navController.currentDestination?.route).isEqualTo("test/{arg}")
+ val route = navController.currentBackStackEntry?.toRoute<TestClass>()
+ assertThat(route!!.arg).isNull()
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateWithObjectQueryNullString() {
@Serializable @SerialName("test") class TestClass(val arg: String? = null)
@@ -4663,6 +4762,144 @@
@UiThreadTest
@Test
+ fun testNavigateWithObjectStringArrayNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: Array<String>)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(arrayOf())) { test<TestClass>() }
+
+ navController.navigate(TestClass(arrayOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg.first()).isEqualTo("one")
+ assertThat(arg.last()).isEqualTo("null")
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringArrayNullableNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: Array<String>?)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(null)) { test<TestClass>() }
+
+ navController.navigate(TestClass(arrayOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg!!.first()).isEqualTo("one")
+ assertThat(arg.last()).isEqualTo("null")
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringNullableArrayNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: Array<String?>)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(arrayOf())) { test<TestClass>() }
+
+ navController.navigate(TestClass(arrayOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg.first()).isEqualTo("one")
+ // should match behavior of String? type where "null" -> null
+ assertThat(arg.last()).isNull()
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringNullableArrayNullString() {
+ @Serializable @SerialName("test") class TestClass(val arg: Array<String?>)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(arrayOf())) { test<TestClass>() }
+
+ navController.navigate(TestClass(arrayOf("one", null)))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg.first()).isEqualTo("one")
+ // should match behavior of String? type where null -> null
+ assertThat(arg.last()).isNull()
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringListNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: List<String>)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(listOf())) { test<TestClass>() }
+
+ navController.navigate(TestClass(listOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg.first()).isEqualTo("one")
+ assertThat(arg.last()).isEqualTo("null")
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringListNullableNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: List<String>?)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(null)) { test<TestClass>() }
+
+ navController.navigate(TestClass(listOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg!!.first()).isEqualTo("one")
+ assertThat(arg.last()).isEqualTo("null")
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringNullableListNullLiteral() {
+ @Serializable @SerialName("test") class TestClass(val arg: List<String?>?)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(null)) { test<TestClass>() }
+
+ navController.navigate(TestClass(listOf("one", "null")))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg!!.first()).isEqualTo("one")
+ assertThat(arg.last()).isNull()
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateWithObjectStringNullableListNullString() {
+ @Serializable @SerialName("test") class TestClass(val arg: List<String?>?)
+
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = TestClass(null)) { test<TestClass>() }
+
+ navController.navigate(TestClass(listOf("one", null)))
+ assertThat(navController.currentDestination?.route).isEqualTo("test?arg={arg}")
+
+ val arg = navController.currentBackStackEntry?.toRoute<TestClass>()!!.arg
+ assertThat(arg!!.first()).isEqualTo("one")
+ assertThat(arg.last()).isNull()
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateWithObjectNullStringList() {
@Serializable @SerialName("test") class TestClass(val arg: List<String>?)
@@ -5125,6 +5362,56 @@
@UiThreadTest
@Test
+ fun testHandleDeepLinkFromRouteOnlyIfExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val collectedDestinationRoutes = mutableListOf<String?>()
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ collectedDestinationRoutes.add(destination.route)
+ }
+
+ assertThat(collectedDestinationRoutes).containsExactly("start")
+
+ val intent =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("explicit_start_deeplink")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return true with valid deep link")
+ .that(navController.handleDeepLink(intent))
+ .isTrue()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+
+ val intent2 =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("start")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return false with invalid deep link")
+ .that(navController.handleDeepLink(intent2))
+ .isFalse()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+ }
+
+ @UiThreadTest
+ @Test
fun testHandleDeepLinkToRootInvalid() {
val navController = createNavController()
navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index ae11abb..1fbe388d 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -313,9 +313,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open destination " +
- "NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"graph\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [intArg]"
)
}
@@ -339,9 +339,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph/{intArg} can't be used to " +
- "open destination NavGraph(0xf9423909) startDestination=0x0.\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"graph/{intArg}\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -365,9 +365,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open " +
- "destination NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"graph\" for destination NavGraph(0x0) " +
+ "startDestination=0x0. Following required arguments " +
+ "are missing: [intArg, longArg]"
)
}
@@ -389,9 +389,8 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1 can't be used to open " +
- "destination Destination(0xa1f3a662).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"dest1\" for destination " +
+ "Destination(0x0). Following required arguments are missing: [intArg]"
)
}
@@ -420,9 +419,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1/{intArg} can't be used to " +
- "open destination Destination(0x994aa5a8).\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"dest1/{intArg}\" for " +
+ "destination Destination(0x0). Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -448,9 +447,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest can't be used to open " +
- "destination Destination(0x78d64faf).\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"dest\" for destination Destination(0x0). Following required arguments are missing: [intArg, longArg]"
)
}
@@ -1874,8 +1871,12 @@
val nestedId = ("android-app://androidx.navigation/nested/{longArg}").hashCode()
- val expected = assertFailsWith<NullPointerException> { navController.navigate(nestedId) }
- assertThat(expected.message).isEqualTo("null cannot be cast to non-null type kotlin.Long")
+ val expected =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(nestedId) }
+ assertThat(expected.message)
+ .isEqualTo(
+ "Cannot navigate to startDestination Destination(0x893cce52) route=dest2/{longArg}. Missing required arguments [[longArg]]"
+ )
}
@UiThreadTest
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 0075458..1d6d518 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1683,7 +1683,7 @@
return null
}
// if not matched by routePattern, try matching with route args
- if (_graph!!.route == route || _graph!!.matchDeepLink(route) != null) {
+ if (_graph!!.route == route || _graph!!.matchRoute(route) != null) {
return _graph
}
return backQueue.getTopGraph().findNode(route)
@@ -2150,7 +2150,7 @@
backStackMap.values.removeAll { it == backStackId }
val backStackState = backStackStates.remove(backStackId)
- val matchingDeepLink = matchingDestination.matchDeepLink(route)
+ val matchingDeepLink = matchingDestination.matchRoute(route)
// check if the topmost NavBackStackEntryState contains the arguments in this
// matchingDeepLink. If not, we didn't find the correct stack.
val isCorrectStack =
@@ -2424,11 +2424,35 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ requireNotNull(_graph) {
+ "Cannot navigate to $route. Navigation graph has not been set for " +
+ "NavController $this."
+ }
+ val currGraph = backQueue.getTopGraph()
+ val deepLinkMatch =
+ currGraph.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = true,
+ lastVisited = currGraph
+ )
+ if (deepLinkMatch != null) {
+ val destination = deepLinkMatch.destination
+ val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+ val node = deepLinkMatch.destination
+ val intent =
+ Intent().apply {
+ setDataAndType(createRoute(destination.route).toUri(), null)
+ action = null
+ }
+ args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+ navigate(node, args, navOptions, navigatorExtras)
+ } else {
+ throw IllegalArgumentException(
+ "Navigation destination that matches route $route cannot be found in the " +
+ "navigation graph $_graph"
+ )
+ }
}
/**
@@ -2470,12 +2494,7 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- val finalRoute = generateRouteFilled(route)
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(finalRoute).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ navigate(generateRouteFilled(route), navOptions, navigatorExtras)
}
/**
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index 6b926eb..8b2401e 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -49,7 +49,7 @@
testImplementation(libs.junit)
testImplementation(libs.googleCompileTesting)
- testImplementation(projectOrArtifact(":room:room-compiler-processing-testing"), {
+ testImplementation(project(":room:room-compiler-processing-testing"), {
exclude group: "androidx.room", module: "room-compiler-processing"
})
testImplementationAarAsJar(project(":navigation:navigation-common"))
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index 82e6e99..4d38bfa 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -27,6 +27,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
import androidx.navigation.navOptions
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -86,6 +87,26 @@
}
@Test
+ fun testSupportingPaneLifecycle() {
+ val navigator = SupportingPaneTestNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, false)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
fun testWithTransitionLifecycle() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -129,6 +150,58 @@
}
@Test
+ fun testWithSupportingPaneTransitionLifecycle() {
+ val navigator = SupportingPaneTestTransitionNavigator()
+ navigator.onAttach(state)
+ val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ navigator.navigate(listOf(firstEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+ navigator.navigate(listOf(secondEntry), null, null)
+ // Both are started because they are SupportingPane destinations
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(secondEntry)
+ // Even though the secondEntry has completed its transition, the firstEntry
+ // hasn't completed its transition, so it shouldn't be resumed yet
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ state.markTransitionComplete(firstEntry)
+ // Both are resumed because they are SupportingPane destinations that have finished
+ // their transitions
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ navigator.popBackStack(secondEntry, true)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ state.markTransitionComplete(firstEntry)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ state.markTransitionComplete(secondEntry)
+ assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+ val restoredSecondEntry = state.restoreBackStackEntry(secondEntry)
+ navigator.navigate(listOf(restoredSecondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(firstEntry)
+ state.markTransitionComplete(restoredSecondEntry)
+ assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
fun testSameEntry() {
val navigator = TestTransitionNavigator()
navigator.onAttach(state)
@@ -362,6 +435,35 @@
internal class FloatingTestDestination(navigator: Navigator<out NavDestination>) :
NavDestination(navigator), FloatingWindow
+ @Navigator.Name("test")
+ internal class SupportingPaneTestNavigator : Navigator<SupportingPaneTestDestination>() {
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+ }
+
+ @Navigator.Name("test")
+ internal class SupportingPaneTestTransitionNavigator :
+ Navigator<SupportingPaneTestDestination>() {
+
+ override fun createDestination(): SupportingPaneTestDestination =
+ SupportingPaneTestDestination(this)
+
+ override fun navigate(
+ entries: List<NavBackStackEntry>,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ) {
+ entries.forEach { entry -> state.pushWithTransition(entry) }
+ }
+
+ override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+ state.popWithTransition(popUpTo, savedState)
+ }
+ }
+
+ internal class SupportingPaneTestDestination(navigator: Navigator<out NavDestination>) :
+ NavDestination(navigator), SupportingPane
+
class TestViewModel : ViewModel() {
var wasCleared = false
diff --git a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
index beb1d0a..f26e922 100644
--- a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
+++ b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
@@ -25,6 +25,7 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavViewModelStoreProvider
import androidx.navigation.NavigatorState
+import androidx.navigation.SupportingPane
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -174,6 +175,17 @@
} else {
Lifecycle.State.STARTED
}
+ previousEntry.destination is SupportingPane -> {
+ // Match the previous entry's destination, making sure
+ // a transitioning destination does not go to resumed
+ previousEntry.maxLifecycle.coerceAtMost(
+ if (!transitioning) {
+ Lifecycle.State.RESUMED
+ } else {
+ Lifecycle.State.STARTED
+ }
+ )
+ }
previousEntry.destination is FloatingWindow -> Lifecycle.State.STARTED
else -> Lifecycle.State.CREATED
}
diff --git a/navigation/navigation-testing/src/test/java/androidx/navigation/testing/TestSavedStateHandleFactory.kt b/navigation/navigation-testing/src/test/java/androidx/navigation/testing/TestSavedStateHandleFactory.kt
index 4e25fb3..115d839 100644
--- a/navigation/navigation-testing/src/test/java/androidx/navigation/testing/TestSavedStateHandleFactory.kt
+++ b/navigation/navigation-testing/src/test/java/androidx/navigation/testing/TestSavedStateHandleFactory.kt
@@ -98,7 +98,7 @@
val handle = SavedStateHandle(TestClass("null"))
assertThat(handle.contains("arg")).isTrue()
val arg = handle.get<String>("arg")
- assertThat(arg).isNull()
+ assertThat(arg).isEqualTo("null")
}
@Test
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index a1918e9..891a0b3 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -24,17 +24,17 @@
dependencies {
implementation("androidx.arch.core:core-runtime:2.2.0")
- implementation(projectOrArtifact(":room:room-ktx"))
- implementation(projectOrArtifact(":room:room-rxjava2"))
- implementation(projectOrArtifact(":room:room-paging"))
+ implementation(project(":room:room-ktx"))
+ implementation(project(":room:room-rxjava2"))
+ implementation(project(":room:room-paging"))
implementation(project(":paging:paging-common-ktx"))
implementation(project(":paging:paging-runtime"))
implementation(project(":paging:paging-rxjava2"))
- ksp(projectOrArtifact(":room:room-compiler"))
+ ksp(project(":room:room-compiler"))
- implementation(projectOrArtifact(":recyclerview:recyclerview"))
+ implementation(project(":recyclerview:recyclerview"))
implementation("androidx.fragment:fragment-ktx:1.3.0")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation(libs.kotlinStdlib)
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 8d938df..df215df 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -52,16 +52,16 @@
commonTest {
dependencies {
- implementation projectOrArtifact(":compose:ui:ui-tooling")
+ implementation project(":compose:ui:ui-tooling")
implementation(project(":compose:test-utils"))
- implementation(projectOrArtifact(":internal-testutils-paging"))
+ implementation(project(":internal-testutils-paging"))
}
}
jvmTest {
dependsOn(commonTest)
dependencies {
- implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:ui:ui-test-junit4"))
}
}
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index 2541828..901505d 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -37,15 +37,15 @@
implementation("androidx.activity:activity:1.7.1")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
- implementation(projectOrArtifact(":compose:integration-tests:demos:common"))
- implementation(projectOrArtifact(":compose:foundation:foundation"))
- implementation(projectOrArtifact(":compose:material:material"))
- implementation(projectOrArtifact(":paging:paging-compose"))
- implementation(projectOrArtifact(":paging:paging-compose:paging-compose-samples"))
+ implementation(project(":compose:integration-tests:demos:common"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":paging:paging-compose"))
+ implementation(project(":paging:paging-compose:paging-compose-samples"))
- ksp(projectOrArtifact(":room:room-compiler"))
- implementation(projectOrArtifact(":room:room-ktx"))
- implementation(projectOrArtifact(":room:room-paging"))
+ ksp(project(":room:room-compiler"))
+ implementation(project(":room:room-ktx"))
+ implementation(project(":room:room-paging"))
}
androidx {
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e11cacf..c7ec26a 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -50,7 +50,7 @@
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation(project(":internal-testutils-paging"))
androidTestImplementation(project(":paging:paging-testing"))
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/pdf/integration-tests/testapp/build.gradle b/pdf/integration-tests/testapp/build.gradle
index 24a850b..66b4549 100644
--- a/pdf/integration-tests/testapp/build.gradle
+++ b/pdf/integration-tests/testapp/build.gradle
@@ -30,8 +30,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore)
androidTestImplementation(project(":pdf:pdf-viewer-fragment"))
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index d31fa8e..b8d85ac 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -18,11 +18,13 @@
import android.app.Activity
import android.content.ContentResolver
+import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
import android.widget.FrameLayout
import androidx.core.os.BundleCompat
import androidx.core.view.WindowInsetsCompat
@@ -66,6 +68,7 @@
import androidx.pdf.widget.ZoomView
import androidx.pdf.widget.ZoomView.ZoomScroll
import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
import kotlinx.coroutines.launch
/**
@@ -235,7 +238,6 @@
): View? {
super.onCreateView(inflater, container, savedInstanceState)
this.container = container
-
if (!hasContents && delayedContentsAvailable == null) {
if (savedInstanceState != null) {
restoreContents(savedInstanceState)
@@ -298,7 +300,7 @@
}
}
},
- onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+ onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
)
setUpEditFab()
@@ -362,20 +364,25 @@
/** Adjusts the [FindInFileView] to be displayed on top of the keyboard. */
private fun adjustInsetsForSearchMenu(findInFileView: FindInFileView, activity: Activity) {
- val screenHeight = activity.resources.displayMetrics.heightPixels
+ val containerLocation = IntArray(2)
+ container!!.getLocationInWindow(containerLocation)
+
+ val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val screenHeight = windowManager.currentWindowMetrics.bounds.height()
+
val imeInsets =
activity.window.decorView.rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime())
- var menuMargin = 0
val keyboardTop = screenHeight - imeInsets.bottom
- if (container!!.bottom >= keyboardTop) {
- menuMargin = container!!.bottom - keyboardTop
- }
+ val absoluteContainerBottom = container!!.height + containerLocation[1]
+ var menuMargin = 0
+ if (absoluteContainerBottom >= keyboardTop) {
+ menuMargin = absoluteContainerBottom - keyboardTop
+ }
findInFileView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = menuMargin
}
-
isSearchMenuAdjusted = true
}
@@ -494,8 +501,11 @@
savedState?.let { state ->
if (isFileRestoring) {
state.containsKey(KEY_LAYOUT_REACH).let {
- val layoutReach = state.getInt(KEY_LAYOUT_REACH)
- layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
+ if (layoutReach != -1) {
+ layoutHandler?.pageLayoutReach = layoutReach
+ layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ }
}
// Restore page selection from saved state if it exists
@@ -529,12 +539,7 @@
paginatedView?.pageViewFactory = updatedPageViewFactory
selectionObserver =
- PageSelectionValueObserver(
- paginatedView!!,
- paginationModel!!,
- pageViewFactory!!,
- requireContext()
- )
+ PageSelectionValueObserver(paginatedView!!, pageViewFactory!!, requireContext())
pdfLoaderCallbacks?.selectionModel?.selection()?.addObserver(selectionObserver)
}
@@ -581,6 +586,7 @@
pdfLoaderCallbacks?.pdfLoader = pdfLoader
layoutHandler = LayoutHandler(pdfLoader)
+ paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
val updatedSelectionModel = PdfSelectionModel(pdfLoader)
updateSelectionModel(updatedSelectionModel)
@@ -604,7 +610,6 @@
selectedMatchObserver =
SelectedMatchValueObserver(
paginatedView!!,
- paginationModel!!,
pageViewFactory!!,
zoomView!!,
layoutHandler!!,
@@ -630,7 +635,7 @@
// app that owns it has been killed by the system. We will still recover,
// but log this.
viewState.set(ViewState.ERROR)
- onLoadDocumentError(e)
+ showLoadingErrorView(e)
}
}
}
@@ -654,9 +659,7 @@
}
private fun destroyContentModel() {
-
pdfLoader?.cancelAll()
-
paginationModel = null
selectionHandles?.destroy()
@@ -737,6 +740,13 @@
)
}
+ private fun showLoadingErrorView(error: Throwable) {
+ context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+ loadingView?.showErrorView(it)
+ }
+ onLoadDocumentError(error)
+ }
+
private fun loadFile(fileUri: Uri) {
Preconditions.checkNotNull(fileUri)
Preconditions.checkArgument(
@@ -759,8 +769,13 @@
try {
validateFileUri(fileUri)
fetchFile(fileUri)
- } catch (e: SecurityException) {
- onLoadDocumentError(e)
+ } catch (error: Exception) {
+ when (error) {
+ is IOException,
+ is SecurityException,
+ is NullPointerException -> showLoadingErrorView(error)
+ else -> throw error
+ }
}
if (localUri != null && localUri != fileUri) {
annotationButton?.hide()
@@ -787,7 +802,7 @@
}
override fun failed(thrown: Throwable) {
- onLoadDocumentError(thrown)
+ showLoadingErrorView(thrown)
}
override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 78bb274..595a07f 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -51,8 +51,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore)
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
index 46be5f5..4c55ca7 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
@@ -16,7 +16,10 @@
package androidx.pdf.find;
+import android.app.Activity;
import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -33,17 +36,22 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
import androidx.pdf.R;
+import androidx.pdf.models.MatchRects;
import androidx.pdf.util.Accessibility;
import androidx.pdf.util.CycleRange;
import androidx.pdf.util.ObservableValue;
import androidx.pdf.util.ObservableValue.ValueObserver;
import androidx.pdf.viewer.PaginatedView;
import androidx.pdf.viewer.SearchModel;
+import androidx.pdf.viewer.SelectedMatch;
import androidx.pdf.viewer.loader.PdfLoader;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import java.util.Objects;
/**
* A View that has a search query box, find-next and find-previous button, useful for finding
@@ -52,6 +60,12 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FindInFileView extends LinearLayout {
+ private static final char MATCH_STATUS_COUNTING = '\u2026';
+ private static final String KEY_SUPER = "super";
+ private static final String KEY_IS_SAVED = "is_saved";
+ private static final String KEY_MATCH_RECTS = "match_rects";
+ private static final String KEY_SELECTED_PAGE = "selected_page";
+ private static final String KEY_SELECTED_INDEX = "selected_index";
private TextView mQueryBox;
private ImageView mPrevButton;
@@ -59,14 +73,20 @@
private TextView mMatchStatus;
private View mCloseButton;
private FloatingActionButton mAnnotationButton;
- private FindInFileListener mFindInFileListener;
- private ObservableValue<MatchCount> mMatchCount;
- private SearchModel mSearchModel;
private PaginatedView mPaginatedView;
+
+ private FindInFileListener mFindInFileListener;
private Runnable mOnClosedButtonCallback;
+
+ private SearchModel mSearchModel;
+ private ObservableValue<MatchCount> mMatchCount;
+
private boolean mIsAnnotationIntentResolvable;
- private static final char MATCH_STATUS_COUNTING = '\u2026';
- private static final String TAG = FindInFileView.class.getSimpleName();
+ private boolean mIsRestoring;
+ private int mViewingPage;
+ private int mSelectedMatch;
+ private MatchRects mMatches;
+
private final OnClickListener mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
@@ -164,6 +184,34 @@
this.setFocusableInTouchMode(true);
}
+ @NonNull
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(KEY_SUPER, super.onSaveInstanceState());
+ if (mSearchModel != null && mSearchModel.selectedMatch().get() != null) {
+ bundle.putBoolean(KEY_IS_SAVED, true);
+ bundle.putParcelable(KEY_MATCH_RECTS, Objects.requireNonNull(
+ mSearchModel.selectedMatch().get()).getPageMatches());
+ bundle.putInt(KEY_SELECTED_PAGE, mSearchModel.getSelectedPage());
+ bundle.putInt(KEY_SELECTED_INDEX,
+ Objects.requireNonNull(mSearchModel.selectedMatch().get()).getSelected());
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ Bundle bundle = (Bundle) state;
+ super.onRestoreInstanceState(bundle.getParcelable(KEY_SUPER, Parcelable.class));
+ if (bundle.getBoolean(KEY_IS_SAVED)) {
+ mIsRestoring = true;
+ mSelectedMatch = bundle.getInt(KEY_SELECTED_INDEX);
+ mViewingPage = bundle.getInt(KEY_SELECTED_PAGE);
+ mMatches = bundle.getParcelable(KEY_MATCH_RECTS, MatchRects.class);
+ }
+ }
+
/**
* Sets the pdfLoader and create a new {@link SearchModel} instance with the given pdfLoader.
*/
@@ -209,6 +257,11 @@
mAnnotationButton.hide();
}
setupFindInFileBtn();
+ WindowCompat.getInsetsController(((Activity) getContext()).getWindow(), this)
+ .show(WindowInsetsCompat.Type.ime());
+ if (mIsRestoring) {
+ restoreSelectedMatch();
+ }
} else {
this.setVisibility(GONE);
}
@@ -220,6 +273,17 @@
this.setVisibility(GONE);
mQueryBox.clearFocus();
mQueryBox.setText("");
+ mIsRestoring = false;
+ }
+
+ private void restoreSelectedMatch() {
+ // If the first match is selected, no need to restore since it will be reselected by default
+ if (mSelectedMatch > 0) {
+ mSearchModel.setSelectedMatch(
+ new SelectedMatch(mSearchModel.query().get(), mViewingPage, mMatches,
+ mSelectedMatch - 1));
+ mSearchModel.selectNextMatch(CycleRange.Direction.FORWARDS, mViewingPage);
+ }
}
private void setupFindInFileBtn() {
@@ -227,11 +291,7 @@
queryBoxRequestFocus();
mCloseButton.setOnClickListener(view -> {
- mOnClosedButtonCallback.run();
- View parentLayout = (View) mCloseButton.getParent();
- mQueryBox.clearFocus();
- mQueryBox.setText("");
- parentLayout.setVisibility(GONE);
+ resetFindInFile();
if (mIsAnnotationIntentResolvable) {
mAnnotationButton.show();
}
@@ -243,8 +303,7 @@
@Override
public boolean onQueryTextChange(@androidx.annotation.Nullable String query) {
if (mSearchModel != null && mPaginatedView != null) {
- mSearchModel.setQuery(query,
- mPaginatedView.getPageRangeHandler().getVisiblePage());
+ mSearchModel.setQuery(query, getViewingPage());
return true;
}
return false;
@@ -305,4 +364,11 @@
private void queryBoxRequestFocus() {
mQueryBox.requestFocus();
}
+
+ private int getViewingPage() {
+ if (mIsRestoring) {
+ return mViewingPage;
+ }
+ return mPaginatedView.getPageRangeHandler().getVisiblePage();
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
index 984cebb..adac774 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/select/SelectionActionMode.java
@@ -39,6 +39,7 @@
import androidx.pdf.viewer.PaginatedView;
import androidx.pdf.viewer.PdfSelectionHandles;
+import java.util.List;
import java.util.Objects;
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -194,9 +195,10 @@
if (pageSelection.getRects().size() == 1 || startHandlerect.intersect(0, 0, screenWidth,
screenHeight)) {
- return pageSelection.getRects().getFirst();
+ return pageSelection.getRects().get(0);
} else if (stopHandleRect.intersect(0, 0, screenWidth, screenHeight)) {
- return pageSelection.getRects().getLast();
+ List<Rect> rects = pageSelection.getRects();
+ return rects.get(rects.size() - 1);
} else {
// Center of the view in page coordinates
int viewCentreX = mPaginatedView.getViewArea().centerX()
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
index 9314bd2..ddcc5bb 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
@@ -29,15 +29,12 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class PageSelectionValueObserver implements ObservableValue.ValueObserver<PageSelection> {
private final PaginatedView mPaginatedView;
- private final PaginationModel mPaginationModel;
private final PageViewFactory mPageViewFactory;
private Context mContext;
public PageSelectionValueObserver(@NonNull PaginatedView paginatedView,
- @NonNull PaginationModel paginationModel,
@NonNull PageViewFactory pageViewFactory, @NonNull Context context) {
mPaginatedView = paginatedView;
- mPaginationModel = paginationModel;
mPageViewFactory = pageViewFactory;
mContext = context;
}
@@ -56,13 +53,14 @@
mPageViewFactory.getOrCreatePageView(
newSelection.getPage(),
PaginationUtils.getPageElevationInPixels(mContext),
- mPaginationModel.getPageSize(newSelection.getPage()))
+ mPaginatedView.getModel().getPageSize(newSelection.getPage()))
.setOverlay(new PdfHighlightOverlay(newSelection));
}
}
private boolean isPageCreated(int pageNum) {
- return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ return pageNum < mPaginatedView.getModel().getSize() && mPaginatedView.getViewAt(pageNum)
+ != null;
}
private PageViewFactory.PageView getPage(int pageNum) {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 3cdd35d..f3d3afd 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,6 +18,8 @@
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
@@ -27,6 +29,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.core.os.ParcelCompat;
import androidx.pdf.ViewState;
import androidx.pdf.data.Range;
import androidx.pdf.util.PaginationUtils;
@@ -94,9 +97,6 @@
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- if (mPageRangeHandler != null) {
- mPageRangeHandler.setVisiblePages(null);
- }
mModel.removeObserver(this);
}
@@ -168,6 +168,22 @@
}
}
+ @Nullable
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState, mModel);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(((SavedState) state).getSuperState());
+ mModel = savedState.mModel;
+ mPageRangeHandler = new PageRangeHandler(mModel);
+ requestLayout();
+ }
+
/**
* Returns the current viewport in content coordinates
*/
@@ -314,6 +330,10 @@
@Override
public void removeAllViews() {
+ if (mPageRangeHandler != null) {
+ mPageRangeHandler.setVisiblePages(null);
+ }
+
for (int i = 0; i < mPageViews.size(); i++) {
mPageViews.valueAt(i).clearAll();
}
@@ -522,4 +542,24 @@
public boolean isConfigurationChanged() {
return mIsConfigurationChanged;
}
+
+ static class SavedState extends View.BaseSavedState {
+ final PaginationModel mModel;
+
+ SavedState(Parcelable superState, PaginationModel model) {
+ super(superState);
+ mModel = model;
+ }
+
+ SavedState(Parcel source, ClassLoader loader) {
+ super(source);
+ mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeParcelable(mModel, flags);
+ }
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 8407a743..3279dfe 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,8 +16,11 @@
package androidx.pdf.viewer;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -57,7 +60,8 @@
* pages are added
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PaginationModel {
+@SuppressLint("BanParcelableUsage")
+public class PaginationModel implements Parcelable {
/**
* The spacing added before and after each page (the actual space between 2 consecutive pages is
* twice this distance), in pixels.
@@ -87,6 +91,28 @@
mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
}
+ protected PaginationModel(@NonNull Parcel in) {
+ mPageSpacingPx = in.readInt();
+ mMaxPages = in.readInt();
+ mPages = in.createTypedArray(Dimensions.CREATOR);
+ mPageStops = in.createIntArray();
+ mSize = in.readInt();
+ mEstimatedPageHeight = in.readFloat();
+ mAccumulatedPageSize = in.readInt();
+ }
+
+ public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
+ @Override
+ public PaginationModel createFromParcel(Parcel in) {
+ return new PaginationModel(in);
+ }
+
+ @Override
+ public PaginationModel[] newArray(int size) {
+ return new PaginationModel[size];
+ }
+ };
+
/**
* Initializes the model.
*
@@ -264,7 +290,6 @@
}
-
/**
* Returns the location of the page in the model.
*
@@ -277,7 +302,7 @@
* maximizes the portion of that view that is visible on the screen
* </ul>
*
- * @param pageNum - index of requested page
+ * @param pageNum - index of requested page
* @param viewArea - the current viewport in content coordinates
* @return - coordinates of the page within this model
*/
@@ -391,4 +416,20 @@
mObservers.clear();
super.finalize();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPageSpacingPx);
+ dest.writeInt(mMaxPages);
+ dest.writeTypedArray(mPages, flags);
+ dest.writeIntArray(mPageStops);
+ dest.writeInt(mSize);
+ dest.writeFloat(mEstimatedPageHeight);
+ dest.writeInt(mAccumulatedPageSize);
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index 02df1c8..c7ff01b 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -286,12 +286,12 @@
mPaginatedView.setPageViewFactory(mPageViewFactory);
mSelectionObserver =
- new PageSelectionValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ new PageSelectionValueObserver(mPaginatedView, mPageViewFactory,
requireContext());
mSelectionModel.selection().addObserver(mSelectionObserver);
mSelectedMatchObserver =
- new SelectedMatchValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+ new SelectedMatchValueObserver(mPaginatedView, mPageViewFactory,
mZoomView, mLayoutHandler, requireContext());
mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
index 626930d..09438bf 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchModel.java
@@ -352,4 +352,9 @@
public static String whiteSpaceToNull(@NonNull String query) {
return (query != null && TextUtils.isGraphic(query)) ? query : null;
}
+
+ /** Update the current selected match */
+ public void setSelectedMatch(@NonNull SelectedMatch selectedMatch) {
+ mSelectedMatch.set(selectedMatch);
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
index 9979a5d..9da8f85 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatch.java
@@ -46,7 +46,8 @@
* the
* matches is selected, or one with no matches.
*/
- SelectedMatch(String query, int page, MatchRects pageMatches, int selected) {
+ public SelectedMatch(@Nullable String query, int page, @Nullable MatchRects pageMatches,
+ int selected) {
Preconditions.checkNotNull(query);
Preconditions.checkNotNull(pageMatches);
Preconditions.checkArgument(!pageMatches.isEmpty(), "Cannot select empty matches");
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
index 55a9e21..1acf4b2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
@@ -29,18 +29,16 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class SelectedMatchValueObserver implements ObservableValue.ValueObserver<SelectedMatch> {
private final PaginatedView mPaginatedView;
- private final PaginationModel mPaginationModel;
private final PageViewFactory mPageViewFactory;
private final LayoutHandler mLayoutHandler;
private final ZoomView mZoomView;
private final Context mContext;
public SelectedMatchValueObserver(@NonNull PaginatedView paginatedView,
- @NonNull PaginationModel paginationModel, @NonNull PageViewFactory pageViewFactory,
+ @NonNull PageViewFactory pageViewFactory,
@NonNull ZoomView zoomView, @NonNull LayoutHandler layoutHandler,
@NonNull Context context) {
mPaginatedView = paginatedView;
- mPaginationModel = paginationModel;
mPageViewFactory = pageViewFactory;
mZoomView = zoomView;
mLayoutHandler = layoutHandler;
@@ -67,26 +65,27 @@
}
private boolean isPageCreated(int pageNum) {
- return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+ return pageNum < mPaginatedView.getModel().getSize() && mPaginatedView.getViewAt(pageNum)
+ != null;
}
private void lookAtSelection(SelectedMatch selection) {
if (selection == null || selection.isEmpty()) {
return;
}
- if (selection.getPage() >= mPaginationModel.getSize()) {
+ if (selection.getPage() >= mPaginatedView.getModel().getSize()) {
mLayoutHandler.layoutPages(selection.getPage() + 1);
return;
}
Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
- int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
- int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
+ int x = mPaginatedView.getModel().getLookAtX(selection.getPage(), rect.centerX());
+ int y = mPaginatedView.getModel().getLookAtY(selection.getPage(), rect.centerY());
mZoomView.centerAt(x, y);
PageMosaicView pageView = mPageViewFactory.getOrCreatePageView(
selection.getPage(),
PaginationUtils.getPageElevationInPixels(mContext),
- mPaginationModel.getPageSize(selection.getPage()));
+ mPaginatedView.getModel().getPageSize(selection.getPage()));
pageView.setOverlay(selection.getOverlay());
}
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index b5fbe27..820d9d5 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -210,10 +210,7 @@
searchModel?.setNumPages(numPages)
}
- if (isTextSearchActive) {
- findInFileView.setFindInFileView(true)
- findInFileView.setVisibility(VISIBLE)
- }
+ findInFileView.setFindInFileView(isTextSearchActive)
}
override fun documentNotLoaded(status: PdfStatus) {
@@ -228,9 +225,6 @@
"Document not loaded but status " + status.number
)
PdfStatus.PDF_ERROR -> {
- loadingView.showErrorView(
- context.resources.getString(R.string.error_cannot_open_pdf)
- )
handleError(status)
}
PdfStatus.FILE_ERROR,
@@ -259,9 +253,7 @@
override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
if (viewState.get() != ViewState.NO_VIEW) {
-
paginatedView.model.addPage(pageNum, dimensions)
-
layoutHandler!!.pageLayoutReach = paginatedView.model.size
if (
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
index c34e569..1fbd636 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/FastScrollView.java
@@ -150,6 +150,7 @@
super.onViewRemoved(child);
// Prevent leaks if ZoomView is removed from this ViewGroup.
if (child instanceof ZoomView && child == mZoomView) {
+ mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
mZoomView = null;
}
}
@@ -165,9 +166,7 @@
mZoomViewBasePadding =
new Rect(
mZoomView.getPaddingLeft(),
- mZoomView.getPaddingTop()
- + getResources().getDimensionPixelSize(
- R.dimen.viewer_doc_additional_top_offset),
+ mZoomView.getPaddingTop(),
mZoomView.getPaddingRight(),
mZoomView.getPaddingBottom());
mZoomViewConfigured = true;
@@ -183,9 +182,9 @@
if (mZoomView != null) {
mZoomView.setPadding(
0,
- mZoomViewBasePadding.top + insetsCompat.top,
+ mZoomViewBasePadding.top,
0,
- mZoomViewBasePadding.bottom + insetsCompat.bottom);
+ mZoomViewBasePadding.bottom);
setScrollbarMarginTop(mZoomView.getPaddingTop());
// Ignore ZoomView's intrinsic padding on the right side as we want it to be
// right-anchored
@@ -200,7 +199,6 @@
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mZoomView != null && mZoomViewConfigured) {
- mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
mZoomViewConfigured = false;
}
if (mPaginationModel != null) {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
index e5a021c..9a25ff1 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
@@ -142,6 +142,7 @@
private static final String KEY_RAW_BOUNDS = "b";
private static final String KEY_PADDING = "pa";
private static final String KEY_LOOKAT_POINT = "l";
+ private static final String KEY_DEFAULT_ZOOM_CHANGED = "dzc";
private static final int OVERSCROLL_THRESHOLD = 25;
/** Fallback duration for the zoom animation, when material attributes are unavailable. */
private static final int FALLBACK_ZOOM_ANIMATION_DURATION_MS = 250;
@@ -216,6 +217,7 @@
private boolean mStraightenVerticalScroll;
private boolean mInitialZoomDone = false;
private boolean mScaleInProgress = false;
+ private boolean mDefaultZoomChanged = false;
/**
* Padding changes require a {@link #mViewport} update. {@link #setPadding(int, int, int, int)}
* will request a layout pass but {@link #onLayout(boolean, int, int, int, int)} will be called
@@ -573,9 +575,11 @@
// Possibly change the zoom, depending on keepFitZoomOnRotate or RotateMode setting.
if (isFitZoom && mKeepFitZoomOnRotate) {
- setZoom(getConstrainedZoomToFit());
+ setZoom(getDefaultZoom());
} else if (mRotateMode == RotateMode.KEEP_SAME_VIEWPORT_WIDTH) {
- setZoom(constrainZoom(getZoom() * newWidth / oldWidth));
+ setZoom(constrainZoom(mDefaultZoomChanged
+ ? (getZoom() * newWidth / oldWidth)
+ : getDefaultZoom()));
} else if (mRotateMode == RotateMode.KEEP_SAME_VIEWPORT_HEIGHT) {
setZoom(constrainZoom(getZoom() * newHeight / oldHeight));
} else {
@@ -623,7 +627,7 @@
public float getInitialZoom() {
switch (mInitialZoomMode) {
case InitialZoomMode.ZOOM_TO_FIT:
- return getConstrainedZoomToFit();
+ return getDefaultZoom();
case InitialZoomMode.MIN_ZOOM:
return getMinZoom();
case InitialZoomMode.MAX_ZOOM:
@@ -669,6 +673,19 @@
return mMaxZoom;
}
+ private float getDefaultZoom() {
+ Screen screenUtils = new Screen(getContext());
+ int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels;
+ int screenWidthDp = screenUtils.dpFromPx(screenWidth);
+ if (screenWidthDp > 840) {
+ // Add paddings on both sides for large form factors
+ float viewingScreen = mViewport.width() - screenUtils.pxFromDp(160);
+
+ return viewingScreen / mContentView.getWidth();
+ }
+ return getConstrainedZoomToFit();
+ }
+
/** Set the maximum zoom - also configurable in XML. Returns this. */
public void setMaxZoom(float maxZoom) {
this.mMaxZoom = maxZoom;
@@ -884,6 +901,7 @@
public void setZoom(float zoom, float pivotX, float pivotY) {
zoom = Float.isNaN(zoom) ? ZOOM_RESET : zoom;
mInitialZoomDone = true;
+ mDefaultZoomChanged = getDefaultZoom() != zoom;
int deltaX = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotX,
getScrollX());
int deltaY = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotY,
@@ -1050,6 +1068,7 @@
bundle.putParcelable(KEY_RAW_BOUNDS, mContentRawBounds);
bundle.putParcelable(KEY_PADDING, mPaddingOnLastViewportUpdate);
bundle.putParcelable(KEY_LOOKAT_POINT, computeLookAtPoint());
+ bundle.putBoolean(KEY_DEFAULT_ZOOM_CHANGED, mDefaultZoomChanged);
}
return bundle;
}
@@ -1068,6 +1087,7 @@
mContentRawBounds.set(Objects.requireNonNull(bundle.getParcelable(KEY_RAW_BOUNDS)));
mPaddingOnLastViewportUpdate = bundle.getParcelable(KEY_PADDING);
mRestoreLookAtPoint = bundle.getParcelable(KEY_LOOKAT_POINT);
+ mDefaultZoomChanged = bundle.getBoolean(KEY_DEFAULT_ZOOM_CHANGED);
}
}
diff --git a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
index 176e323..2f655f7 100644
--- a/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
+++ b/pdf/pdf-viewer/src/main/res/layout/find_in_file.xml
@@ -34,14 +34,15 @@
android:hint="@string/hint_find"
android:textColor="?attr/colorOnSurface"
android:textColorHint="?attr/colorOutline"
- android:paddingLeft="16dp"
+ android:paddingLeft="14dp"
android:imeOptions="actionSearch"
android:inputType="textFilter"
- android:textSize="20sp"
+ android:textSize="16sp"
android:clickable="true"
android:focusable="true"
android:background="@null"
- style="@style/TextAppearance.Material3.TitleMedium">
+ style="@style/TextAppearance.Material3.TitleSmall"
+ android:layout_gravity="center_vertical">
</androidx.pdf.widget.SearchEditText>
<TextView android:id="@+id/match_status_textview"
@@ -49,7 +50,8 @@
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/query_box"
android:paddingRight="10dp"
- android:textColor="?attr/colorOnSurfaceVariant">
+ android:textColor="?attr/colorOnSurfaceVariant"
+ android:layout_gravity="center_vertical">
</TextView>
</LinearLayout>
diff --git a/pdf/pdf-viewer/src/main/res/values-af/strings.xml b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
index 3efa5d2..a35820c 100644
--- a/pdf/pdf-viewer/src/main/res/values-af/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-af/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kon nie die lêer oopmaak nie. Moontlike toestemmingkwessie?"</string>
<string name="page_broken" msgid="2968770793669433462">"Bladsy is vir die PDF-dokument gebreek"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende data om die PDF-dokument te verwerk"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan nie PDF-lêer oopmaak nie"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-am/strings.xml b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
index a2679692..02183fd 100644
--- a/pdf/pdf-viewer/src/main/res/values-am/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-am/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ፋይሉን መክፈት አልተሳካም። የፈቃድ ችግር ሊሆን ይችላል?"</string>
<string name="page_broken" msgid="2968770793669433462">"ለPDF ሰነዱ ገፅ ተበላሽቷል"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ሰነዱን ለማሰናዳት በቂ ያልሆነ ውሂብ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ፋይል መክፈት አይቻልም"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
index eae5948..0bddaff 100644
--- a/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ar/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"تعذّر فتح الملف. هل توجد مشكلة محتملة في الأذونات؟"</string>
<string name="page_broken" msgid="2968770793669433462">"تعذّر تحميل صفحة من مستند PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"البيانات غير كافية لمعالجة مستند PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"يتعذّر فتح ملف PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-as/strings.xml b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
index f9c9ef7..c165c74 100644
--- a/pdf/pdf-viewer/src/main/res/values-as/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-as/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ফাইলটো খুলিব পৰা নগ’ল। সম্ভাব্য অনুমতি সম্পৰ্কীয় সমস্যা?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF নথিৰ বাবে পৃষ্ঠাখন বিসংগতিপূৰ্ণ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF নথিখন প্ৰক্ৰিয়াকৰণ কৰিবলৈ অপৰ্যাপ্ত ডেটা"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ফাইল খুলিব নোৱাৰি"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-az/strings.xml b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
index 6ef1751..3222a01 100644
--- a/pdf/pdf-viewer/src/main/res/values-az/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-az/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Fayl açılmadı. İcazə problemi var?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF sənədi üçün səhifədə xəta var"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF sənədini emal etmək üçün kifayət qədər data yoxdur"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF faylını açmaq olmur"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
index f259654..e6cbe55 100644
--- a/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-b+sr+Latn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspelo. Možda postoje problemi sa dozvolom?"</string>
<string name="page_broken" msgid="2968770793669433462">"Neispravna stranica za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Otvaranje PDF fajla nije uspelo"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-be/strings.xml b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
index c951b2c..0ca6aa3 100644
--- a/pdf/pdf-viewer/src/main/res/values-be/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-be/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не ўдалося адкрыць файл. Магчыма, праблема з дазволам?"</string>
<string name="page_broken" msgid="2968770793669433462">"Старонка дакумента PDF пашкоджана"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Не хапае даных для апрацоўкі дакумента PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не ўдаецца адкрыць файл PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
index 937be09..063be07 100644
--- a/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ফাইল খোলা যায়নি। অনুমতি সংক্রান্ত সমস্যার কারণে এটি হতে পারে?"</string>
<string name="page_broken" msgid="2968770793669433462">"পিডিএফ ডকুমেন্টের ক্ষেত্রে পৃষ্ঠা ভেঙে গেছে"</string>
<string name="needs_more_data" msgid="3520133467908240802">"পিডিএফ ডকুমেন্ট প্রসেস করার জন্য যথেষ্ট ডেটা নেই"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ফাইল খোলা যাচ্ছে না"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
index cd49b7a..9d4f998 100644
--- a/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-bs/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje fajla nije uspjelo. Možda postoji problem s odobrenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je prelomljena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nije moguće otvoriti PDF fajl"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
index cdc4b65..259ed4a 100644
--- a/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-cs/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Soubor se nepodařilo otevřít. Může se jednat o problém s oprávněním."</string>
<string name="page_broken" msgid="2968770793669433462">"Dokument PDF obsahuje poškozenou stránku"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nedostatek dat ke zpracování dokumentu PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Soubor PDF se nepodařilo otevřít"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-da/strings.xml b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
index 5bcd02c..d132b90e 100644
--- a/pdf/pdf-viewer/src/main/res/values-da/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-da/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Filen kunne ikke åbnes Mon der er et problem med tilladelserne?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Der er ikke nok data til at behandle PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-filen kan ikke åbnes"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-de/strings.xml b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
index 72b1096..7e7ed4c 100644
--- a/pdf/pdf-viewer/src/main/res/values-de/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-de/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Datei konnte nicht geöffnet werden. Möglicherweise ein Berechtigungsproblem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Seite für PDF-Dokument ist fehlerhaft"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Keine ausreichenden Daten, um das PDF-Dokument zu verarbeiten"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF‑Datei kann nicht geöffnet werden"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-el/strings.xml b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
index ada33d8..95d082c 100644
--- a/pdf/pdf-viewer/src/main/res/values-el/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-el/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Δεν ήταν δυνατό το άνοιγμα του αρχείου. Μήπως υπάρχει κάποιο πρόβλημα με την άδεια;"</string>
<string name="page_broken" msgid="2968770793669433462">"Δεν ήταν δυνατή η φόρτωση του εγγράφου PDF από τη σελίδα"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Μη επαρκή δεδομένα για την επεξεργασία του εγγράφου PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Δεν είναι δυνατό το άνοιγμα του αρχείου PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rAU/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rGB/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
index 2f9e726..41add4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-en-rIN/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Failed to open the file. Possible permission issue?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page broken for the PDF document"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Insufficient data for processing the PDF document"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Can\'t open PDF file"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
index 388c071..eadfc4b 100644
--- a/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es-rUS/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"No se pudo abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"No hay datos suficientes para procesar el documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"No se puede abrir el archivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-es/strings.xml b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
index b1aca5f..6fdeb58 100644
--- a/pdf/pdf-viewer/src/main/res/values-es/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-es/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"No se ha podido abrir el archivo. ¿Puede que se deba a un problema de permisos?"</string>
<string name="page_broken" msgid="2968770793669433462">"La página no funciona para el documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Datos insuficientes para procesar el documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"No se puede abrir el archivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-et/strings.xml b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
index d640ef5..91eb54d3 100644
--- a/pdf/pdf-viewer/src/main/res/values-et/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-et/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Faili avamine nurjus. Probleem võib olla seotud lubadega."</string>
<string name="page_broken" msgid="2968770793669433462">"Rikutud leht PDF-dokumendis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF-dokumendi töötlemiseks pole piisavalt andmeid"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-faili ei saa avada"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
index 20770aa..ba3a027 100644
--- a/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-eu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Ezin izan da ireki fitxategia. Agian ez duzu horretarako baimenik?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumentuaren orria hondatuta dago"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ez dago behar adina daturik PDF dokumentua prozesatzeko"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ezin da ireki PDF fitxategia"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
index 0669b17..30063e7 100644
--- a/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fa/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"فایل باز نشد. احتمالاً مشکلی در اجازه وجود دارد؟"</string>
<string name="page_broken" msgid="2968770793669433462">"صفحه سند PDF خراب است"</string>
<string name="needs_more_data" msgid="3520133467908240802">"دادهها برای پردازش سند PDF کافی نیست"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"فایل PDF باز نشد"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
index 3518c19..bde5225 100644
--- a/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fi/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Tiedoston avaaminen epäonnistui. Mahdollinen lupaan liittyvä ongelma?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF-dokumenttiin liittyvä sivu on rikki"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Riittämätön data PDF-dokumentin käsittelyyn"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF-tiedostoa ei voi avata"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
index 8a25992..7e7ac2c 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr-rCA/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation éventuel?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page brisée pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossible d\'ouvrir le fichier PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
index dd5fcad..ff3027c 100644
--- a/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-fr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Échec de l\'ouverture du fichier. Problème d\'autorisation possible ?"</string>
<string name="page_broken" msgid="2968770793669433462">"Page non fonctionnelle pour le document PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Données insuffisantes pour le traitement du document PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossible d\'ouvrir le fichier PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
index ebe0816..9cfb9c7 100644
--- a/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Produciuse un erro ao abrir o ficheiro. É posible que haxa problemas co permiso?"</string>
<string name="page_broken" msgid="2968770793669433462">"Non funciona a páxina para o documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Os datos non son suficientes para procesar o documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Non se puido abrir o PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
index ae95929..f3dfb43 100644
--- a/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-gu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ફાઇલ ખોલવામાં નિષ્ફળ રહ્યાં. શું તમારી પાસે આની પરવાનગી નથી?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF દસ્તાવેજ માટે પેજ લોડ થઈ રહ્યું નથી"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF દસ્તાવેજ પર પ્રક્રિયા કરવા માટે પર્યાપ્ત ડેટા નથી"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ફાઇલ ખોલી શકાતી નથી"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
index 7248c6ee..71cafc1 100644
--- a/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Otvaranje datoteke nije uspjelo. Možda postoji problem s dopuštenjem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Stranica je raščlanjena za PDF dokument"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nema dovoljno podataka za obradu PDF dokumenta"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF datoteka ne može se otvoriti"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
index f63a921..df1766c 100644
--- a/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nem sikerült megnyitni a fájlt. Engedéllyel kapcsolatos problémáról lehet szó?"</string>
<string name="page_broken" msgid="2968770793669433462">"Az oldal nem tölt be a PDF-dokumentumban"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nem áll rendelkezésre elegendő adat a PDF-dokumentum feldolgozásához"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nem sikerült megnyitni a PDF-fájlt"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
index 71d24c2..5faf6897 100644
--- a/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-hy/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Չհաջողվեց բացել ֆայլը։ Գուցե թույլտվության հետ կապված խնդի՞ր է։"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF փաստաթղթի էջը վնասված է"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ոչ բավարար տվյալներ PDF փաստաթղթի մշակման համար"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Չհաջողվեց բացել PDF ֆայլը"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-in/strings.xml b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
index 3c9e624..c71a4e2 100644
--- a/pdf/pdf-viewer/src/main/res/values-in/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-in/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka file. Kemungkinan masalah izin?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman dokumen PDF rusak"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak cukup untuk memproses dokumen PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Tidak dapat membuka file PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-is/strings.xml b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
index 9378e05..9fe38d7 100644
--- a/pdf/pdf-viewer/src/main/res/values-is/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-is/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Ekki tókst að opna skrána. Hugsanlega vandamál tengt heimildum?"</string>
<string name="page_broken" msgid="2968770793669433462">"Síða í PDF-skjali er gölluð"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Ekki næg gögn fyrir úrvinnslu á PDF-skjali"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ekki tókst að opna PDF-skrá"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-it/strings.xml b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
index 89a8d17..e2acf03 100644
--- a/pdf/pdf-viewer/src/main/res/values-it/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-it/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Impossibile aprire il file. Possibile problema di autorizzazione?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina inaccessibile per il documento PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dati insufficienti per l\'elaborazione del documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Impossibile aprire il file PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
index 3dfd510..a450133 100644
--- a/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-iw/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"לא ניתן לפתוח את הקובץ. יכול להיות שיש בעיה בהרשאה."</string>
<string name="page_broken" msgid="2968770793669433462">"קישור מנותק בדף למסמך ה-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"אין מספיק נתונים כדי לעבד את מסמך ה-PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"לא ניתן לפתוח את קובץ ה-PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
index 5846202..01429a0 100644
--- a/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файл ашылмады. Бәлкім, рұқсатқа қатысты бір мәселе бар?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF құжатының беті бұзылған."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF құжатын өңдеу үшін деректер жеткіліксіз."</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файлын ашу мүмкін емес."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-km/strings.xml b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
index 931c43f..ecde5c2 100644
--- a/pdf/pdf-viewer/src/main/res/values-km/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-km/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"មិនអាចបើកឯកសារនេះបានទេ។ អាចមានបញ្ហានៃការអនុញ្ញាតឬ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ទំព័រមិនដំណើរការសម្រាប់ឯកសារ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"មានទិន្នន័យមិនគ្រប់គ្រាន់សម្រាប់ដំណើរការឯកសារ PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"មិនអាចបើកឯកសារ PDF បានទេ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
index fb8b009..c023a93 100644
--- a/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-kn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ಫೈಲ್ ತೆರೆಯಲು ವಿಫಲವಾಗಿದೆ. ಸಂಭವನೀಯ ಅನುಮತಿ ಸಮಸ್ಯೆ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ಡಾಕ್ಯುಮೆಂಟ್ಗೆ ಸಂಬಂಧಿಸಿದ ಪುಟ ಮುರಿದಿದೆ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲು ಸಾಕಷ್ಟು ಡೇಟಾ ಇಲ್ಲ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ಫೈಲ್ ಅನ್ನು ತೆರೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
index 9530691..766dd73 100644
--- a/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ko/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"파일을 열 수 없습니다. 권한 문제가 있을 수 있나요?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 문서의 페이지가 손상되었습니다."</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF 문서 처리를 위한 데이터가 부족합니다."</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF 파일을 열 수 없음"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
index 92e5119..ef64372 100644
--- a/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ky/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файл ачылган жок. Керектүү уруксаттар жок окшойт."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF документинин барагы бузук"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF документин иштетүү үчүн маалымат жетишсиз"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файл ачылбай жатат"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
index baa1c28..f497820 100644
--- a/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lo/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ເປີດໄຟລ໌ບໍ່ສຳເລັດ. ອາດເປັນຍ້ອນບັນຫາທາງການອະນຸຍາດບໍ?"</string>
<string name="page_broken" msgid="2968770793669433462">"ໜ້າເສຍຫາຍສໍາລັບເອກະສານ PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"ຂໍ້ມູນບໍ່ພຽງພໍສໍາລັບການປະມວນຜົນເອກະສານ PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"ບໍ່ສາມາດເປີດໄຟລ໌ PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
index 588ff87..6c3088c 100644
--- a/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lt/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nepavyko atidaryti failo. Galima su leidimais susijusi problema?"</string>
<string name="page_broken" msgid="2968770793669433462">"Sugadintas PDF dokumento puslapis"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepakanka duomenų PDF dokumentui apdoroti"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nepavyksta atidaryti PDF failo"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
index 738615d..a1d28ba 100644
--- a/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-lv/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Neizdevās atvērt failu. Iespējams, ir radusies problēma ar atļaujām."</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokumenta lapa ir bojāta"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Nepietiekams datu apjoms, lai apstrādātu PDF dokumentu"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nevar atvērt PDF failu."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
index e6b85c2..3d48afd 100644
--- a/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не можеше да се отвори датотеката. Можеби има проблем со дозволата?"</string>
<string name="page_broken" msgid="2968770793669433462">"Страницата не може да го вчита PDF-документот"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недоволно податоци за обработка на PDF-документот"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не може да се отвори PDF-датотеката"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
index 760a696..d8bf511 100644
--- a/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ml/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ഫയൽ തുറക്കാനായില്ല. അനുമതി സംബന്ധിച്ച പ്രശ്നമാകാൻ സാധ്യതയുണ്ടോ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ഡോക്യുമെന്റിനായി പേജ് ലോഡ് ചെയ്യാനായില്ല"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ഡോക്യുമെന്റ് പ്രോസസ് ചെയ്യാൻ മതിയായ ഡാറ്റയില്ല"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ഫയൽ തുറക്കാനാകുന്നില്ല"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
index 9c8f052..04d4944 100644
--- a/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mn/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Файлыг нээж чадсангүй. Зөвшөөрөлтэй холбоотой асуудал байж болох уу?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF баримт бичгийн хуудас эвдэрсэн"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF баримт бичгийг боловсруулахад өгөгдөл хангалтгүй байна"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF файлыг нээх боломжгүй"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
index 40ee1a8..3c3c6de 100644
--- a/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-mr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"फाइल उघडता आली नाही. परवानगीशी संबंधित संभाव्य समस्या?"</string>
<string name="page_broken" msgid="2968770793669433462">"पीडीएफ दस्तऐवजासाठी पेज खंडित झाले आहे"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF दस्तऐवजावर प्रक्रिया करण्यासाठी डेटा पुरेसा नाही"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF फाइल उघडू शकत नाही"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
index f905244..af16379 100644
--- a/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ms/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Gagal membuka fail. Kemungkinan terdapat masalah berkaitan dengan kebenaran?"</string>
<string name="page_broken" msgid="2968770793669433462">"Halaman rosak untuk dokumen PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Data tidak mencukupi untuk memproses dokumen PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Tidak dapat membuka fail PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-my/strings.xml b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
index c288df3..27f063f 100644
--- a/pdf/pdf-viewer/src/main/res/values-my/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-my/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ဖိုင်ကို ဖွင့်၍မရလိုက်ပါ။ ခွင့်ပြုချက် ပြဿနာ ဖြစ်နိုင်လား။"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF မှတ်တမ်းအတွက် စာမျက်နှာ ပျက်နေသည်"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF မှတ်တမ်း လုပ်ဆောင်ရန်အတွက် ဒေတာ မလုံလောက်ပါ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ဖိုင်ကို ဖွင့်၍မရပါ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
index 06e5547..64bb49b 100644
--- a/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nb/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kunne ikke åpne filen. Kan det være et problem med tillatelser?"</string>
<string name="page_broken" msgid="2968770793669433462">"Siden er ødelagt for PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Det er utilstrekkelige data for behandling av PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan ikke åpne PDF-filen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
index d847ed0c..109c214 100644
--- a/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ne/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"फाइल खोल्न सकिएन। तपाईंसँग यो फाइल खोल्ने अनुमति छैन?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF डकुमेन्टको पेज लोड गर्न सकिएन"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF डकुमेन्ट प्रोसेस गर्न पर्याप्त जानकारी छैन"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF फाइल खोल्न सकिएन"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
index 5ceea0c..08eb1bd5 100644
--- a/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-nl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Kan het bestand niet openen. Mogelijk rechtenprobleem?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagina van het pdf-document kan niet worden geladen"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Onvoldoende gegevens om het pdf-document te verwerken"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Kan pdf-bestand niet openen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-or/strings.xml b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
index 66b7f68..9623c13 100644
--- a/pdf/pdf-viewer/src/main/res/values-or/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-or/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ଫାଇଲ ଖୋଲିବାରେ ବିଫଳ ହୋଇଛି। ସମ୍ଭାବ୍ୟ ଅନୁମତି ସମସ୍ୟା ଅଛି?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପାଇଁ ପୃଷ୍ଠା ବିଭାଜିତ ହୋଇଛି"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ଡକ୍ୟୁମେଣ୍ଟ ପ୍ରକ୍ରିୟାକରଣ ପାଇଁ ପର୍ଯ୍ୟାପ୍ତ ଡାଟା ନାହିଁ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ଫାଇଲକୁ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
index 8d4c105..edab9fc 100644
--- a/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pa/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ਫ਼ਾਈਲ ਨੂੰ ਖੋਲ੍ਹਣਾ ਅਸਫਲ ਰਿਹਾ। ਕੀ ਸੰਭਵ ਇਜਾਜ਼ਤ ਸੰਬੰਧੀ ਸਮੱਸਿਆ ਹੈ?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ਦਸਤਾਵੇਜ਼ ਲਈ ਪੰਨਾ ਲੋਡ ਨਹੀਂ ਹੋ ਰਿਹਾ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ਦਸਤਾਵੇਜ਼ \'ਤੇ ਪ੍ਰਕਿਰਿਆ ਕਰਨ ਲਈ ਲੋੜੀਂਦਾ ਡਾਟਾ ਨਹੀਂ ਹੈ"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ਫ਼ਾਈਲ ਖੋਲ੍ਹੀ ਨਹੀਂ ਜਾ ਸਕਦੀ"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
index 6583cb5..baa168f 100644
--- a/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pl/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nie udało się otworzyć pliku. Może to przez problem z uprawnieniami?"</string>
<string name="page_broken" msgid="2968770793669433462">"Strona w dokumencie PDF jest uszkodzona"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Brak wystarczającej ilości danych do przetworzenia dokumentu PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nie można otworzyć pliku PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
index e8beb95..6c852a6 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt-rBR/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Não é possível abrir o arquivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
index e8beb95..6c852a6 100644
--- a/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-pt/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Falha ao abrir o arquivo. Possível problema de permissão?"</string>
<string name="page_broken" msgid="2968770793669433462">"Página do documento PDF corrompida"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Dados insuficientes para processamento do documento PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Não é possível abrir o arquivo PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
index ef6b73d..43bec5af 100644
--- a/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ro/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Nu s-a putut deschide fișierul. Există vreo problemă cu permisiunile?"</string>
<string name="page_broken" msgid="2968770793669433462">"Pagină deteriorată pentru documentul PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Date insuficiente pentru procesarea documentului PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Nu se poate deschide fișierul PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
index 665e7cb..7aec0cd 100644
--- a/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ru/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не удалось открыть файл. Возможно, нет необходимых разрешений."</string>
<string name="page_broken" msgid="2968770793669433462">"Страница документа PDF повреждена"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостаточно данных для обработки документа PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не удается открыть PDF-файл."</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-si/strings.xml b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
index dc5d075..482776b8 100644
--- a/pdf/pdf-viewer/src/main/res/values-si/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-si/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ගොනුව විවෘත කිරීමට අසමත් විය. අවසර ගැටලුවක් විය හැකි ද?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ලේඛනය සඳහා පිටුව හානි වී ඇත"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ලේඛනය සැකසීම සඳහා ප්රමාණවත් දත්ත නොමැත"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ගොනුව විවෘත කළ නොහැක"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
index d44c7b6..e214804 100644
--- a/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Súbor sa nepodarilo otvoriť. Možno sa vyskytol problém s povolením."</string>
<string name="page_broken" msgid="2968770793669433462">"Stránka sa v dokumente vo formáte PDF nedá načítať"</string>
<string name="needs_more_data" msgid="3520133467908240802">"V dokumente vo formáte PDF nie je dostatok údajov na spracovanie"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Súbor PDF sa nepodarilo otvoriť"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
index 37347d3..ae8b533 100644
--- a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Hapja e skedarit dështoi. Problem i mundshëm me lejet?"</string>
<string name="page_broken" msgid="2968770793669433462">"Faqe e dëmtuar për dokumentin PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Të dhëna të pamjaftueshme për përpunimin e dokumentit PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Skedari PDF nuk mund të hapet"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
index bcab510..9de9525 100644
--- a/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Отварање фајла није успело. Можда постоје проблеми са дозволом?"</string>
<string name="page_broken" msgid="2968770793669433462">"Неисправна страница за PDF документ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недовољно података за обраду PDF документа"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Отварање PDF фајла није успело"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
index 3fa349f..186bb52 100644
--- a/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sv/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Det gick inte att öppna filen. Detta kan bero på ett behörighetsproblem."</string>
<string name="page_broken" msgid="2968770793669433462">"Det gick inte att läsa in en sida i PDF-dokumentet"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Otillräcklig data för att behandla PDF-dokumentet"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Det går inte att öppna PDF-filen"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
index abf9c4b5..0a46009 100644
--- a/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sw/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Imeshindwa kufungua faili. Je, linaweza kuwa tatizo la ruhusa?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ukurasa wa hati ya PDF una tatizo"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Hamna data ya kutosha kuchakata hati ya PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Imeshindwa kufungua faili ya PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
index 2d1d291..fe6db7a 100644
--- a/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ta/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"ஃபைலைத் திறக்க முடியவில்லை. அனுமதி தொடர்பான சிக்கல் உள்ளதா?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF ஆவணத்தை ஏற்ற முடியவில்லை"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF ஆவணத்தைச் செயலாக்குவதற்குப் போதுமான தரவு இல்லை"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF ஃபைலைத் திறக்க முடியவில்லை"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
index 8ad6bf2..916c0e0 100644
--- a/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-tr/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Dosya açılamadı. İzin sorunundan kaynaklanıyor olabilir mi?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF dokümanının sayfası bozuk"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF dokümanını işleyecek kadar yeterli veri yok"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF dosyası açılamıyor"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
index c381f20..f413bfc 100644
--- a/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-uk/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Не вдалося відкрити файл. Можливо, виникла проблема з дозволом."</string>
<string name="page_broken" msgid="2968770793669433462">"Сторінку документа PDF пошкоджено"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Недостатньо даних для обробки документа PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Не вдалося відкрити файл PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
index 6027e36..f2c3bd2 100644
--- a/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-ur/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"فائل کھولنے میں ناکام۔ کیا یہ اجازت کا مسئلہ ہو سکتا ہے؟"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF دستاویز کیلئے شکستہ صفحہ"</string>
<string name="needs_more_data" msgid="3520133467908240802">"PDF دستاویز پر کارروائی کرنے کیلئے ڈیٹا ناکافی ہے"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"PDF فائل کو کھولا نہیں جا سکتا"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
index 5f50634..165db32 100644
--- a/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-vi/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Không mở được tệp này. Có thể là do vấn đề về quyền?"</string>
<string name="page_broken" msgid="2968770793669433462">"Tài liệu PDF này bị lỗi trang"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Không đủ dữ liệu để xử lý tài liệu PDF này"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Không mở được tệp PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
index 691bdfe..9eb4c8f 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rHK/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。可能有權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件頁面已損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"沒有足夠資料處理 PDF 文件"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"無法開啟 PDF 檔案"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
index b2be3f4..daaadee 100644
--- a/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zh-rTW/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"無法開啟檔案。有可能是權限問題?"</string>
<string name="page_broken" msgid="2968770793669433462">"PDF 文件的頁面損毀"</string>
<string name="needs_more_data" msgid="3520133467908240802">"資料不足,無法處理 PDF 文件"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"無法開啟 PDF 檔案"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
index ba456e5..e58cf20 100644
--- a/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-zu/strings.xml
@@ -53,6 +53,5 @@
<string name="file_error" msgid="4003885928556884091">"Yehlulekile ukuvula ifayela. Inkinga yemvume engaba khona?"</string>
<string name="page_broken" msgid="2968770793669433462">"Ikhasi eliphuliwe ledokhumenti ye-PDF"</string>
<string name="needs_more_data" msgid="3520133467908240802">"Idatha enganele yokucubungula idokhumenti ye-PDF"</string>
- <!-- no translation found for error_cannot_open_pdf (2361919778558145071) -->
- <skip />
+ <string name="error_cannot_open_pdf" msgid="2361919778558145071">"Ayikwazi ukuvula ifayela le-PDF"</string>
</resources>
diff --git a/pdf/pdf-viewer/src/main/res/values/dimensions.xml b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
index 03bf958..584fda6 100644
--- a/pdf/pdf-viewer/src/main/res/values/dimensions.xml
+++ b/pdf/pdf-viewer/src/main/res/values/dimensions.xml
@@ -15,7 +15,6 @@
-->
<resources>
- <dimen name="viewer_doc_additional_top_offset">12dp</dimen>
<dimen name="viewer_fastscroll_edge_offset">24dp</dimen>
<dimen name="viewer_doc_padding_y">4dp</dimen>
<dimen name="viewer_doc_padding_x">0dp</dimen>
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
index 384ab67..9c4b2a6 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
@@ -78,7 +78,7 @@
PdfViewer.setScreenForTest(mContext);
PageSelectionValueObserver pageSelectionValueObserver =
- new PageSelectionValueObserver(mMockPaginatedView, mMockPaginationModel,
+ new PageSelectionValueObserver(mMockPaginatedView,
mMockPageViewFactory, mContext);
pageSelectionValueObserver.onChange(mMockOldPageSelection, mMockNewPageSelection);
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
index 9c15f78..1ef67d9 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
@@ -89,7 +89,7 @@
PdfViewer.setScreenForTest(mContext);
SelectedMatchValueObserver selectedMatchValueObserver = new SelectedMatchValueObserver(
- mMockPaginatedView, mMockPaginationModel, mMockPageViewFactory, mMockZoomView,
+ mMockPaginatedView, mMockPageViewFactory, mMockZoomView,
mMockLayoutHandler, mContext);
selectedMatchValueObserver.onChange(mMockOldSelection, mMockNewSelection);
diff --git a/playground-projects/activity-playground/settings.gradle b/playground-projects/activity-playground/settings.gradle
index 4019de7..26fdaa6 100644
--- a/playground-projects/activity-playground/settings.gradle
+++ b/playground-projects/activity-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply ../../buildSrc/ndk.gradle
+
rootProject.name = "activity-playground"
playground {
diff --git a/playground-projects/appcompat-playground/settings.gradle b/playground-projects/appcompat-playground/settings.gradle
index ef7867c..849c38b 100644
--- a/playground-projects/appcompat-playground/settings.gradle
+++ b/playground-projects/appcompat-playground/settings.gradle
@@ -4,8 +4,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply ../../buildSrc/ndk.gradle
+
rootProject.name = "appcompat-playground"
playground {
diff --git a/playground-projects/biometric-playground/settings.gradle b/playground-projects/biometric-playground/settings.gradle
index 1259d4b..61833b6 100644
--- a/playground-projects/biometric-playground/settings.gradle
+++ b/playground-projects/biometric-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply ../../buildSrc/ndk.gradle
+
rootProject.name = "biometric-playground"
playground {
diff --git a/playground-projects/collection-playground/settings.gradle b/playground-projects/collection-playground/settings.gradle
index 7436415..33fc766 100644
--- a/playground-projects/collection-playground/settings.gradle
+++ b/playground-projects/collection-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply ../../buildSrc/ndk.gradle
+
rootProject.name = "collections-playground"
playground {
diff --git a/playground-projects/compose/runtime-playground/settings.gradle b/playground-projects/compose/runtime-playground/settings.gradle
index f6e803c..805f643 100644
--- a/playground-projects/compose/runtime-playground/settings.gradle
+++ b/playground-projects/compose/runtime-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply ../../buildSrc/ndk.gradle
+
rootProject.name = "compose-runtime"
playground {
diff --git a/playground-projects/core-playground/settings.gradle b/playground-projects/core-playground/settings.gradle
index b395f59..fb46ab0 100644
--- a/playground-projects/core-playground/settings.gradle
+++ b/playground-projects/core-playground/settings.gradle
@@ -4,8 +4,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "core-playground"
playground {
diff --git a/playground-projects/datastore-playground/settings.gradle b/playground-projects/datastore-playground/settings.gradle
index d5b04bd..bbd0dd8 100644
--- a/playground-projects/datastore-playground/settings.gradle
+++ b/playground-projects/datastore-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "datastore-playground"
playground {
diff --git a/playground-projects/fragment-playground/settings.gradle b/playground-projects/fragment-playground/settings.gradle
index bf5e622..e1587c0d 100644
--- a/playground-projects/fragment-playground/settings.gradle
+++ b/playground-projects/fragment-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "fragment-playground"
playground {
diff --git a/playground-projects/ktfmt-playground/settings.gradle b/playground-projects/ktfmt-playground/settings.gradle
index 14b3633..95bc38c 100644
--- a/playground-projects/ktfmt-playground/settings.gradle
+++ b/playground-projects/ktfmt-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
playground {
setupPlayground("../..")
selectProjectsFromAndroidX({ name ->
diff --git a/playground-projects/lifecycle-playground/settings.gradle b/playground-projects/lifecycle-playground/settings.gradle
index c8f4d97..18e73f7 100644
--- a/playground-projects/lifecycle-playground/settings.gradle
+++ b/playground-projects/lifecycle-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "lifecycle-playground"
playground {
diff --git a/playground-projects/navigation-playground/settings.gradle b/playground-projects/navigation-playground/settings.gradle
index 8d9c51f..02dd8fe 100644
--- a/playground-projects/navigation-playground/settings.gradle
+++ b/playground-projects/navigation-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "navigation-playground"
playground {
diff --git a/playground-projects/paging-playground/settings.gradle b/playground-projects/paging-playground/settings.gradle
index 13c078f..6a7f6ca 100644
--- a/playground-projects/paging-playground/settings.gradle
+++ b/playground-projects/paging-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "paging-playground"
playground {
diff --git a/playground-projects/room-playground/settings.gradle b/playground-projects/room-playground/settings.gradle
index 406a91d..f40faf3 100644
--- a/playground-projects/room-playground/settings.gradle
+++ b/playground-projects/room-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "room-playground"
playground {
diff --git a/playground-projects/sqlite-playground/settings.gradle b/playground-projects/sqlite-playground/settings.gradle
index 2ecf4ae..c132192 100644
--- a/playground-projects/sqlite-playground/settings.gradle
+++ b/playground-projects/sqlite-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "sqlite-playground"
playground {
diff --git a/playground-projects/work-playground/settings.gradle b/playground-projects/work-playground/settings.gradle
index ae0868e..3ccf68d 100644
--- a/playground-projects/work-playground/settings.gradle
+++ b/playground-projects/work-playground/settings.gradle
@@ -20,8 +20,11 @@
}
plugins {
id "playground"
+ id "com.android.settings" version "8.7.0-alpha02"
}
+apply from: "../../buildSrc/ndk.gradle"
+
rootProject.name = "work-playground"
playground {
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 27d3503..d3140ab 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.truth)
}
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
index 305ee1a..a66f24d 100644
--- a/privacysandbox/ads/ads-adservices-java/build.gradle
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -40,7 +40,7 @@
api(libs.guavaListenableFuture)
implementation project(path: ':privacysandbox:ads:ads-adservices')
- androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
+ androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation project(path: ':privacysandbox:ads:ads-adservices')
androidTestImplementation project(path: ':javascriptengine:javascriptengine')
androidTestImplementation(libs.junit)
@@ -53,8 +53,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation(libs.dexmakerMockitoInlineExtended)
}
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
index 5610d1a..afa4d60 100644
--- a/privacysandbox/ads/ads-adservices/build.gradle
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestImplementation(libs.dexmakerMockitoInlineExtended)
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index fb5e7c0..8753f06 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -120,8 +120,8 @@
androidTestImplementation(project(':internal-testutils-runtime'))
androidTestImplementation(project(":internal-testutils-truth")) // for assertThrows
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
androidTestBundleDex(project(":privacysandbox:sdkruntime:test-sdks:current"))
androidTestBundleDex(project(":privacysandbox:sdkruntime:test-sdks:v4"))
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
index a8366d3..21ed9ac 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
}
android {
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
index 83865d4..3e22512 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockitoInline)
}
android {
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
index 8f967a8..211bc75 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
@@ -61,6 +61,7 @@
sdkClasspath
.toFile()
.walk()
+ .filter { it.isFile }
.map { it.toPath() }
.filter { shouldKeepFile(sdkClasspath, it) }
.forEach { file ->
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index 2301a5b..3cdbd9a 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -174,6 +174,25 @@
}
@Test
+ fun dirWithClassExtension_ignored() {
+ val source =
+ Source.kotlin(
+ "com/mysdk/Valid.kt",
+ """
+ |package com.mysdk
+ |interface Valid
+ """
+ .trimMargin()
+ )
+ val sdkClasspath = compileAll(listOf(source)).outputClasspath.first().toPath()
+ sdkClasspath.resolve("otherdir.class").createDirectories()
+ val sdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+
+ // Does not throw
+ PrivacySandboxApiPackager().packageSdkDescriptors(sdkClasspath, sdkDescriptor)
+ }
+
+ @Test
fun sdkClasspathDoesNotExist_throwException() {
val invalidClasspathFile = makeTestDirectory().resolve("dir_that_does_not_exist")
val validSdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
diff --git a/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle b/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
index 4ac1624..2073543 100644
--- a/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
+++ b/privacysandbox/ui/integration-tests/mediateesdkprovider/build.gradle
@@ -26,6 +26,7 @@
defaultConfig {
applicationId "androidx.privacysandbox.ui.integration.mediateesdkprovider"
minSdk 33
+ compileSdk 35
}
buildTypes {
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
index 323d9a0..c136012 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/build.gradle
@@ -22,9 +22,11 @@
android {
namespace "androidx.privacysandbox.ui.integration.sdkproviderutils"
+ compileSdk 35
}
dependencies {
+ implementation project(':privacysandbox:ui:ui-client')
implementation project(':privacysandbox:ui:ui-core')
implementation project(':privacysandbox:ui:ui-provider')
implementation project(':privacysandbox:ui:integration-tests:testaidl')
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
index 66da2c5..d466ef1 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/SdkApiConstants.kt
@@ -32,7 +32,8 @@
companion object {
const val NON_MEDIATED = 0
const val SDK_RUNTIME_MEDIATEE = 1
- const val IN_APP_MEDIATEE = 2
+ const val SDK_RUNTIME_MEDIATEE_WITH_OVERLAY = 2
+ const val IN_APP_MEDIATEE = 3
}
}
}
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
index b613689..1d05403 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
@@ -25,6 +25,7 @@
import android.graphics.Paint
import android.graphics.Path
import android.net.Uri
+import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
@@ -37,6 +38,10 @@
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkView
import androidx.privacysandbox.ui.core.SandboxedUiAdapter
import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
import androidx.webkit.WebViewAssetLoader
@@ -46,7 +51,7 @@
class TestAdapters(private val sdkContext: Context) {
inner class TestBannerAd(private val text: String, private val withSlowDraw: Boolean) :
BannerAd() {
- override fun buildAdView(sessionContext: Context): View {
+ override fun buildAdView(sessionContext: Context, width: Int, height: Int): View? {
return TestView(sessionContext, withSlowDraw, text)
}
}
@@ -55,7 +60,7 @@
lateinit var sessionClientExecutor: Executor
lateinit var sessionClient: SandboxedUiAdapter.SessionClient
- abstract fun buildAdView(sessionContext: Context): View?
+ abstract fun buildAdView(sessionContext: Context, width: Int, height: Int): View?
override fun openSession(
context: Context,
@@ -72,7 +77,8 @@
.post(
Runnable lambda@{
Log.d(TAG, "Session requested")
- val adView: View = buildAdView(context) ?: return@lambda
+ val adView: View =
+ buildAdView(context, initialWidth, initialHeight) ?: return@lambda
adView.layoutParams = ViewGroup.LayoutParams(initialWidth, initialHeight)
clientExecutor.execute { client.onSessionOpened(BannerAdSession(adView)) }
}
@@ -112,7 +118,7 @@
) != 0
}
- override fun buildAdView(sessionContext: Context): View? {
+ override fun buildAdView(sessionContext: Context, width: Int, height: Int): View? {
if (isAirplaneModeOn()) {
sessionClientExecutor.execute {
sessionClient.onSessionError(Throwable("Cannot load WebView in airplane mode."))
@@ -128,7 +134,7 @@
inner class VideoBannerAd(private val playerViewProvider: PlayerViewProvider) : BannerAd() {
- override fun buildAdView(sessionContext: Context): View {
+ override fun buildAdView(sessionContext: Context, width: Int, height: Int): View? {
return playerViewProvider.createPlayerView(
sessionContext,
"https://html5demos.com/assets/dizzy.mp4"
@@ -137,7 +143,7 @@
}
inner class WebViewAdFromLocalAssets : BannerAd() {
- override fun buildAdView(sessionContext: Context): View {
+ override fun buildAdView(sessionContext: Context, width: Int, height: Int): View? {
val webView = WebView(sessionContext)
val assetLoader =
WebViewAssetLoader.Builder()
@@ -151,6 +157,35 @@
}
}
+ inner class OverlaidAd(private val mediateeBundle: Bundle) : BannerAd() {
+ override fun buildAdView(sessionContext: Context, width: Int, height: Int): View {
+ val adapter = SandboxedUiAdapterFactory.createFromCoreLibInfo(mediateeBundle)
+ val linearLayout = LinearLayout(sessionContext)
+ linearLayout.orientation = LinearLayout.VERTICAL
+ linearLayout.layoutParams = LinearLayout.LayoutParams(width, height)
+ // The SandboxedSdkView will take up 90% of the parent height, with the overlay taking
+ // the other 10%
+ val ssvParams =
+ LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 0.9f)
+ val overlayParams =
+ LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 0.1f)
+ val sandboxedSdkView = SandboxedSdkView(sessionContext)
+ sandboxedSdkView.setAdapter(adapter)
+ sandboxedSdkView.layoutParams = ssvParams
+ linearLayout.addView(sandboxedSdkView)
+ val textView =
+ TextView(sessionContext).also {
+ it.setBackgroundColor(Color.GRAY)
+ it.text = "Mediator Overlay"
+ it.textSize = 20f
+ it.setTextColor(Color.BLACK)
+ it.layoutParams = overlayParams
+ }
+ linearLayout.addView(textView)
+ return linearLayout
+ }
+ }
+
private inner class TestView(
context: Context,
private val withSlowDraw: Boolean,
@@ -251,7 +286,6 @@
settings.javaScriptEnabled = true
settings.setGeolocationEnabled(true)
settings.setSupportZoom(true)
- settings.databaseEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = true
settings.allowContentAccess = true
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 86fed41..79fdec1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -17,8 +17,11 @@
package androidx.privacysandbox.ui.integration.testapp
import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
}
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+ }
+
/** Returns a handle to the already loaded SDK. */
fun getSdkApi(): ISdkApi {
return sdkApi
@@ -119,6 +126,8 @@
val parent = view.parent as ViewGroup
val index = parent.indexOfChild(view)
val textView = TextView(requireActivity())
+ textView.setTypeface(null, Typeface.BOLD_ITALIC)
+ textView.setTextColor(Color.RED)
textView.text = state.throwable.message
requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 02d4cec..c929aaf 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -192,26 +192,8 @@
isCalledOnStartingApp = false
return
}
- // Mediation is enabled if Runtime-Runtime Mediation option or Runtime-App
- // Mediation
- // option is selected.
- val appOwnedMediationEnabled =
- selectedMediationOptionId == MediationOption.IN_APP_MEDIATEE.toLong()
- val mediationEnabled =
- (selectedMediationOptionId ==
- MediationOption.SDK_RUNTIME_MEDIATEE.toLong() ||
- appOwnedMediationEnabled)
- mediationOption =
- if (mediationEnabled) {
- if (appOwnedMediationEnabled) {
- MediationOption.IN_APP_MEDIATEE
- } else {
- MediationOption.SDK_RUNTIME_MEDIATEE
- }
- } else {
- MediationOption.NON_MEDIATED
- }
+ mediationOption = selectedMediationOptionId.toInt()
loadAllAds()
}
@@ -285,15 +267,15 @@
if (!isDrawerOpen) {
isDrawerOpen = true
currentFragment.handleDrawerStateChange(isDrawerOpen = true)
+ } else if (slideOffset == 0f) {
+ isDrawerOpen = false
+ currentFragment.handleDrawerStateChange(isDrawerOpen = false)
}
}
override fun onDrawerOpened(drawerView: View) {}
- override fun onDrawerClosed(drawerView: View) {
- isDrawerOpen = false
- currentFragment.handleDrawerStateChange(isDrawerOpen = false)
- }
+ override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
} catch (e: Exception) {
Log.w(TAG, "Ad not loaded $e")
}
+ childSandboxedSdkView.addStateChangedListener()
sandboxedSdkViewSet.add(childSandboxedSdkView)
}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
}
private fun loadResizableBannerAd() {
- resizableBannerView.addStateChangedListener()
loadBannerAd(
currentAdType,
currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
}
private fun loadBottomBannerAd() {
- bottomBannerView.addStateChangedListener()
bottomBannerView.layoutParams =
inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/mediation_options.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/mediation_options.xml
index 54ce3c3..6b526be 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/values/mediation_options.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/values/mediation_options.xml
@@ -18,7 +18,8 @@
<resources>
<string-array name="mediation_dropdown_menu_array">
<item>Mediated Ad > None (default)</item>
- <item>Runtime-Runtime Mediation</item>
+ <item>Runtime-Runtime Mediation without Overlay</item>
+ <item>Runtime-Runtime Mediation with Overlay</item>
<item>Runtime-App Mediation</item>
</string-array>
</resources>
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle b/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
index 3c96f20..441c562 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/build.gradle
@@ -26,6 +26,7 @@
defaultConfig {
applicationId "androidx.privacysandbox.ui.integration.testsdkprovider"
minSdk 33
+ compileSdk 35
}
buildTypes {
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index 53534f9..c5370e1 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -40,17 +40,19 @@
waitInsideOnDraw: Boolean,
drawViewability: Boolean
): Bundle {
- val isMediation =
- (mediationOption == MediationOption.SDK_RUNTIME_MEDIATEE ||
- mediationOption == MediationOption.IN_APP_MEDIATEE)
+ val isMediation = mediationOption != MediationOption.NON_MEDIATED
val isAppOwnedMediation = (mediationOption == MediationOption.IN_APP_MEDIATEE)
if (isMediation) {
- return maybeGetMediateeBannerAdBundle(
- isAppOwnedMediation,
- adType,
- waitInsideOnDraw,
- drawViewability
- )
+ val mediateeBundle =
+ maybeGetMediateeBannerAdBundle(
+ isAppOwnedMediation,
+ adType,
+ waitInsideOnDraw,
+ drawViewability
+ )
+ return if (mediationOption == MediationOption.SDK_RUNTIME_MEDIATEE_WITH_OVERLAY) {
+ testAdapters.OverlaidAd(mediateeBundle).toCoreLibInfo(sdkContext)
+ } else mediateeBundle
}
val adapter: SandboxedUiAdapter =
when (adType) {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index a131ca8..98e46be 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -133,6 +133,9 @@
internal val stateListenerManager: StateListenerManager = StateListenerManager()
private var viewContainingPoolingContainerListener: View? = null
private var poolingContainerListener = PoolingContainerListener {}
+ private val frameCommitCallback = Runnable {
+ stateListenerManager.currentUiSessionState = Active
+ }
internal var signalMeasurer: SandboxedSdkViewSignalMeasurer? = null
/** Adds a state change listener to the UI session and immediately reports the current state. */
@@ -249,6 +252,7 @@
private fun removeCallbacksOnWindowDetachment() {
viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
+ CompatImpl.unregisterFrameCommitCallback(viewTreeObserver, frameCommitCallback)
}
private fun removeCallbacks() {
@@ -272,10 +276,7 @@
}
// Wait for the next frame commit before sending an ACTIVE state change to listeners.
- // TODO(b/338196636): Unregister this when necessary.
- CompatImpl.registerFrameCommitCallback(viewTreeObserver) {
- stateListenerManager.currentUiSessionState = Active
- }
+ CompatImpl.registerFrameCommitCallback(viewTreeObserver, frameCommitCallback)
if (contentView is SurfaceView) {
contentView.holder.addCallback(surfaceChangedCallback)
@@ -637,6 +638,12 @@
}
}
+ fun unregisterFrameCommitCallback(observer: ViewTreeObserver, callback: Runnable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Api29PlusImpl.unregisterFrameCommitCallback(observer, callback)
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private object Api34PlusImpl {
@@ -707,6 +714,11 @@
fun registerFrameCommitCallback(observer: ViewTreeObserver, callback: Runnable) {
observer.registerFrameCommitCallback(callback)
}
+
+ @JvmStatic
+ fun unregisterFrameCommitCallback(observer: ViewTreeObserver, callback: Runnable) {
+ observer.unregisterFrameCommitCallback(callback)
+ }
}
}
}
diff --git a/profileinstaller/profileinstaller-benchmark/build.gradle b/profileinstaller/profileinstaller-benchmark/build.gradle
index 8646ff0..d813b34 100644
--- a/profileinstaller/profileinstaller-benchmark/build.gradle
+++ b/profileinstaller/profileinstaller-benchmark/build.gradle
@@ -32,14 +32,14 @@
dependencies {
androidTestImplementation(project(":profileinstaller:profileinstaller"))
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-runtime')
androidTestImplementation(libs.kotlinStdlib)
}
diff --git a/recyclerview/recyclerview-selection/build.gradle b/recyclerview/recyclerview-selection/build.gradle
index b599c61..0768c45 100644
--- a/recyclerview/recyclerview-selection/build.gradle
+++ b/recyclerview/recyclerview-selection/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.junit)
}
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index 92b9d6f..75f30a7 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -19,15 +19,15 @@
implementation("androidx.collection:collection:1.4.2")
api("androidx.customview:customview:1.0.0")
implementation("androidx.customview:customview-poolingcontainer:1.0.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinStdlib)
diff --git a/remotecallback/remotecallback/build.gradle b/remotecallback/remotecallback/build.gradle
index c1da69a..5cd252f 100644
--- a/remotecallback/remotecallback/build.gradle
+++ b/remotecallback/remotecallback/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestAnnotationProcessor(project(":remotecallback:remotecallback-processor"))
}
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index c679882..146a376 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -37,7 +37,7 @@
kspAndroidTest project(":room:room-compiler")
androidTestImplementation(project(":room:room-rxjava2"))
androidTestImplementation("androidx.arch.core:core-runtime:2.2.0")
- androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(libs.rxjava2)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
diff --git a/room/integration-tests/autovaluetestapp/build.gradle b/room/integration-tests/autovaluetestapp/build.gradle
index 86c2124..3336f4f 100644
--- a/room/integration-tests/autovaluetestapp/build.gradle
+++ b/room/integration-tests/autovaluetestapp/build.gradle
@@ -35,8 +35,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index 9b8764b..3be749d 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -230,6 +230,9 @@
fun getBookFlowable(bookId: String): Flowable<Book>
@Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookObservable(bookId: String): Observable<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
fun getBookJavaOptional(bookId: String): java.util.Optional<Book>
@Query("SELECT * FROM book WHERE bookId = :bookId")
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
index 1fa133b..e1b25e63 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BoxedNonNullTypesTest.kt
@@ -27,7 +27,6 @@
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
-import androidx.room.integration.kotlintestapp.RoomTestConfig
import androidx.room.integration.kotlintestapp.assumeKsp
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -138,14 +137,6 @@
@Test // repro for: b/211822920
fun getAsRx2ObservableUnknownNullabilityInCursor() {
- if (RoomTestConfig.isKsp) {
- // only in KSP we know the value is non-null, hence default to 0.
- // in RX, it would generate code that would return null and get filtered by RxRoom
- // Even though this becomes inconsistent between KSP and KAPT, the KSP path is more
- // consistent with the non-observable version of the query.
- assertThat(db.myDao().getAsRx2ObservableUnknownTypeInCursor().blockingFirst())
- .isEqualTo(0L)
- }
db.myDao().insert(MyEntity(9))
assertThat(db.myDao().getAsRx2ObservableUnknownTypeInCursor().blockingFirst()).isEqualTo(9L)
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
index 00a8fc7..4aae5e3 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
@@ -27,7 +27,7 @@
class RxJava2QueryTest : TestDatabaseTest() {
@Test
- fun observeBooksById() {
+ fun observeBooksByIdFlowable() {
booksDao.addAuthors(TestUtil.AUTHOR_1)
booksDao.addPublishers(TestUtil.PUBLISHER)
booksDao.addBooks(TestUtil.BOOK_1)
@@ -39,6 +39,38 @@
}
@Test
+ fun observeBooksByIdFlowable_noBook() {
+ booksDao
+ .getBookFlowable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertNoErrors()
+ .assertNoValues()
+ }
+
+ @Test
+ fun observeBooksByIdObservable() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+ booksDao
+ .getBookObservable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertValue { book -> book == TestUtil.BOOK_1 }
+ }
+
+ @Test
+ fun observeBooksById_noBook() {
+ booksDao
+ .getBookObservable(TestUtil.BOOK_1.bookId)
+ .test()
+ .also { drain() }
+ .assertNoErrors()
+ .assertNoValues()
+ }
+
+ @Test
fun observeBooksByIdSingle() {
booksDao.addAuthors(TestUtil.AUTHOR_1)
booksDao.addPublishers(TestUtil.PUBLISHER)
@@ -76,10 +108,9 @@
booksDao.addPublishers(TestUtil.PUBLISHER)
booksDao.addBooks(TestUtil.BOOK_1)
- var expected =
+ val expected =
BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title, TestUtil.PUBLISHER)
- var expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
+ val expectedList = listOf(expected)
booksDao.getBooksWithPublisherFlowable().test().also { drain() }.assertValue(expectedList)
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/PreKmpDatabase_TheDao_Impl.kt b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/PreKmpDatabase_TheDao_Impl.kt
index f0f3883..7dbe317 100644
--- a/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/PreKmpDatabase_TheDao_Impl.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenKotlin/java/androidx/room/integration/kotlintestapp/test/PreKmpDatabase_TheDao_Impl.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION", "ktlint") // Due to old entity adapter usage.
+
package androidx.room.integration.kotlintestapp.test
import android.database.Cursor
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/UUIDTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/UUIDTest.kt
new file mode 100644
index 0000000..0dcc0f8
--- /dev/null
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/UUIDTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.multiplatformtestapp.test
+
+import androidx.kruth.assertThat
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.integration.multiplatformtestapp.test.UUIDTest.SampleJvmDatabase
+import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import java.util.UUID
+import kotlin.test.Test
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.runTest
+
+class UUIDTest {
+ @Test
+ fun testUUIDQuery() = runTest {
+ val db =
+ Room.inMemoryDatabaseBuilder<SampleJvmDatabase>()
+ .setDriver(BundledSQLiteDriver())
+ .setQueryCoroutineContext(Dispatchers.IO)
+ .build()
+ val dao = db.dao()
+ val text = "88c6af75-8d2a-489c-85c9-92e5dd8a108c"
+ val uuid = UUID.fromString(text)
+
+ dao.insertWithQuery(uuid)
+ assertThat(dao.getEntity(uuid)).isEqualTo(UUIDEntity(uuid))
+ }
+
+ @Database(entities = [UUIDEntity::class], version = 1, exportSchema = false)
+ abstract class SampleJvmDatabase : RoomDatabase() {
+ abstract fun dao(): ByteDao
+ }
+
+ @Dao
+ interface ByteDao {
+ @Insert suspend fun insert(byteEntity: UUIDEntity)
+
+ @Query("INSERT INTO UUIDEntity (id_UUID) VALUES (:uuid)")
+ suspend fun insertWithQuery(uuid: UUID): Long
+
+ @Query("SELECT * FROM UUIDEntity WHERE id_UUID = :uuid")
+ suspend fun getEntity(uuid: UUID): UUIDEntity
+ }
+
+ @Entity
+ data class UUIDEntity(
+ @PrimaryKey val id_UUID: UUID,
+ )
+}
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 86b9003..6834e9b 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -102,7 +102,7 @@
androidTestImplementation(project(":room:room-guava"))
androidTestImplementation(project(":room:room-paging"))
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
- androidTestImplementation(projectOrArtifact(":paging:paging-runtime"))
+ androidTestImplementation(project(":paging:paging-runtime"))
androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
androidTestImplementation("androidx.lifecycle:lifecycle-livedata:2.6.1")
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
index 2df7d0e..1713fb2 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultiInstanceInvalidationTest.java
@@ -45,6 +45,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
@@ -200,6 +201,7 @@
}
@Test
+ @Ignore // Flaky test, b/363246309.
public void invalidationInAnotherInstance_closed() throws Exception {
final SampleDatabase db1 = openDatabase(true);
final SampleDatabase db2 = openDatabase(true);
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 5cb971c..1a1f6d9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -53,16 +53,19 @@
name: String,
annotations: List<XAnnotationSpec>
) = apply {
+ val paramSpec = ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
actual.addParameter(
- ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
- .apply {
- if (typeName.nullability == XNullability.NULLABLE) {
- addAnnotation(NULLABLE_ANNOTATION)
- } else if (typeName.nullability == XNullability.NONNULL) {
- addAnnotation(NONNULL_ANNOTATION)
- }
- }
- .build()
+ // Adding nullability annotation to primitive parameters is redundant as
+ // primitives can never be null.
+ if (typeName.isPrimitive) {
+ paramSpec.build()
+ } else {
+ when (typeName.nullability) {
+ XNullability.NULLABLE -> paramSpec.addAnnotation(NULLABLE_ANNOTATION)
+ XNullability.NONNULL -> paramSpec.addAnnotation(NONNULL_ANNOTATION)
+ else -> paramSpec
+ }.build()
+ }
)
// TODO(b/247247439): Add other annotations
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 325d436..4fe0f34 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -25,6 +25,7 @@
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
+import javax.lang.model.type.ExecutableType
import javax.lang.model.util.Types
import kotlin.coroutines.Continuation
@@ -42,14 +43,24 @@
}
internal val Element.nullability: XNullability
- get() =
- if (asType().kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
+ get() {
+ // Get the type of the element: if this is a method, use the return type instead of the full
+ // method type since the return is what determines nullability.
+ val asType =
+ asType().let {
+ when (it) {
+ is ExecutableType -> it.returnType
+ else -> it
+ }
+ }
+ return if (asType.kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
XNullability.NONNULL
} else if (hasAnyOf(NULLABLE_ANNOTATIONS)) {
XNullability.NULLABLE
} else {
XNullability.UNKNOWN
}
+ }
internal fun Element.requireEnclosingType(env: JavacProcessingEnv): JavacTypeElement {
return checkNotNull(enclosingType(env)) { "Cannot find required enclosing type for $this" }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 54f1708..e8c823f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -210,3 +210,18 @@
val parent = parent ?: return false
return parent.hasSuppressWildcardsAnnotationInHierarchy()
}
+
+/**
+ * Returns the inner arguments for this type.
+ *
+ * Specifically it excludes outer type args when this type is an inner type.
+ *
+ * Needed due to https://github.com/google/ksp/issues/2065
+ */
+val KSType.innerArguments: List<KSTypeArgument>
+ get() =
+ if (arguments.isNotEmpty()) {
+ arguments.subList(0, declaration.typeParameters.size)
+ } else {
+ emptyList()
+ }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
index 07910e7..fac410c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeJavaPoetExt.kt
@@ -21,6 +21,7 @@
import androidx.room.compiler.processing.tryBox
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.outerType
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSName
@@ -158,28 +159,52 @@
resolver: Resolver,
typeResolutionContext: TypeResolutionContext,
): JTypeName {
- return if (declaration is KSTypeAlias) {
- replaceTypeAliases(resolver).asJTypeName(resolver, typeResolutionContext)
- } else if (
- this.arguments.isNotEmpty() &&
+ if (declaration is KSTypeAlias) {
+ return replaceTypeAliases(resolver).asJTypeName(resolver, typeResolutionContext)
+ }
+ val typeName = declaration.asJTypeName(resolver, typeResolutionContext)
+ val isJavaPrimitiveOrVoid = typeName.tryBox() !== typeName
+ if (
+ !isTypeParameter() &&
!resolver.isJavaRawType(this) &&
// Excluding generic value classes used directly otherwise we may generate something
// like `Object<String>`.
- !(declaration.isValueClass() && declaration.isUsedDirectly(typeResolutionContext))
+ !(declaration.isValueClass() && declaration.isUsedDirectly(typeResolutionContext)) &&
+ !isJavaPrimitiveOrVoid
) {
val args: Array<JTypeName> =
- this.arguments
+ this.innerArguments
.map { typeArg -> typeArg.asJTypeName(resolver, typeResolutionContext) }
.map { it.tryBox() }
.toTypedArray()
- when (val typeName = declaration.asJTypeName(resolver, typeResolutionContext).tryBox()) {
- is JArrayTypeName -> JArrayTypeName.of(args.single())
- is JClassName -> JParameterizedTypeName.get(typeName, *args)
+ when (typeName) {
+ is JArrayTypeName -> {
+ return if (args.isEmpty()) {
+ // e.g. IntArray
+ typeName
+ } else {
+ JArrayTypeName.of(args.single())
+ }
+ }
+ is JClassName -> {
+ val outerType = this.outerType
+ if (outerType != null) {
+ val outerTypeName = outerType.asJTypeName(resolver, typeResolutionContext)
+ if (outerTypeName is JParameterizedTypeName) {
+ return outerTypeName.nestedClass(typeName.simpleName(), args.toList())
+ }
+ }
+ return if (args.isEmpty()) {
+ typeName
+ } else {
+ JParameterizedTypeName.get(typeName, *args)
+ }
+ }
else -> error("Unexpected type name for KSType: $typeName")
}
} else {
- this.declaration.asJTypeName(resolver, typeResolutionContext)
+ return typeName
}
}
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 c564ae1..8e16852 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
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.outerType
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSName
@@ -32,6 +33,7 @@
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.javapoet.KClassName
+import com.squareup.kotlinpoet.javapoet.KParameterizedTypeName
import com.squareup.kotlinpoet.javapoet.KTypeName
import com.squareup.kotlinpoet.javapoet.KTypeVariableName
import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
@@ -128,24 +130,38 @@
resolver: Resolver,
typeArgumentTypeLookup: KTypeArgumentTypeLookup
): KTypeName {
- return if (declaration is KSTypeAlias) {
- replaceTypeAliases(resolver).asKTypeName(resolver, typeArgumentTypeLookup)
- } else
- if (this.arguments.isNotEmpty() && !resolver.isJavaRawType(this)) {
- val args: List<KTypeName> =
- this.arguments.map { typeArg ->
- typeArg.asKTypeName(
- resolver = resolver,
- typeArgumentTypeLookup = typeArgumentTypeLookup
- )
- }
- val typeName = declaration.asKTypeName(resolver, typeArgumentTypeLookup)
- check(typeName is KClassName) { "Unexpected type name for KSType: $typeName" }
- typeName.parameterizedBy(args)
- } else {
- this.declaration.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (declaration is KSTypeAlias) {
+ return replaceTypeAliases(resolver).asKTypeName(resolver, typeArgumentTypeLookup)
+ }
+ fun resolveTypeName(): KTypeName {
+ val typeName = declaration.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (!isTypeParameter() && !resolver.isJavaRawType(this)) {
+ check(typeName is KClassName) { "Unexpected type name for KSType: $typeName" }
+ val args: List<KTypeName> =
+ this.innerArguments.map { typeArg ->
+ typeArg.asKTypeName(
+ resolver = resolver,
+ typeArgumentTypeLookup = typeArgumentTypeLookup
+ )
+ }
+ val outerType = this.outerType
+ if (outerType != null) {
+ val outerTypeName = outerType.asKTypeName(resolver, typeArgumentTypeLookup)
+ if (outerTypeName is KParameterizedTypeName) {
+ return outerTypeName.nestedClass(typeName.simpleName, args)
+ }
}
- .copy(nullable = isMarkedNullable || nullability == Nullability.PLATFORM)
+ return if (args.isEmpty()) {
+ typeName
+ } else {
+ typeName.parameterizedBy(args)
+ }
+ } else {
+ return typeName
+ }
+ }
+ return resolveTypeName()
+ .copy(nullable = isMarkedNullable || nullability == Nullability.PLATFORM)
}
/** See [KTypeVariableNameFactory.newInstance] */
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index fd53c9fe..4fec882 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -408,9 +408,9 @@
}
val arguments =
newTypeArguments
- ?: newType.arguments.indices.map { i ->
+ ?: newType.innerArguments.indices.map { i ->
KSTypeArgumentWrapper(
- originalTypeArg = newType.arguments[i],
+ originalTypeArg = newType.innerArguments[i],
typeParam = newType.declaration.typeParameters[i],
resolver = resolver,
)
@@ -451,7 +451,18 @@
fun isTypeParameter() = originalType.isTypeParameter()
- fun unwrap() = newType.replace(arguments.map { it.unwrap() })
+ fun unwrap(): KSType {
+ val newArgs = arguments.map { it.unwrap() }
+ return newType.replace(
+ newType.arguments.mapIndexed { index, oldArg ->
+ if (index < newArgs.size) {
+ newArgs[index]
+ } else {
+ oldArg
+ }
+ }
+ )
+ }
override fun toString() = buildString {
if (originalType.annotations.toList().isNotEmpty()) {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 85a8062..cfbaa82 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -104,9 +104,14 @@
null
} else {
declaration.superTypes
- .singleOrNull {
- val declaration = it.resolve().declaration.replaceTypeAliases()
- declaration is KSClassDeclaration && declaration.classKind == ClassKind.CLASS
+ .firstOrNull {
+ val type = it.resolve()
+ val declaration = type.declaration.replaceTypeAliases()
+ declaration is KSClassDeclaration &&
+ (declaration.classKind == ClassKind.CLASS &&
+ // Filter out error class declarations, for consistency with KAPT these
+ // are exposed as super interfaces.
+ (isFromJava() || !type.isError))
}
?.let { env.wrap(it).makeNonNullable() } ?: anyTypeElement.type
}
@@ -115,8 +120,13 @@
override val superInterfaces by lazy {
declaration.superTypes
.filter {
- val declaration = it.resolve().declaration.replaceTypeAliases()
- declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE
+ val type = it.resolve()
+ val declaration = type.declaration.replaceTypeAliases()
+ declaration is KSClassDeclaration &&
+ (declaration.classKind == ClassKind.INTERFACE ||
+ // Workaround https://github.com/google/ksp/issues/1443 by exposing
+ // error class declarations as super interfaces.
+ (isFromKotlin() && type.isError))
}
.mapTo(mutableListOf()) { env.wrap(it).makeNonNullable() }
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
index 2fa1cee..20bd0db 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
@@ -430,7 +430,7 @@
"""
.trimIndent()
)
- // https://github.com/google/ksp/issues/1963
+ // https://github.com/google/ksp/issues/2077
runTest(sources = listOf(kotlinSrc, javaSrc), kotlincArgs = KOTLINC_LANGUAGE_1_9_ARGS) {
invocation ->
listOf("KotlinClass", "JavaClass")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
index a07baba..3b3b462 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -168,7 +168,7 @@
"""
.trimIndent()
)
- // https://github.com/google/ksp/issues/1890
+ // https://github.com/google/ksp/issues/2078
runTest(sources = listOf(javaSrc, kotlinSrc), kotlincArgs = KOTLINC_LANGUAGE_1_9_ARGS) {
invocation ->
val typeElement = invocation.processingEnv.requireTypeElement("Foo")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 8c7fd8f..17ba896 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -57,6 +57,9 @@
public String returnsNonNull() {
return "";
}
+ public int returnsPrimitiveInt() {
+ return 0;
+ }
public String parameters(
int primitiveParam,
@@ -76,6 +79,10 @@
element.getField("primitiveInt").let { field ->
assertThat(field.type.nullability).isEqualTo(NONNULL)
}
+ element.getMethodByJvmName("returnsPrimitiveInt").let { method ->
+ assertThat(method.returnType.nullability).isEqualTo(NONNULL)
+ assertThat(method.executableType.returnType.nullability).isEqualTo(NONNULL)
+ }
element.getField("boxedInt").let { field ->
assertThat(field.type.nullability).isEqualTo(UNKNOWN)
}
@@ -161,6 +168,8 @@
nonNullGenericWithNullableType: List<Int?>
) {
}
+
+ val nullableLambda: ((String) -> Int)? = null
}
"""
.trimIndent()
@@ -246,6 +255,9 @@
Triple("nonNullGenericWithNullableType", NONNULL, NULLABLE)
)
}
+ element.getField("nullableLambda").let { field ->
+ assertThat(field.type.nullability).isEqualTo(NULLABLE)
+ }
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 7c7d413..26f8787 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -23,7 +23,6 @@
import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.javac.JavacType
import androidx.room.compiler.processing.ksp.KspProcessingEnv
-import androidx.room.compiler.processing.util.KOTLINC_LANGUAGE_1_9_ARGS
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.asKClassName
@@ -34,6 +33,7 @@
import androidx.room.compiler.processing.util.getDeclaredMethodByJvmName
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethodByJvmName
+import androidx.room.compiler.processing.util.kspProcessingEnv
import androidx.room.compiler.processing.util.runJavaProcessorTest
import androidx.room.compiler.processing.util.runKspTest
import androidx.room.compiler.processing.util.runProcessorTest
@@ -1357,9 +1357,7 @@
"""
.trimIndent()
)
- ),
- // https://github.com/google/ksp/issues/1890
- kotlincArgs = KOTLINC_LANGUAGE_1_9_ARGS
+ )
) { invocation ->
val appSubject = invocation.processingEnv.requireTypeElement("test.Subject")
val methodNames = appSubject.getAllMethods().map { it.name }.toList()
@@ -1367,7 +1365,11 @@
val objectMethodNames = invocation.objectMethodNames()
if (invocation.isKsp) {
assertThat(methodNames - objectMethodNames).containsExactly("f1", "f2")
- assertThat(methodJvmNames - objectMethodNames).containsExactly("notF1", "notF2")
+ if (invocation.kspProcessingEnv.isKsp2 && isPreCompiled) {
+ assertThat(methodJvmNames - objectMethodNames).containsExactly("f1", "f2")
+ } else {
+ assertThat(methodJvmNames - objectMethodNames).containsExactly("notF1", "notF2")
+ }
} else {
assertThat(methodNames - objectMethodNames).containsExactly("f1", "f1", "f2")
assertThat(methodJvmNames - objectMethodNames)
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 66738e0..f9a7379 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -543,24 +543,115 @@
}
@Test
- fun errorTypeForSuper() {
- val missingTypeRef =
+ fun errorTypeForSuperJava() {
+ val missingSuperClassType =
Source.java(
"foo.bar.Baz",
"""
package foo.bar;
public class Baz extends IDontExist {
- NotExistingType foo() {
- throw new RuntimeException("Stub");
- }
}
"""
.trimIndent()
)
- runProcessorTest(sources = listOf(missingTypeRef)) {
+ runProcessorTest(sources = listOf(missingSuperClassType)) {
+ it.assertCompilationResult { compilationDidFail() }
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
assertThat(element.superClass?.isError()).isTrue()
+ assertThat(element.superInterfaces).isEmpty()
+ }
+
+ val missingSuperInterfaceType =
+ Source.java(
+ "foo.bar.Baz",
+ """
+ package foo.bar;
+ public class Baz implements IDontExist {
+ }
+ """
+ .trimIndent()
+ )
+ runProcessorTest(sources = listOf(missingSuperInterfaceType)) {
it.assertCompilationResult { compilationDidFail() }
+ val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
+ if (it.isKsp) { // Due to https://github.com/google/ksp/issues/1443
+ assertThat(element.superClass?.isError()).isTrue()
+ assertThat(element.superInterfaces).isEmpty()
+ } else {
+ assertThat(element.superClass?.isError()).isFalse()
+ assertThat(element.superInterfaces).isNotEmpty()
+ assertThat(element.superInterfaces.single().isError()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun errorTypeForSuperKotlin() {
+ val src =
+ Source.kotlin(
+ "Subject.kt",
+ """
+ package test
+
+ interface SubjectInterface : MissingType
+ class SubjectClassOne : MissingType
+ class SubjectClassTwo : MissingType()
+ class SubjectClassThree : ValidSuperClass(), MissingType
+ class SubjectClassFour : ValidSuperInterface, MissingType
+
+ abstract class ValidSuperClass
+ interface ValidSuperInterface
+ """
+ .trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(src),
+ kotlincArguments =
+ listOf("-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true")
+ ) { invocation ->
+ invocation.assertCompilationResult { compilationDidFail() }
+ invocation.processingEnv.requireTypeElement("test.SubjectInterface").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNull() // interfaces has no super class
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassOne").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassTwo").let {
+ if (invocation.isKsp) {
+ // In KSP regardless of the parenthesis in the super type, they are always
+ // classified as class declarations.
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ } else {
+ // In KAPT depending if the super type has a parenthesis or not, indicating
+ // it is a super class not a super interface, then the stub will correctly
+ // put the reference in 'extends' vs 'implements'.
+ assertThat(it.superInterfaces).isEmpty()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.isError()).isTrue()
+ }
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassThree").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces.first().isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName())
+ .isEqualTo(XClassName.get("test", "ValidSuperClass"))
+ }
+ invocation.processingEnv.requireTypeElement("test.SubjectClassFour").let {
+ assertThat(it.superInterfaces).isNotEmpty()
+ assertThat(it.superInterfaces[0].isError()).isFalse()
+ assertThat(it.superInterfaces[1].isError()).isTrue()
+ assertThat(it.superClass).isNotNull()
+ assertThat(it.superClass!!.asTypeName()).isEqualTo(XTypeName.ANY_OBJECT)
+ }
}
}
@@ -2455,4 +2546,299 @@
}
}
}
+
+ @Test
+ fun innerTypeNames(@TestParameter isPrecompiled: Boolean) {
+ val kotlinSrc =
+ Source.kotlin(
+ "KotlinOuter.kt",
+ """
+ class KotlinOuter<T> {
+ inner class Inner<P> {
+ inner class InnerAgain<Q>
+ }
+ inner class InnerWithoutArgs
+ }
+ class KotlinOuterWithoutArgs {
+ inner class Inner<P> {
+ inner class InnerAgain<Q>
+ }
+ inner class InnerWithoutArgs
+ }
+ class KotlinClient {
+ fun outer(): KotlinOuter<String> = TODO()
+ fun inner(): KotlinOuter<String>.Inner<Number> = TODO()
+ fun innerAgain(): KotlinOuter<String>.Inner<Number>.InnerAgain<Boolean> = TODO()
+ fun innerWithoutArgs(): KotlinOuter<String>.InnerWithoutArgs = TODO()
+ fun outerWithoutArgs(): KotlinOuterWithoutArgs = TODO()
+ fun innerInOuterWithoutArgs(): KotlinOuterWithoutArgs.Inner<String> = TODO()
+ fun innerAgainInOuterWithoutArgs(): KotlinOuterWithoutArgs.Inner<String>.InnerAgain<Number> = TODO()
+ fun innerWithoutArgsInOuterWithoutArgs(): KotlinOuterWithoutArgs.InnerWithoutArgs = TODO()
+ }
+ """
+ .trimIndent()
+ )
+ val javaSrc =
+ Source.java(
+ "JavaOuter",
+ """
+ class JavaOuter<T> {
+ class Nested<P> {
+ class NestedAgain<Q> {}
+ class NestedAgainWithoutArgs {}
+ }
+ class NestedWithoutArgs {}
+ }
+ class JavaOuterWithoutArgs {
+ class Nested<P> {
+ class NestedAgain<Q> {}
+ }
+ class NestedWithoutArgs {}
+ }
+ class JavaClient {
+ JavaOuter<String> javaOuter() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number> nested() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number>.NestedAgain<Boolean> nestedAgain() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.Nested<Number>.NestedAgainWithoutArgs nestedAgainWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuter<String>.NestedWithoutArgs nestedWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuter javaOuterRaw() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs javaOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.Nested<String> nestedInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.Nested<String>.NestedAgain<Boolean> nestedAgainInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ JavaOuterWithoutArgs.NestedWithoutArgs nestedWithoutArgsInOuterWithoutArgs() { throw new RuntimeException("Stub"); }
+ }
+ """
+ .trimIndent()
+ )
+ runProcessorTest(
+ sources =
+ if (isPrecompiled) {
+ emptyList()
+ } else {
+ listOf(kotlinSrc, javaSrc)
+ },
+ classpath =
+ if (isPrecompiled) {
+ compileFiles(listOf(kotlinSrc, javaSrc))
+ } else {
+ emptyList()
+ },
+ ) { invocation ->
+ invocation.processingEnv.requireTypeElement("KotlinClient").let { cls ->
+ cls.getDeclaredMethodByJvmName("outer").returnType.asTypeName().let { typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("KotlinOuter<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("inner").returnType.asTypeName().let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuter<java.lang.String>.Inner<java.lang.Number>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>.Inner<kotlin.Number>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerAgain").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "KotlinOuter<java.lang.String>.Inner<java.lang.Number>.InnerAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "KotlinOuter<kotlin.String>.Inner<kotlin.Number>.InnerAgain<kotlin.Boolean>"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerWithoutArgs").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuter<java.lang.String>.InnerWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuter<kotlin.String>.InnerWithoutArgs")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("outerWithoutArgs").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("KotlinOuterWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString()).isEqualTo("KotlinOuterWithoutArgs")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.Inner<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.Inner<kotlin.String>")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerAgainInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "KotlinOuterWithoutArgs.Inner<java.lang.String>.InnerAgain<java.lang.Number>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "KotlinOuterWithoutArgs.Inner<kotlin.String>.InnerAgain<kotlin.Number>"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("innerWithoutArgsInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.InnerWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("KotlinOuterWithoutArgs.InnerWithoutArgs")
+ }
+ }
+ }
+
+ invocation.processingEnv.requireTypeElement("JavaClient").let { cls ->
+ cls.getDeclaredMethodByJvmName("javaOuter").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuter<java.lang.String>")
+ if (invocation.isKsp)
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>?")
+ }
+
+ if (!isPrecompiled && invocation.isKsp) {
+ // https://github.com/google/ksp/issues/2066
+ } else {
+ cls.getDeclaredMethodByJvmName("nested").returnType.asTypeName().let { typeName
+ ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuter<java.lang.String>.Nested<java.lang.Number>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>.Nested<kotlin.Number?>?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedAgain").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuter<java.lang.String>.Nested<java.lang.Number>.NestedAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuter<kotlin.String?>.Nested<kotlin.Number?>.NestedAgain<kotlin.Boolean?>?"
+ )
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuter<java.lang.String>.NestedWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuter<kotlin.String?>.NestedWithoutArgs?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("javaOuterRaw").returnType.asTypeName().let {
+ typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuter")
+ if (invocation.isKsp) {
+ // TODO: This is wrong as Kotlin doesn't allow raw types, but it's
+ // probably not possible for us to know what type arg to fill when the\
+ // type parameter has multiple bounds.
+ assertThat(typeName.kotlin.toString()).isEqualTo("JavaOuter?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("javaOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString()).isEqualTo("JavaOuterWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs?")
+ }
+ }
+
+ cls.getDeclaredMethodByJvmName("nestedInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuterWithoutArgs.Nested<java.lang.String>")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs.Nested<kotlin.String?>?")
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedAgainInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuterWithoutArgs.Nested<java.lang.String>.NestedAgain<java.lang.Boolean>"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuterWithoutArgs.Nested<kotlin.String?>.NestedAgain<kotlin.Boolean?>?"
+ )
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedWithoutArgsInOuterWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo("JavaOuterWithoutArgs.NestedWithoutArgs")
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo("JavaOuterWithoutArgs.NestedWithoutArgs?")
+ }
+ }
+ cls.getDeclaredMethodByJvmName("nestedAgainWithoutArgs")
+ .returnType
+ .asTypeName()
+ .let { typeName ->
+ assertThat(typeName.java.toString())
+ .isEqualTo(
+ "JavaOuter<java.lang.String>.Nested<java.lang.Number>.NestedAgainWithoutArgs"
+ )
+ if (invocation.isKsp) {
+ assertThat(typeName.kotlin.toString())
+ .isEqualTo(
+ "JavaOuter<kotlin.String?>.Nested<kotlin.Number?>.NestedAgainWithoutArgs?"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 060f27d..8494f84 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -82,7 +82,7 @@
testImplementation(libs.autoValue) // to access the processor in tests
testImplementation(libs.autoServiceAnnotations)
testImplementation(libs.autoService) // to access the processor in tests
- testImplementation(projectOrArtifact(":paging:paging-common"))
+ testImplementation(project(":paging:paging-common"))
testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.junit)
testImplementation(libs.jsr250)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 3cc5e276..b6d986f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -394,7 +394,6 @@
return when {
builtInConverterFlags.enums.isEnabled() && typeElement?.isEnum() == true ->
EnumColumnTypeAdapter(typeElement, type)
- !context.isAndroidOnlyTarget() -> null // UUID and ByteBuffer are Android-only
builtInConverterFlags.uuid.isEnabled() && type.isUUID() -> UuidColumnTypeAdapter(type)
builtInConverterFlags.byteBuffer.isEnabled() && type.isByteBuffer() ->
ByteBufferColumnTypeAdapter(type)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
index aad54a6..ecb23d9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxQueryResultBinderProvider.kt
@@ -33,7 +33,8 @@
context.processingEnv.findType(rxType.className.canonicalName)?.rawType
}
- override fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+ override fun extractTypeArg(declared: XType): XType =
+ declared.typeArguments.first().makeNullable()
override fun create(
typeArg: XType,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 45a0481..226ca2e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -33,12 +33,13 @@
import androidx.room.compiler.processing.XType
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.RoomMemberNames
-import androidx.room.ext.RoomTypeNames
import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER_COMPAT
import androidx.room.ext.RoomTypeNames.INSERT_ADAPTER
import androidx.room.ext.RoomTypeNames.INSERT_ADAPTER_COMPAT
+import androidx.room.ext.RoomTypeNames.RAW_QUERY
import androidx.room.ext.RoomTypeNames.ROOM_DB
+import androidx.room.ext.RoomTypeNames.ROOM_SQL_QUERY
import androidx.room.ext.RoomTypeNames.UPSERT_ADAPTER
import androidx.room.ext.RoomTypeNames.UPSERT_ADAPTER_COMPAT
import androidx.room.ext.SupportDbTypeNames
@@ -333,21 +334,36 @@
}
private fun createRawQueryMethodBody(method: RawQueryMethod): XCodeBlock {
- if (
- method.runtimeQueryParam == null ||
- !method.runtimeQueryParam.isRawQuery() ||
- !method.queryResultBinder.isMigratedToDriver()
- ) {
+ if (method.runtimeQueryParam == null || !method.queryResultBinder.isMigratedToDriver()) {
return compatCreateRawQueryMethodBody(method)
}
val scope = CodeGenScope(this@DaoWriter, useDriverApi = true)
val sqlQueryVar = scope.getTmpVar("_sql")
+ val rawQueryParamName =
+ if (method.runtimeQueryParam.isSupportQuery()) {
+ val rawQueryVar = scope.getTmpVar("_rawQuery")
+ scope.builder.addLocalVariable(
+ name = rawQueryVar,
+ typeName = RAW_QUERY,
+ assignExpr =
+ XCodeBlock.of(
+ scope.language,
+ format = "%T.copyFrom(%L).toRoomRawQuery()",
+ ROOM_SQL_QUERY,
+ method.runtimeQueryParam.paramName
+ )
+ )
+ rawQueryVar
+ } else {
+ method.runtimeQueryParam.paramName
+ }
+
scope.builder.addLocalVal(
sqlQueryVar,
CommonTypeNames.STRING,
"%L.%L",
- method.runtimeQueryParam.paramName,
+ rawQueryParamName,
when (codeLanguage) {
CodeLanguage.JAVA -> "getSql()"
CodeLanguage.KOTLIN -> "sql"
@@ -360,7 +376,7 @@
bindStatement = { stmtVar ->
this.builder.addStatement(
"%L.getBindingFunction().invoke(%L)",
- method.runtimeQueryParam.paramName,
+ rawQueryParamName,
stmtVar
)
},
@@ -387,7 +403,7 @@
shouldReleaseQuery = true
addLocalVariable(
name = roomSQLiteQueryVar,
- typeName = RoomTypeNames.ROOM_SQL_QUERY,
+ typeName = ROOM_SQL_QUERY,
assignExpr =
XCodeBlock.of(
codeLanguage,
@@ -402,7 +418,7 @@
shouldReleaseQuery = false
addLocalVariable(
name = roomSQLiteQueryVar,
- typeName = RoomTypeNames.ROOM_SQL_QUERY,
+ typeName = ROOM_SQL_QUERY,
assignExpr =
XCodeBlock.of(
codeLanguage,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
index 95779d9..a8095c4 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
@@ -268,6 +268,7 @@
}
"""
)
+ // https://github.com/google/ksp/issues/2051
runProcessorTestWithK1(sources = listOf(baseClass, extension, COMMON.USER, fakeDb)) {
invocation ->
val daoElm = invocation.processingEnv.requireTypeElement("foo.bar.MyDao")
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/autovalue/AutoValuePojoProcessorDelegateTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/autovalue/AutoValuePojoProcessorDelegateTest.kt
index c36fc4b..7e30981 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/autovalue/AutoValuePojoProcessorDelegateTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/autovalue/AutoValuePojoProcessorDelegateTest.kt
@@ -112,6 +112,7 @@
// between javac (argN) and kotlinc (pN).
javacArguments = listOf("-parameters")
)
+ // https://github.com/google/ksp/issues/2033
runProcessorTestWithK1(
sources = emptyList(),
classpath = libraryClasspath,
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
index 3f13507..dafe0ac 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -145,7 +145,9 @@
COMMON.RX3_FLOWABLE,
COMMON.RX2_OBSERVABLE,
COMMON.RX3_OBSERVABLE,
- COMMON.PUBLISHER
+ COMMON.PUBLISHER,
+ COMMON.PAGING_SOURCE,
+ COMMON.LIMIT_OFFSET_PAGING_SOURCE
)
)
runProcessorTestWithK1(sources = sources, classpath = libs) { invocation ->
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
index 2632fe5..8c22368 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
@@ -156,7 +156,9 @@
COMMON.LIVE_DATA,
COMMON.COMPUTABLE_LIVE_DATA,
COMMON.GUAVA_ROOM,
- COMMON.LISTENABLE_FUTURE
+ COMMON.LISTENABLE_FUTURE,
+ COMMON.PAGING_SOURCE,
+ COMMON.LIMIT_OFFSET_PAGING_SOURCE
)
)
runProcessorTestWithK1(
diff --git a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
index ef3d537..6502b66 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
@@ -16,6 +16,7 @@
package foo.bar;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.*;
import androidx.sqlite.db.SupportSQLiteQuery;
import com.google.common.util.concurrent.ListenableFuture;
@@ -85,4 +86,7 @@
@RawQuery(observedEntities = User.class)
abstract public User getUserViaRawQuery(SupportSQLiteQuery rawQuery);
+
+ @Query("SELECT * FROM Child1 ORDER BY id ASC")
+ abstract public PagingSource<Integer, Child1> loadItems();
}
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
index 0fa55d4..32f9284 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
@@ -1,11 +1,13 @@
package foo.bar;
-import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
+import androidx.room.RoomSQLiteQuery;
import androidx.room.guava.GuavaRoom;
-import androidx.room.util.CursorUtil;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
import androidx.room.util.StringUtil;
@@ -627,20 +629,69 @@
}
@Override
- public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
- try {
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
- } else {
- _result = null;
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- }
+ };
+ }
+
+ @Override
+ public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
+ final RoomRawQuery _rawQuery = RoomSQLiteQuery.copyFrom(rawQuery).toRoomRawQuery();
+ final String _sql = _rawQuery.getSql();
+ return DBUtil.performBlocking(__db, true, false, (_connection) -> {
+ final SQLiteStatement _stmt = _connection.prepare(_sql);
+ try {
+ _rawQuery.getBindingFunction().invoke(_stmt);
+ final User _result;
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_fooBarUser(_stmt);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _stmt.close();
+ }
+ });
}
@NonNull
@@ -648,34 +699,34 @@
return Collections.emptyList();
}
- private User __entityCursorConverter_fooBarUser(@NonNull final Cursor cursor) {
+ private User __entityStatementConverter_fooBarUser(@NonNull final SQLiteStatement statement) {
final User _entity;
- final int _cursorIndexOfUid = CursorUtil.getColumnIndex(cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndex(cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndex(cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndex(cursor, "ageColumn");
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndex(statement, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndex(statement, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndex(statement, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndex(statement, "ageColumn");
_entity = new User();
if (_cursorIndexOfUid != -1) {
- _entity.uid = cursor.getInt(_cursorIndexOfUid);
+ _entity.uid = (int) (statement.getLong(_cursorIndexOfUid));
}
if (_cursorIndexOfName != -1) {
- if (cursor.isNull(_cursorIndexOfName)) {
+ if (statement.isNull(_cursorIndexOfName)) {
_entity.name = null;
} else {
- _entity.name = cursor.getString(_cursorIndexOfName);
+ _entity.name = statement.getText(_cursorIndexOfName);
}
}
if (_cursorIndexOfLastName != -1) {
final String _tmpLastName;
- if (cursor.isNull(_cursorIndexOfLastName)) {
+ if (statement.isNull(_cursorIndexOfLastName)) {
_tmpLastName = null;
} else {
- _tmpLastName = cursor.getString(_cursorIndexOfLastName);
+ _tmpLastName = statement.getText(_cursorIndexOfLastName);
}
_entity.setLastName(_tmpLastName);
}
if (_cursorIndexOfAge != -1) {
- _entity.age = cursor.getInt(_cursorIndexOfAge);
+ _entity.age = (int) (statement.getLong(_cursorIndexOfAge));
}
return _entity;
}
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
index 8074e12..6d31a43 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
@@ -1,12 +1,14 @@
package foo.bar;
-import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
+import androidx.room.RoomSQLiteQuery;
import androidx.room.guava.GuavaRoom;
-import androidx.room.util.CursorUtil;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
import androidx.room.util.StringUtil;
@@ -690,20 +692,73 @@
}
@Override
- public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
- try {
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
- } else {
- _result = null;
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- }
+ };
+ }
+
+ @Override
+ public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
+ final RoomRawQuery _rawQuery = RoomSQLiteQuery.copyFrom(rawQuery).toRoomRawQuery();
+ final String _sql = _rawQuery.getSql();
+ return DBUtil.performBlocking(__db, true, false, new Function1<SQLiteConnection, User>() {
+ @Override
+ @NonNull
+ public User invoke(@NonNull final SQLiteConnection _connection) {
+ final SQLiteStatement _stmt = _connection.prepare(_sql);
+ try {
+ _rawQuery.getBindingFunction().invoke(_stmt);
+ final User _result;
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_fooBarUser(_stmt);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _stmt.close();
+ }
+ }
+ });
}
@NonNull
@@ -711,34 +766,34 @@
return Collections.emptyList();
}
- private User __entityCursorConverter_fooBarUser(@NonNull final Cursor cursor) {
+ private User __entityStatementConverter_fooBarUser(@NonNull final SQLiteStatement statement) {
final User _entity;
- final int _cursorIndexOfUid = CursorUtil.getColumnIndex(cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndex(cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndex(cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndex(cursor, "ageColumn");
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndex(statement, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndex(statement, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndex(statement, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndex(statement, "ageColumn");
_entity = new User();
if (_cursorIndexOfUid != -1) {
- _entity.uid = cursor.getInt(_cursorIndexOfUid);
+ _entity.uid = (int) (statement.getLong(_cursorIndexOfUid));
}
if (_cursorIndexOfName != -1) {
- if (cursor.isNull(_cursorIndexOfName)) {
+ if (statement.isNull(_cursorIndexOfName)) {
_entity.name = null;
} else {
- _entity.name = cursor.getString(_cursorIndexOfName);
+ _entity.name = statement.getText(_cursorIndexOfName);
}
}
if (_cursorIndexOfLastName != -1) {
final String _tmpLastName;
- if (cursor.isNull(_cursorIndexOfLastName)) {
+ if (statement.isNull(_cursorIndexOfLastName)) {
_tmpLastName = null;
} else {
- _tmpLastName = cursor.getString(_cursorIndexOfLastName);
+ _tmpLastName = statement.getText(_cursorIndexOfLastName);
}
_entity.setLastName(_tmpLastName);
}
if (_cursorIndexOfAge != -1) {
- _entity.age = cursor.getInt(_cursorIndexOfAge);
+ _entity.age = (int) (statement.getLong(_cursorIndexOfAge));
}
return _entity;
}
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 54daec0..41bd29f 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -1,11 +1,13 @@
package foo.bar;
-import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
+import androidx.room.RoomSQLiteQuery;
import androidx.room.guava.GuavaRoom;
-import androidx.room.util.CursorUtil;
+import androidx.room.paging.LimitOffsetPagingSource;
import androidx.room.util.DBUtil;
import androidx.room.util.SQLiteStatementUtil;
import androidx.room.util.StringUtil;
@@ -623,20 +625,69 @@
}
@Override
- public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
- __db.assertNotSuspendingTransaction();
- final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
- try {
- final User _result;
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_fooBarUser(_cursor);
- } else {
- _result = null;
+ public PagingSource<Integer, Child1> loadItems() {
+ final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+ final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+ return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+ @Override
+ @NonNull
+ protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+ final int itemCount) {
+ _rawQuery.getBindingFunction().invoke(statement);
+ final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+ final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+ final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+ final List<Child1> _result = new ArrayList<Child1>();
+ while (statement.step()) {
+ final Child1 _item;
+ final int _tmpId;
+ _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+ final String _tmpName;
+ if (statement.isNull(_cursorIndexOfName)) {
+ _tmpName = null;
+ } else {
+ _tmpName = statement.getText(_cursorIndexOfName);
+ }
+ final Info _tmpInfo;
+ if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+ _tmpInfo = new Info();
+ _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+ if (statement.isNull(_cursorIndexOfCode)) {
+ _tmpInfo.code = null;
+ } else {
+ _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+ }
+ } else {
+ _tmpInfo = null;
+ }
+ _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+ _result.add(_item);
+ }
+ return _result;
}
- return _result;
- } finally {
- _cursor.close();
- }
+ };
+ }
+
+ @Override
+ public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
+ final RoomRawQuery _rawQuery = RoomSQLiteQuery.copyFrom(rawQuery).toRoomRawQuery();
+ final String _sql = _rawQuery.getSql();
+ return DBUtil.performBlocking(__db, true, false, (_connection) -> {
+ final SQLiteStatement _stmt = _connection.prepare(_sql);
+ try {
+ _rawQuery.getBindingFunction().invoke(_stmt);
+ final User _result;
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_fooBarUser(_stmt);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _stmt.close();
+ }
+ });
}
@NonNull
@@ -644,34 +695,34 @@
return Collections.emptyList();
}
- private User __entityCursorConverter_fooBarUser(@NonNull final Cursor cursor) {
+ private User __entityStatementConverter_fooBarUser(@NonNull final SQLiteStatement statement) {
final User _entity;
- final int _cursorIndexOfUid = CursorUtil.getColumnIndex(cursor, "uid");
- final int _cursorIndexOfName = CursorUtil.getColumnIndex(cursor, "name");
- final int _cursorIndexOfLastName = CursorUtil.getColumnIndex(cursor, "lastName");
- final int _cursorIndexOfAge = CursorUtil.getColumnIndex(cursor, "ageColumn");
+ final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndex(statement, "uid");
+ final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndex(statement, "name");
+ final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndex(statement, "lastName");
+ final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndex(statement, "ageColumn");
_entity = new User();
if (_cursorIndexOfUid != -1) {
- _entity.uid = cursor.getInt(_cursorIndexOfUid);
+ _entity.uid = (int) (statement.getLong(_cursorIndexOfUid));
}
if (_cursorIndexOfName != -1) {
- if (cursor.isNull(_cursorIndexOfName)) {
+ if (statement.isNull(_cursorIndexOfName)) {
_entity.name = null;
} else {
- _entity.name = cursor.getString(_cursorIndexOfName);
+ _entity.name = statement.getText(_cursorIndexOfName);
}
}
if (_cursorIndexOfLastName != -1) {
final String _tmpLastName;
- if (cursor.isNull(_cursorIndexOfLastName)) {
+ if (statement.isNull(_cursorIndexOfLastName)) {
_tmpLastName = null;
} else {
- _tmpLastName = cursor.getString(_cursorIndexOfLastName);
+ _tmpLastName = statement.getText(_cursorIndexOfLastName);
}
_entity.setLastName(_tmpLastName);
}
if (_cursorIndexOfAge != -1) {
- _entity.age = cursor.getInt(_cursorIndexOfAge);
+ _entity.age = (int) (statement.getLong(_cursorIndexOfAge));
}
return _entity;
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
index 18e3a2b..1a220ab 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx2.kt
@@ -49,7 +49,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -57,7 +57,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
@@ -87,7 +87,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -95,7 +95,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
index 463fa8b..509762a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/callableQuery_rx3.kt
@@ -49,7 +49,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -57,7 +57,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
@@ -87,7 +87,7 @@
}
val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_stmt, "other")
- val _result: MyEntity
+ val _result: MyEntity?
if (_stmt.step()) {
val _tmpPk: Int
_tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
@@ -95,7 +95,7 @@
_tmpOther = _stmt.getText(_cursorIndexOfOther)
_result = MyEntity(_tmpPk,_tmpOther)
} else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ _result = null
}
_result
} finally {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
index 0bb30fb..4e804c0 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
@@ -1,14 +1,11 @@
-import android.database.Cursor
-import androidx.room.CoroutinesRoom
import androidx.room.RoomDatabase
import androidx.room.RoomRawQuery
+import androidx.room.RoomSQLiteQuery
import androidx.room.coroutines.createFlow
import androidx.room.util.getColumnIndex
import androidx.room.util.performBlocking
-import androidx.room.util.query
import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteQuery
-import java.util.concurrent.Callable
import javax.`annotation`.processing.Generated
import kotlin.Double
import kotlin.Float
@@ -31,54 +28,64 @@
}
public override fun getEntitySupport(sql: SupportSQLiteQuery): MyEntity {
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, sql, false, null)
- try {
- val _result: MyEntity
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_MyEntity(_cursor)
- } else {
- error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ val _rawQuery: RoomRawQuery = RoomSQLiteQuery.copyFrom(sql).toRoomRawQuery()
+ val _sql: String = _rawQuery.sql
+ return performBlocking(__db, true, false) { _connection ->
+ val _stmt: SQLiteStatement = _connection.prepare(_sql)
+ try {
+ _rawQuery.getBindingFunction().invoke(_stmt)
+ val _result: MyEntity
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_MyEntity(_stmt)
+ } else {
+ error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
+ }
+ _result
+ } finally {
+ _stmt.close()
}
- return _result
- } finally {
- _cursor.close()
}
}
public override fun getNullableEntitySupport(sql: SupportSQLiteQuery): MyEntity? {
- __db.assertNotSuspendingTransaction()
- val _cursor: Cursor = query(__db, sql, false, null)
- try {
- val _result: MyEntity?
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_MyEntity(_cursor)
- } else {
- _result = null
+ val _rawQuery: RoomRawQuery = RoomSQLiteQuery.copyFrom(sql).toRoomRawQuery()
+ val _sql: String = _rawQuery.sql
+ return performBlocking(__db, true, false) { _connection ->
+ val _stmt: SQLiteStatement = _connection.prepare(_sql)
+ try {
+ _rawQuery.getBindingFunction().invoke(_stmt)
+ val _result: MyEntity?
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_MyEntity(_stmt)
+ } else {
+ _result = null
+ }
+ _result
+ } finally {
+ _stmt.close()
}
- return _result
- } finally {
- _cursor.close()
}
}
- public override fun getEntitySupportFlow(sql: SupportSQLiteQuery): Flow<MyEntity> =
- CoroutinesRoom.createFlow(__db, false, arrayOf("MyEntity"), object : Callable<MyEntity> {
- public override fun call(): MyEntity {
- val _cursor: Cursor = query(__db, sql, false, null)
+ public override fun getEntitySupportFlow(sql: SupportSQLiteQuery): Flow<MyEntity> {
+ val _rawQuery: RoomRawQuery = RoomSQLiteQuery.copyFrom(sql).toRoomRawQuery()
+ val _sql: String = _rawQuery.sql
+ return createFlow(__db, false, arrayOf("MyEntity")) { _connection ->
+ val _stmt: SQLiteStatement = _connection.prepare(_sql)
try {
+ _rawQuery.getBindingFunction().invoke(_stmt)
val _result: MyEntity
- if (_cursor.moveToFirst()) {
- _result = __entityCursorConverter_MyEntity(_cursor)
+ if (_stmt.step()) {
+ _result = __entityStatementConverter_MyEntity(_stmt)
} else {
error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
}
- return _result
+ _result
} finally {
- _cursor.close()
+ _stmt.close()
}
}
- })
+ }
public override fun getEntity(query: RoomRawQuery): MyEntity {
val _sql: String = query.sql
@@ -137,33 +144,6 @@
}
}
- private fun __entityCursorConverter_MyEntity(cursor: Cursor): MyEntity {
- val _entity: MyEntity
- val _cursorIndexOfPk: Int = getColumnIndex(cursor, "pk")
- val _cursorIndexOfDoubleColumn: Int = getColumnIndex(cursor, "doubleColumn")
- val _cursorIndexOfFloatColumn: Int = getColumnIndex(cursor, "floatColumn")
- val _tmpPk: Long
- if (_cursorIndexOfPk == -1) {
- _tmpPk = 0
- } else {
- _tmpPk = cursor.getLong(_cursorIndexOfPk)
- }
- val _tmpDoubleColumn: Double
- if (_cursorIndexOfDoubleColumn == -1) {
- _tmpDoubleColumn = 0.0
- } else {
- _tmpDoubleColumn = cursor.getDouble(_cursorIndexOfDoubleColumn)
- }
- val _tmpFloatColumn: Float
- if (_cursorIndexOfFloatColumn == -1) {
- _tmpFloatColumn = 0f
- } else {
- _tmpFloatColumn = cursor.getFloat(_cursorIndexOfFloatColumn)
- }
- _entity = MyEntity(_tmpPk,_tmpDoubleColumn,_tmpFloatColumn)
- return _entity
- }
-
private fun __entityStatementConverter_MyEntity(statement: SQLiteStatement): MyEntity {
val _entity: MyEntity
val _cursorIndexOfPk: Int = getColumnIndex(statement, "pk")
diff --git a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
index fb7d965..820c1d5 100644
--- a/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
+++ b/room/room-paging/src/androidMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.android.kt
@@ -46,19 +46,14 @@
sourceQuery: RoomSQLiteQuery,
db: RoomDatabase,
vararg tables: String,
- ) : this(
- sourceQuery =
- RoomRawQuery(sql = sourceQuery.sql, onBindStatement = { sourceQuery.bindTo(it) }),
- db = db,
- tables = tables
- )
+ ) : this(sourceQuery = sourceQuery.toRoomRawQuery(), db = db, tables = tables)
constructor(
supportSQLiteQuery: SupportSQLiteQuery,
db: RoomDatabase,
vararg tables: String,
) : this(
- sourceQuery = RoomSQLiteQuery.copyFrom(supportSQLiteQuery),
+ sourceQuery = RoomSQLiteQuery.copyFrom(supportSQLiteQuery).toRoomRawQuery(),
db = db,
tables = tables,
)
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 80ef901..0fb8a01 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -60,12 +60,12 @@
method public final int handleMultiple(androidx.sqlite.SQLiteConnection connection, T?[]? entities);
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
- ctor public EntityDeletionOrUpdateAdapter(androidx.room.RoomDatabase database);
- method protected abstract void bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity);
- method public final int handle(T entity);
- method public final int handleMultiple(Iterable<? extends T> entities);
- method public final int handleMultiple(T[] entities);
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
+ ctor @Deprecated public EntityDeletionOrUpdateAdapter(androidx.room.RoomDatabase database);
+ method @Deprecated protected abstract void bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity);
+ method @Deprecated public final int handle(T entity);
+ method @Deprecated public final int handleMultiple(Iterable<? extends T> entities);
+ method @Deprecated public final int handleMultiple(T[] entities);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityInsertAdapter<T> {
@@ -84,19 +84,19 @@
method public final java.util.List<java.lang.Long> insertAndReturnIdsList(androidx.sqlite.SQLiteConnection connection, T?[]? entities);
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityInsertionAdapter<T> extends androidx.room.SharedSQLiteStatement {
- ctor public EntityInsertionAdapter(androidx.room.RoomDatabase database);
- method protected abstract void bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity);
- method public final void insert(Iterable<? extends T> entities);
- method public final void insert(T entity);
- method public final void insert(T[] entities);
- method public final long insertAndReturnId(T entity);
- method public final long[] insertAndReturnIdsArray(java.util.Collection<? extends T> entities);
- method public final long[] insertAndReturnIdsArray(T[] entities);
- method public final Long[] insertAndReturnIdsArrayBox(java.util.Collection<? extends T> entities);
- method public final Long[] insertAndReturnIdsArrayBox(T[] entities);
- method public final java.util.List<java.lang.Long> insertAndReturnIdsList(java.util.Collection<? extends T> entities);
- method public final java.util.List<java.lang.Long> insertAndReturnIdsList(T[] entities);
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityInsertionAdapter<T> extends androidx.room.SharedSQLiteStatement {
+ ctor @Deprecated public EntityInsertionAdapter(androidx.room.RoomDatabase database);
+ method @Deprecated protected abstract void bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity);
+ method @Deprecated public final void insert(Iterable<? extends T> entities);
+ method @Deprecated public final void insert(T entity);
+ method @Deprecated public final void insert(T[] entities);
+ method @Deprecated public final long insertAndReturnId(T entity);
+ method @Deprecated public final long[] insertAndReturnIdsArray(java.util.Collection<? extends T> entities);
+ method @Deprecated public final long[] insertAndReturnIdsArray(T[] entities);
+ method @Deprecated public final Long[] insertAndReturnIdsArrayBox(java.util.Collection<? extends T> entities);
+ method @Deprecated public final Long[] insertAndReturnIdsArrayBox(T[] entities);
+ method @Deprecated public final java.util.List<java.lang.Long> insertAndReturnIdsList(java.util.Collection<? extends T> entities);
+ method @Deprecated public final java.util.List<java.lang.Long> insertAndReturnIdsList(T[] entities);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EntityUpsertAdapter<T> {
@@ -117,18 +117,18 @@
public static final class EntityUpsertAdapter.Companion {
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EntityUpsertionAdapter<T> {
- ctor public EntityUpsertionAdapter(androidx.room.EntityInsertionAdapter<T> insertionAdapter, androidx.room.EntityDeletionOrUpdateAdapter<T> updateAdapter);
- method public void upsert(Iterable<? extends T> entities);
- method public void upsert(T entity);
- method public void upsert(T[] entities);
- method public long upsertAndReturnId(T entity);
- method public long[] upsertAndReturnIdsArray(java.util.Collection<? extends T> entities);
- method public long[] upsertAndReturnIdsArray(T[] entities);
- method public Long[] upsertAndReturnIdsArrayBox(java.util.Collection<? extends T> entities);
- method public Long[] upsertAndReturnIdsArrayBox(T[] entities);
- method public java.util.List<java.lang.Long> upsertAndReturnIdsList(java.util.Collection<? extends T> entities);
- method public java.util.List<java.lang.Long> upsertAndReturnIdsList(T[] entities);
+ @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EntityUpsertionAdapter<T> {
+ ctor @Deprecated public EntityUpsertionAdapter(androidx.room.EntityInsertionAdapter<T> insertionAdapter, androidx.room.EntityDeletionOrUpdateAdapter<T> updateAdapter);
+ method @Deprecated public void upsert(Iterable<? extends T> entities);
+ method @Deprecated public void upsert(T entity);
+ method @Deprecated public void upsert(T[] entities);
+ method @Deprecated public long upsertAndReturnId(T entity);
+ method @Deprecated public long[] upsertAndReturnIdsArray(java.util.Collection<? extends T> entities);
+ method @Deprecated public long[] upsertAndReturnIdsArray(T[] entities);
+ method @Deprecated public Long[] upsertAndReturnIdsArrayBox(java.util.Collection<? extends T> entities);
+ method @Deprecated public Long[] upsertAndReturnIdsArrayBox(T[] entities);
+ method @Deprecated public java.util.List<java.lang.Long> upsertAndReturnIdsList(java.util.Collection<? extends T> entities);
+ method @Deprecated public java.util.List<java.lang.Long> upsertAndReturnIdsList(T[] entities);
}
@SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalRoomApi {
@@ -359,6 +359,7 @@
method public String getSql();
method public void init(String query, int initArgCount);
method public void release();
+ method public androidx.room.RoomRawQuery toRoomRawQuery();
property public int argCount;
property public final int capacity;
property public String sql;
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityDeletionOrUpdateAdapter.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityDeletionOrUpdateAdapter.android.kt
index 019825b..16ecdf8 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityDeletionOrUpdateAdapter.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityDeletionOrUpdateAdapter.android.kt
@@ -28,6 +28,7 @@
* given database.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated("No longer used by generated code.", ReplaceWith("EntityDeleteOrUpdateAdapter"))
abstract class EntityDeletionOrUpdateAdapter<T>(database: RoomDatabase) :
SharedSQLiteStatement(database) {
/**
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityInsertionAdapter.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityInsertionAdapter.android.kt
index 4b54029..aa1be1a 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityInsertionAdapter.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityInsertionAdapter.android.kt
@@ -28,6 +28,7 @@
* database.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated("No longer used by generated code.", ReplaceWith("EntityInsertAdapter"))
abstract class EntityInsertionAdapter<T>(database: RoomDatabase) : SharedSQLiteStatement(database) {
/**
* Binds the entity into the given statement.
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityUpsertionAdapter.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityUpsertionAdapter.android.kt
index 3c61d3b..bc8c8f2 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityUpsertionAdapter.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/EntityUpsertionAdapter.android.kt
@@ -43,9 +43,10 @@
* the insertion fails
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated("No longer used by generated code.", ReplaceWith("EntityUpsertAdapter"))
class EntityUpsertionAdapter<T>(
- private val insertionAdapter: EntityInsertionAdapter<T>,
- private val updateAdapter: EntityDeletionOrUpdateAdapter<T>
+ @Suppress("DEPRECATION") private val insertionAdapter: EntityInsertionAdapter<T>,
+ @Suppress("DEPRECATION") private val updateAdapter: EntityDeletionOrUpdateAdapter<T>
) {
/**
* Inserts the entity into the database. If a constraint exception is thrown i.e. a primary key
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index b63a06e..f5d77e9 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -23,7 +23,6 @@
import android.content.Context
import android.content.Intent
import android.database.Cursor
-import android.os.Build
import android.os.CancellationSignal
import android.os.Looper
import android.util.Log
@@ -502,22 +501,15 @@
assertNotSuspendingTransaction()
runBlocking {
connectionManager.useConnection(isReadOnly = false) { connection ->
- val supportsDeferForeignKeys = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
- if (hasForeignKeys && !supportsDeferForeignKeys) {
- connection.execSQL("PRAGMA foreign_keys = FALSE")
- }
if (!connection.inTransaction()) {
invalidationTracker.sync()
}
connection.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
- if (hasForeignKeys && supportsDeferForeignKeys) {
+ if (hasForeignKeys) {
execSQL("PRAGMA defer_foreign_keys = TRUE")
}
tableNames.forEach { tableName -> execSQL("DELETE FROM `$tableName`") }
}
- if (hasForeignKeys && !supportsDeferForeignKeys) {
- connection.execSQL("PRAGMA foreign_keys = TRUE")
- }
if (!connection.inTransaction()) {
connection.execSQL("PRAGMA wal_checkpoint(FULL)")
connection.execSQL("VACUUM")
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
index b0a6438..a423a69 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomSQLiteQuery.android.kt
@@ -78,6 +78,11 @@
}
}
+ /** Converts a SupportSQLiteStatement to a [RoomRawQuery]. */
+ fun toRoomRawQuery(): RoomRawQuery {
+ return RoomRawQuery(sql = this.sql, onBindStatement = { this.bindTo(it) })
+ }
+
override val sql: String
get() = checkNotNull(this.query)
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
import androidx.sqlite.SQLiteConnection
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
private val computing = AtomicBoolean(false)
private val registeredObserver = AtomicBoolean(false)
+ private val launchContext =
+ if (database.inCompatibilityMode()) {
+ if (inTransaction) {
+ database.getTransactionContext()
+ } else {
+ database.getQueryContext()
+ }
+ } else {
+ EmptyCoroutineContext
+ }
+
private suspend fun refresh() {
if (registeredObserver.compareAndSet(false, true)) {
database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
}
}
@@ -115,7 +126,7 @@
override fun onActive() {
super.onActive()
container.onActive(this)
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
override fun onInactive() {
@@ -132,13 +143,7 @@
private val callableFunction: Callable<T?>
) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
override suspend fun compute(): T? {
- val queryContext =
- if (inTransaction) {
- database.getTransactionContext()
- } else {
- database.getQueryContext()
- }
- return withContext(queryContext) { callableFunction.call() }
+ return callableFunction.call()
}
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
index 8950000..bc9b664 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
@@ -15,7 +15,6 @@
*/
package androidx.room.util
-import android.os.Build
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.room.ColumnInfo.SQLiteTypeAffinity
@@ -207,13 +206,3 @@
actual override fun toString() = toStringCommon()
}
}
-
-/** Checks if the primary key match. */
-internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
- if (Build.VERSION.SDK_INT >= 20) {
- if (primaryKeyPosition != other.primaryKeyPosition) return false
- } else {
- if (isPrimaryKey != other.isPrimaryKey) return false
- }
- return true
-}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
index 9f41f2d6..feb7d93 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
@@ -201,7 +201,7 @@
internal fun TableInfo.Column.equalsCommon(other: Any?): Boolean {
if (this === other) return true
if (other !is TableInfo.Column) return false
- if (!equalsInPrimaryKey(other)) return false
+ if (isPrimaryKey != other.isPrimaryKey) return false
if (name != other.name) return false
if (notNull != other.notNull) return false
// Only validate default value if it was defined in an entity, i.e. if the info
@@ -231,9 +231,6 @@
return affinity == other.affinity
}
-/** Checks if the primary key match. */
-internal expect fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean
-
/**
* Checks if the default values provided match. Handles the special case in which the default value
* is surrounded by parenthesis (e.g. encountered in b/182284899).
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/UUIDUtil.android.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/util/UUIDUtil.jvmAndroid.kt
similarity index 100%
rename from room/room-runtime/src/androidMain/kotlin/androidx/room/util/UUIDUtil.android.kt
rename to room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/util/UUIDUtil.jvmAndroid.kt
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/TableInfo.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/TableInfo.jvmNative.kt
index 20a2a9d..0e2f13d 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/TableInfo.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/util/TableInfo.jvmNative.kt
@@ -156,8 +156,3 @@
actual override fun toString() = toStringCommon()
}
}
-
-/** Checks if the primary key match. */
-internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
- return isPrimaryKey == other.isPrimaryKey
-}
diff --git a/samples/SupportLeanbackDemos/build.gradle b/samples/SupportLeanbackDemos/build.gradle
index e9845a8..ce71e5e 100644
--- a/samples/SupportLeanbackDemos/build.gradle
+++ b/samples/SupportLeanbackDemos/build.gradle
@@ -19,9 +19,9 @@
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation(libs.constraintLayout)
- implementation(projectOrArtifact(":room:room-paging"))
- implementation(projectOrArtifact(":room:room-runtime"))
- annotationProcessor(projectOrArtifact(":room:room-compiler"))
+ implementation(project(":room:room-paging"))
+ implementation(project(":room:room-runtime"))
+ annotationProcessor(project(":room:room-compiler"))
}
android {
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index 7df64b7..b863381 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -51,7 +51,7 @@
androidInstrumentedTest {
dependsOn(jvmTest)
dependencies {
- implementation(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
implementation(libs.testExtJunit)
implementation(libs.testCore)
implementation(libs.testRunner)
diff --git a/settings.gradle b/settings.gradle
index 988cca5..a898312 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
classpath("com.gradle:develocity-gradle-plugin:3.18")
classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
- classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+ if (agpOverride != null) {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+ } else {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ }
}
}
@@ -83,9 +88,7 @@
apply(plugin: "androidx.build.gradle.gcpbuildcache")
apply(plugin: "com.android.settings")
-android {
- ndkVersion = "27.0.12077973"
-}
+apply(from: "buildSrc/ndk.gradle")
def BUILD_NUMBER = System.getenv("BUILD_NUMBER")
develocity {
@@ -402,8 +405,6 @@
includeProject(":benchmark:integration-tests:startup-benchmark", [BuildType.MAIN])
includeProject(":binarycompatibilityvalidator:binarycompatibilityvalidator", [BuildType.MAIN])
includeProject(":biometric:biometric", [BuildType.MAIN])
-includeProject(":biometric:biometric-ktx", [BuildType.MAIN])
-includeProject(":biometric:biometric-ktx-samples", "biometric/biometric-ktx/samples", [BuildType.MAIN])
includeProject(":biometric:integration-tests:testapp", [BuildType.MAIN])
includeProject(":bluetooth:bluetooth", [BuildType.MAIN])
includeProject(":bluetooth:bluetooth-testing", [BuildType.MAIN])
@@ -429,6 +430,7 @@
includeProject(":camera:camera-feature-combination-query", [BuildType.CAMERA])
includeProject(":camera:camera-feature-combination-query-play-services", [BuildType.CAMERA])
includeProject(":camera:camera-lifecycle", [BuildType.CAMERA])
+includeProject(":camera:camera-media3-effect", [BuildType.CAMERA])
includeProject(":camera:camera-lifecycle:camera-lifecycle-samples", "camera/camera-lifecycle/samples", [BuildType.CAMERA])
includeProject(":camera:camera-mlkit-vision", [BuildType.CAMERA])
includeProject(":camera:camera-testing", [BuildType.CAMERA])
@@ -440,6 +442,7 @@
includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-diagnose", "camera/integration-tests/diagnosetestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-testing", "camera/integration-tests/testingtestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-uiwidgets", "camera/integration-tests/uiwidgetstestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-viewfinder", "camera/integration-tests/viewfindertestapp", [BuildType.CAMERA])
@@ -508,6 +511,7 @@
includeProject(":compose:material3:adaptive:adaptive", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-layout", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-navigation", [BuildType.COMPOSE])
+includeProject(":compose:material3:adaptive:adaptive-render-strategy", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-samples", "compose/material3/adaptive/samples", [BuildType.COMPOSE])
includeProject(":compose:material3:adaptive:adaptive-benchmark", "compose/material3/adaptive/benchmark", [BuildType.COMPOSE])
includeProject(":compose:material3:material3", [BuildType.COMPOSE])
@@ -626,6 +630,7 @@
includeProject(":core:core-role", [BuildType.MAIN])
includeProject(":core:core-telecom", [BuildType.MAIN])
includeProject(":core:core-telecom:integration-tests:testapp", [BuildType.MAIN])
+includeProject(":core:core-telecom:integration-tests:testicsapp", [BuildType.MAIN])
includeProject(":core:haptics:haptics", [BuildType.MAIN])
includeProject(":core:haptics:haptics-samples", "core/haptics/haptics/samples", [BuildType.MAIN])
includeProject(":core:haptics:haptics-demos", "core/haptics/haptics/integration-tests/demos", [BuildType.MAIN])
@@ -696,6 +701,7 @@
includeProject(":glance:glance", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-external-protobuf", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-multiprocess", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-testing", [BuildType.GLANCE])
@@ -743,6 +749,7 @@
includeProject(":ink:ink-geometry", [BuildType.MAIN])
includeProject(":ink:ink-nativeloader", [BuildType.MAIN])
includeProject(":ink:ink-strokes", [BuildType.MAIN])
+includeProject(":ink:ink-rendering", [BuildType.MAIN])
includeProject(":input:input-motionprediction", [BuildType.MAIN])
includeProject(":inspection:inspection", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":inspection:inspection-gradle-plugin", [BuildType.MAIN])
diff --git a/sharetarget/sharetarget/build.gradle b/sharetarget/sharetarget/build.gradle
index 83f12c4..eb41cbe 100644
--- a/sharetarget/sharetarget/build.gradle
+++ b/sharetarget/sharetarget/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-benchmark/build.gradle b/slice/slice-benchmark/build.gradle
index f895e78..02b8c23 100644
--- a/slice/slice-benchmark/build.gradle
+++ b/slice/slice-benchmark/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-core/build.gradle b/slice/slice-core/build.gradle
index 96d5715..9537bbb 100644
--- a/slice/slice-core/build.gradle
+++ b/slice/slice-core/build.gradle
@@ -37,8 +37,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
annotationProcessor (project(":versionedparcelable:versionedparcelable-compiler"))
}
diff --git a/slice/slice-remotecallback/build.gradle b/slice/slice-remotecallback/build.gradle
index 2cbdcd7..d726d05 100644
--- a/slice/slice-remotecallback/build.gradle
+++ b/slice/slice-remotecallback/build.gradle
@@ -38,8 +38,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestAnnotationProcessor project(":remotecallback:remotecallback-processor")
}
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index a485674..d918271 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -40,8 +40,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/slice/slice-view/build.gradle b/slice/slice-view/build.gradle
index 302d901..88a74e9 100644
--- a/slice/slice-view/build.gradle
+++ b/slice/slice-view/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore4)
+ androidTestImplementation(libs.dexmakerMockito)
}
androidx {
diff --git a/startup/integration-tests/first-library/build.gradle b/startup/integration-tests/first-library/build.gradle
index ef0402b..75c7de1 100644
--- a/startup/integration-tests/first-library/build.gradle
+++ b/startup/integration-tests/first-library/build.gradle
@@ -29,8 +29,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/startup/integration-tests/second-library/build.gradle b/startup/integration-tests/second-library/build.gradle
index 96216cb..c9c6ced 100644
--- a/startup/integration-tests/second-library/build.gradle
+++ b/startup/integration-tests/second-library/build.gradle
@@ -28,8 +28,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/startup/startup-runtime/build.gradle b/startup/startup-runtime/build.gradle
index c0b692d..5a9bd97 100644
--- a/startup/startup-runtime/build.gradle
+++ b/startup/startup-runtime/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/swiperefreshlayout/swiperefreshlayout/build.gradle b/swiperefreshlayout/swiperefreshlayout/build.gradle
index 28bce37..5ae85eb 100644
--- a/swiperefreshlayout/swiperefreshlayout/build.gradle
+++ b/swiperefreshlayout/swiperefreshlayout/build.gradle
@@ -24,8 +24,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.swiperefreshlayout", module: "swiperefreshlayout"
diff --git a/testutils/testutils-lifecycle/build.gradle b/testutils/testutils-lifecycle/build.gradle
index b771af6..9d088a1 100644
--- a/testutils/testutils-lifecycle/build.gradle
+++ b/testutils/testutils-lifecycle/build.gradle
@@ -46,7 +46,7 @@
sourceSets {
commonMain {
dependencies {
- api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-runtime"))
api("androidx.annotation:annotation:1.8.1")
api(libs.kotlinStdlib)
diff --git a/testutils/testutils-mockito/build.gradle b/testutils/testutils-mockito/build.gradle
index c50d41e..8550ce9 100644
--- a/testutils/testutils-mockito/build.gradle
+++ b/testutils/testutils-mockito/build.gradle
@@ -30,7 +30,7 @@
}
dependencies {
- api(libs.mockitoCore, excludes.bytebuddy)
+ api(libs.mockitoCore)
implementation(libs.kotlinStdlib)
}
diff --git a/testutils/testutils-navigation/build.gradle b/testutils/testutils-navigation/build.gradle
index 773ee21..52271cd 100644
--- a/testutils/testutils-navigation/build.gradle
+++ b/testutils/testutils-navigation/build.gradle
@@ -31,9 +31,9 @@
}
dependencies {
- api(projectOrArtifact(":navigation:navigation-common"))
+ api(project(":navigation:navigation-common"))
- testImplementation(projectOrArtifact(":navigation:navigation-testing"))
+ testImplementation(project(":navigation:navigation-testing"))
testImplementation("androidx.arch.core:core-testing:2.1.0")
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index d1de213..39fdd42 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -28,8 +28,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.opentest4j)
androidTestImplementation(project(":fragment:fragment"))
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
diff --git a/tv/integration-tests/macrobenchmark-target/build.gradle b/tv/integration-tests/macrobenchmark-target/build.gradle
index 2fca186..7d55cd9 100644
--- a/tv/integration-tests/macrobenchmark-target/build.gradle
+++ b/tv/integration-tests/macrobenchmark-target/build.gradle
@@ -43,11 +43,7 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation("androidx.activity:activity-compose:1.9.0")
- implementation("androidx.compose.runtime:runtime:1.6.7")
implementation("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")
- implementation("androidx.compose.ui:ui:1.6.7")
- implementation("androidx.compose.ui:ui-tooling:1.6.7")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
implementation(project(":tv:tv-foundation"))
implementation(project(":tv:tv-material"))
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index 8d72b69..c2bfe40 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -37,9 +37,7 @@
dependencies {
api(libs.kotlinStdlib)
- def annotationVersion = "1.8.0"
def composeVersion = "1.6.8"
- def profileInstallerVersion = "1.3.1"
api("androidx.annotation:annotation:1.8.1")
api("androidx.compose.animation:animation:$composeVersion")
@@ -51,7 +49,7 @@
api("androidx.compose.ui:ui-graphics:$composeVersion")
api("androidx.compose.ui:ui-text:$composeVersion")
- implementation("androidx.profileinstaller:profileinstaller:$profileInstallerVersion")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
androidTestImplementation(libs.truth)
androidTestImplementation(project(":compose:runtime:runtime"))
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 59df813..f5ae584 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -35,7 +35,7 @@
def annotationVersion = "1.8.0"
def composeVersion = "1.6.8"
- def profileInstallerVersion = "1.3.1"
+ def profileInstallerVersion = "1.4.0"
api("androidx.annotation:annotation:1.8.1")
api("androidx.compose.animation:animation:$composeVersion")
@@ -48,7 +48,7 @@
api("androidx.compose.ui:ui-graphics:$composeVersion")
api("androidx.compose.ui:ui-text:$composeVersion")
- implementation("androidx.profileinstaller:profileinstaller:$profileInstallerVersion")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
androidTestImplementation(libs.truth)
androidTestImplementation(project(":compose:runtime:runtime"))
diff --git a/versionedparcelable/versionedparcelable/build.gradle b/versionedparcelable/versionedparcelable/build.gradle
index 4a1ab25..abf8f79 100644
--- a/versionedparcelable/versionedparcelable/build.gradle
+++ b/versionedparcelable/versionedparcelable/build.gradle
@@ -36,8 +36,8 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
androidTestAnnotationProcessor project(":versionedparcelable:versionedparcelable-compiler")
}
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 70f64ab..2e98fc6 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -22,8 +22,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation project(':internal-testutils-espresso')
}
diff --git a/viewpager2/viewpager2/build.gradle b/viewpager2/viewpager2/build.gradle
index 67c9a66..e3f2872 100644
--- a/viewpager2/viewpager2/build.gradle
+++ b/viewpager2/viewpager2/build.gradle
@@ -43,8 +43,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-espresso"))
androidTestImplementation(project(":internal-testutils-appcompat"), {
exclude group: "androidx.viewpager2", module: "viewpager2"
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 4eacf1f..091eb148 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -39,7 +39,7 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.constraintLayout)
- implementation projectOrArtifact(":activity:activity-ktx")
+ implementation project(":activity:activity-ktx")
implementation 'androidx.core:core-ktx'
implementation(libs.material)
implementation(project(":profileinstaller:profileinstaller"))
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..46ad047d 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -315,7 +315,8 @@
public final class ScrollInfoProviderKt {
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.lazy.LazyListState state);
- method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.ScrollState state, optional float bottomButtonHeight);
+ method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.ScrollState state);
+ method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.LazyColumnState state);
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.ScalingLazyListState state);
}
@@ -388,8 +389,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..46ad047d 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -315,7 +315,8 @@
public final class ScrollInfoProviderKt {
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.lazy.LazyListState state);
- method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.ScrollState state, optional float bottomButtonHeight);
+ method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.compose.foundation.ScrollState state);
+ method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.LazyColumnState state);
method public static androidx.wear.compose.foundation.ScrollInfoProvider ScrollInfoProvider(androidx.wear.compose.foundation.lazy.ScalingLazyListState state);
}
@@ -388,8 +389,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 4844a72..c8e5dc70 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -42,7 +42,7 @@
implementation("androidx.compose.ui:ui-util:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation("androidx.core:core:1.12.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
index a449893..13c0e98 100644
--- a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
@@ -18,8 +18,10 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -262,6 +264,30 @@
}
@Composable
+fun CurvedFontHeight() {
+ Box(
+ modifier =
+ Modifier.aspectRatio(1f)
+ .fillMaxSize()
+ .padding(2.dp)
+ .border(2.dp, Color.White, CircleShape)
+ ) {
+ CurvedLayout() {
+ basicCurvedText(
+ "9⎪:⎪0",
+ style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp),
+ )
+ }
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.CounterClockwise) {
+ basicCurvedText(
+ "9⎪:⎪0",
+ style = CurvedTextStyle(color = Color.Green, fontSize = 30.sp),
+ )
+ }
+ }
+}
+
+@Composable
fun CurvedFonts() {
CurvedLayout(
modifier = Modifier.fillMaxSize(),
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+ @get:Rule val rule = createComposeRule()
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(
+ count = 2,
+ spacing = itemSizePx,
+ startOffset = itemSizePx
+ )
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: LazyColumnState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyColumnLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 4f)
+ ) {
+ item { Box(Modifier.requiredSize(size)) }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+ items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(sizeDp).width(sizeDp * 2),
+ state = rememberLazyColumnState().also { state = it }
+ ) {
+ items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+ }
+ }
+
+ private fun LazyColumnLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItems.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItems.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex")
+ .that(it.offset)
+ .isEqualTo(currentOffset)
+ assertThat(it.height).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.height + spacing
+ }
+ }
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
index f80264a..e4033bf 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
@@ -46,6 +47,7 @@
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performRotaryScrollInput
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
@@ -998,6 +1000,45 @@
rule.onNodeWithTag("scalingLazyColumn").assertIsFocused()
}
+
+ @Test
+ fun scalingLazyColumn_rotary_enabledScroll() {
+ testScalingLazyColumnRotary(true, 3)
+ }
+
+ @Test
+ fun scalingLazyColumn_noRotary_disabledScroll() {
+ testScalingLazyColumnRotary(false, 1)
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ private fun testScalingLazyColumnRotary(
+ userScrollEnabled: Boolean,
+ scrollTarget: Int,
+ scrollItems: Int = 2
+ ) {
+ lateinit var state: ScalingLazyListState
+
+ rule.setContent {
+ state = rememberScalingLazyListState()
+ ScalingLazyColumn(
+ state = state,
+ modifier = Modifier.testTag(scalingLazyColumnTag),
+ userScrollEnabled = userScrollEnabled
+ ) {
+ items(100) {
+ BasicText(text = "item $it", modifier = Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+ rule.onNodeWithTag(scalingLazyColumnTag).performRotaryScrollInput {
+ // try to scroll by N items
+ rotateToScrollVertically(itemSizePx.toFloat() * scrollItems)
+ }
+ rule.waitForIdle()
+
+ assertThat(state.centerItemIndex).isEqualTo(scrollTarget)
+ }
}
internal const val TestTouchSlop = 18f
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicCurvedText.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicCurvedText.kt
index 1ae17c6..271818e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicCurvedText.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicCurvedText.kt
@@ -312,8 +312,8 @@
paint.getTextBounds(text, 0, text.length, rect)
textWidth = rect.width().toFloat()
- textHeight = -paint.fontMetrics.top + paint.fontMetrics.bottom
- baseLinePosition = if (clockwise) -paint.fontMetrics.top else paint.fontMetrics.bottom
+ textHeight = -paint.fontMetrics.ascent + paint.fontMetrics.descent
+ baseLinePosition = if (clockwise) -paint.fontMetrics.ascent else paint.fontMetrics.descent
}
private fun updateTypeFace() {
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/ScrollInfoProvider.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/ScrollInfoProvider.kt
index 6cfbb43..2fe51fe 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/ScrollInfoProvider.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/ScrollInfoProvider.kt
@@ -19,13 +19,11 @@
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastForEach
+import androidx.wear.compose.foundation.lazy.LazyColumnState
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
-import androidx.wear.compose.foundation.lazy.ScalingLazyListItemInfo
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.startOffset
@@ -72,36 +70,40 @@
/**
* Function for creating a [ScrollInfoProvider] from a [ScalingLazyListState], for use with
- * [ScalingLazyColumn] - used to create a ScrollAway modifier directly that can be applied to an
- * object that appears at the top of the screen, to scroll it away vertically when the
- * [ScalingLazyColumn] is scrolled upwards.
- *
- * @param state
+ * [ScalingLazyColumn] - used to coordinate between scrollable content and scaffold content such as
+ * [TimeText] which is scrolled away at the top of the screen and [EdgeButton] which is scaled.
*/
fun ScrollInfoProvider(state: ScalingLazyListState): ScrollInfoProvider =
ScalingLazyListStateScrollInfoProvider(state)
/**
* Function for creating a [ScrollInfoProvider] from a [LazyListState], for use with [LazyColumn] -
- * used to create a ScrollAway modifier directly that can be applied to an object that appears at
- * the top of the screen, to scroll it away vertically when the [LazyColumn] is scrolled upwards.
+ * used to coordinate between scrollable content and scaffold content such as [TimeText] which is
+ * scrolled away at the top of the screen and [EdgeButton] which is scaled.
*/
fun ScrollInfoProvider(state: LazyListState): ScrollInfoProvider =
LazyListStateScrollInfoProvider(state)
/**
+ * Function for creating a [ScrollInfoProvider] from a [LazyColumnState], for use with
+ * [androidx.wear.compose.foundation.lazy.LazyColumn] - used to coordinate between scrollable
+ * content and scaffold content such as [TimeText] which is scrolled away at the top of the screen
+ * and [EdgeButton] which is scaled.
+ */
+fun ScrollInfoProvider(state: LazyColumnState): ScrollInfoProvider =
+ LazyColumnStateScrollInfoProvider(state)
+
+/**
* Function for creating a [ScrollInfoProvider] from a [ScrollState], for use with [Column] - used
- * to create a ScrollAway modifier directly that can be applied to an object that appears at the top
- * of the screen, to scroll it away vertically when the [Column] is scrolled upwards.
+ * to coordinate between scrollable content and scaffold content such as [TimeText] which is
+ * scrolled away at the top of the screen and [EdgeButton] which is scaled.
*
* @param state the [ScrollState] to use as the base for creating the [ScrollInfoProvider]
- * @param bottomButtonHeight optional parameter to specify the size of a bottom button if one is
- * provided.
*/
-fun ScrollInfoProvider(state: ScrollState, bottomButtonHeight: Float = 0f): ScrollInfoProvider =
- ScrollStateScrollInfoProvider(state, bottomButtonHeight)
+fun ScrollInfoProvider(state: ScrollState): ScrollInfoProvider =
+ ScrollStateScrollInfoProvider(state)
-// Implementation of [ScrollAwayInfoProvider] for [ScalingLazyColumn].
+// Implementation of [ScrollInfoProvider] for [ScalingLazyColumn].
// Being in Foundation, this implementation has access to the ScalingLazyListState
// auto-centering params, which are internal.
private class ScalingLazyListStateScrollInfoProvider(val state: ScalingLazyListState) :
@@ -136,14 +138,13 @@
override val lastItemOffset: Float
get() {
val screenHeightPx = state.config.value?.viewportHeightPx ?: 0
- var lastItemInfo: ScalingLazyListItemInfo? = null
- state.layoutInfo.visibleItemsInfo.fastForEach { ii ->
- if (ii.index == state.layoutInfo.totalItemsCount - 1) lastItemInfo = ii
- }
- return lastItemInfo?.let {
- val bottomEdge = it.offset + screenHeightPx / 2 + it.size / 2
- (screenHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
- } ?: 0f
+ val layoutInfo = state.layoutInfo
+ return layoutInfo.visibleItemsInfo
+ .fastFirstOrNull { ii -> ii.index == layoutInfo.totalItemsCount - 1 }
+ ?.let {
+ val bottomEdge = it.offset + screenHeightPx / 2 + it.size / 2
+ (screenHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
+ } ?: 0f
}
override fun toString(): String {
@@ -157,7 +158,7 @@
private var initialStartOffset: Float? = null
}
-// Implementation of [ScrollAwayInfoProvider] for [LazyColumn].
+// Implementation of [ScrollInfoProvider] for [LazyColumn].
private class LazyListStateScrollInfoProvider(val state: LazyListState) : ScrollInfoProvider {
override val isScrollAwayValid
get() = state.layoutInfo.totalItemsCount > 0
@@ -176,15 +177,14 @@
override val lastItemOffset: Float
get() {
- val screenHeightPx = state.layoutInfo.viewportSize.height
- var lastItemInfo: LazyListItemInfo? = null
- state.layoutInfo.visibleItemsInfo.fastForEach { ii ->
- if (ii.index == state.layoutInfo.totalItemsCount - 1) lastItemInfo = ii
- }
- return lastItemInfo?.let {
- val bottomEdge = it.offset + it.size - state.layoutInfo.viewportStartOffset
- (screenHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
- } ?: 0f
+ val layoutInfo = state.layoutInfo
+ val screenHeightPx = layoutInfo.viewportSize.height
+ return layoutInfo.visibleItemsInfo
+ .fastFirstOrNull { ii -> ii.index == layoutInfo.totalItemsCount - 1 }
+ ?.let {
+ val bottomEdge = it.offset + it.size - layoutInfo.viewportStartOffset
+ (screenHeightPx - bottomEdge).toFloat().coerceAtLeast(0f)
+ } ?: 0f
}
override fun toString(): String {
@@ -196,11 +196,8 @@
}
}
-// Implementation of [ScrollAwayInfoProvider] for [Column]
-private class ScrollStateScrollInfoProvider(
- val state: ScrollState,
- val bottomButtonHeight: Float = 0f
-) : ScrollInfoProvider {
+// Implementation of [ScrollInfoProvider] for [Column].
+private class ScrollStateScrollInfoProvider(val state: ScrollState) : ScrollInfoProvider {
override val isScrollAwayValid: Boolean
get() = true
@@ -219,10 +216,7 @@
get() = state.value.toFloat()
override val lastItemOffset: Float
- get() {
- return if (state.maxValue == Int.MAX_VALUE || bottomButtonHeight == 0f) 0f
- else (state.value - state.maxValue + bottomButtonHeight).coerceAtLeast(0f)
- }
+ get() = 0f
override fun toString(): String {
return "ScrollStateScrollInfoProvider(isScrollAwayValid=$isScrollAwayValid, " +
@@ -232,3 +226,47 @@
"lastItemOffset=$lastItemOffset)"
}
}
+
+// Implementation of [ScrollInfoProvider] for [androidx.wear.compose.foundation.lazy.LazyColumn].
+private class LazyColumnStateScrollInfoProvider(val state: LazyColumnState) : ScrollInfoProvider {
+ override val isScrollAwayValid
+ get() = state.layoutInfo.totalItemsCount > 0
+
+ override val isScrollable
+ // TODO: b/349071165 - Update to respect the scroll bounds.
+ get() = state.layoutInfo.totalItemsCount > 0
+
+ override val isScrollInProgress
+ get() = state.isScrollInProgress
+
+ // TODO: b/3364857296 - Rework using scroll anchor item.
+ private var initialStartOffset: Float = Float.NaN
+
+ override val anchorItemOffset: Float
+ get() =
+ state.layoutInfo.visibleItems.firstOrNull()?.offset?.toFloat()?.let { newOffset ->
+ if (initialStartOffset.isNaN()) {
+ initialStartOffset = newOffset
+ }
+ initialStartOffset - newOffset
+ } ?: Float.NaN
+
+ override val lastItemOffset: Float
+ get() {
+ val layoutInfo = state.layoutInfo
+ val screenHeightPx = layoutInfo.viewportSize.height
+ return layoutInfo.visibleItems
+ .fastFirstOrNull { ii ->
+ return@fastFirstOrNull ii.index == layoutInfo.totalItemsCount - 1
+ }
+ ?.let { (screenHeightPx - it.offset - it.height).toFloat().coerceAtLeast(0f) } ?: 0f
+ }
+
+ override fun toString(): String {
+ return "LazyColumnStateScrollInfoProvider(isScrollAwayValid=$isScrollAwayValid, " +
+ "isScrollable=$isScrollable," +
+ "isScrollInProgress=$isScrollInProgress, " +
+ "anchorItemOffset=$anchorItemOffset, " +
+ "lastItemOffset=$lastItemOffset)"
+ }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.foundation.lazy
+import androidx.compose.ui.unit.IntSize
+
/**
* Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
* applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
sealed interface LazyColumnVisibleItemInfo {
/** The index of the item in the underlying data source. */
val index: Int
+
/** The offset of the item from the start of the visible area. */
val offset: Int
+
/** The height of the item after applying any height changes. */
val height: Int
+
/** The scroll progress of the item, indicating its position within the visible area. */
val scrollProgress: LazyColumnItemScrollProgress
}
/** Holds the layout information for a [LazyColumn]. */
sealed interface LazyColumnLayoutInfo {
+
/** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
val visibleItems: List<LazyColumnVisibleItemInfo>
/** The total count of items passed to [LazyColumn]. */
val totalItemsCount: Int
- // TODO: b/352686661 - Expose more properties related to layout.
+ /** The size of the viewport in pixels. */
+ val viewportSize: IntSize
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.foundation.lazy
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
/** The result of the measure pass of the [LazyColumn]. */
internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
override val visibleItems: List<LazyColumnVisibleItemInfo>,
/** see [LazyColumnLayoutInfo.totalItemsCount] */
override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+ /** see [LazyColumnLayoutInfo.viewportSize] */
+ override val viewportSize: IntSize
+ get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
index d40e83c..e1ba021 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumn.kt
@@ -399,9 +399,7 @@
* [ScalingLazyColumn] should be wrapped by [HierarchicalFocusCoordinator]. By default
* [HierarchicalFocusCoordinator] is already implemented in [BasicSwipeToDismissBox], which is a
* part of material Scaffold - meaning that rotary will be able to request a focus without any
- * additional changes. Another FocusRequester can be added through Modifier chain by adding
- * `.focusRequester(focusRequester)`. Do not call `focusable()` or `focusTarget()` after it as this
- * will reset the focusRequester chain and rotary support will not be available.
+ * additional changes.
*
* Example of a [ScalingLazyColumn] with default parameters:
*
@@ -461,7 +459,8 @@
* [flingBehavior] parameter that controls touch scroll are expected to produce similar list
* scrolling. For example, if [rotaryScrollableBehavior] is set for snap (using
* [RotaryScrollableDefaults.snapBehavior]), [flingBehavior] should be set for snap as well (using
- * [ScalingLazyColumnDefaults.snapFlingBehavior]). Can be null if rotary support is not required.
+ * [ScalingLazyColumnDefaults.snapFlingBehavior]). Can be null if rotary support is not required
+ * or when it should be handled externally - with a separate .rotary modifier.
* @param content The content of the [ScalingLazyColumn]
*/
@OptIn(ExperimentalWearFoundationApi::class)
@@ -488,7 +487,7 @@
var initialized by remember { mutableStateOf(false) }
BoxWithConstraints(
modifier =
- if (rotaryScrollableBehavior != null)
+ if (rotaryScrollableBehavior != null && userScrollEnabled)
modifier.rotaryScrollable(
behavior = rotaryScrollableBehavior,
focusRequester = rememberActiveFocusRequester(),
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
index df1b6c6..4b75357 100644
--- a/wear/compose/compose-material-core/build.gradle
+++ b/wear/compose/compose-material-core/build.gradle
@@ -44,7 +44,7 @@
implementation("androidx.compose.material:material-ripple:1.7.0")
implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-foundation"))
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 1d2fc79..864e6ad 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -43,7 +43,7 @@
implementation("androidx.compose.material:material-ripple:1.7.0")
implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-material-core"))
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
implementation("androidx.lifecycle:lifecycle-common:2.7.0")
// This :foundation dependency can be removed once the material libraries are updated to use
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
index bd54d7f..9be2bf8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
@@ -385,32 +385,6 @@
rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
rule.onNodeWithText(dismissedText).assertExists()
}
-
- @Test
- fun calls_ondismissrequest_when_dialog_becomes_hidden() {
- val show = mutableStateOf(true)
- var dismissed = false
- rule.setContentWithTheme {
- Box {
- Dialog(
- showDialog = show.value,
- onDismissRequest = { dismissed = true },
- ) {
- Alert(
- icon = {},
- title = {},
- message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
- content = {},
- )
- }
- }
- }
- rule.waitForIdle()
- show.value = false
-
- rule.waitForIdle()
- assert(dismissed)
- }
}
class DialogContentSizeAndPositionTest {
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
index 697583e..b07f490 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -233,17 +233,18 @@
transitionState.targetState = DialogVisibility.Hide
}
}
- }
- }
- LaunchedEffect(transitionState.currentState) {
- if (
- pendingOnDismissCall &&
- transitionState.currentState == DialogVisibility.Hide &&
- transitionState.isIdle
- ) {
- // After the outro animation, leave the dialog & reset alpha/scale transitions.
- onDismissRequest()
- pendingOnDismissCall = false
+
+ LaunchedEffect(transitionState.currentState) {
+ if (
+ pendingOnDismissCall &&
+ transitionState.currentState == DialogVisibility.Hide &&
+ transitionState.isIdle
+ ) {
+ // After the outro animation, leave the dialog & reset alpha/scale transitions.
+ onDismissRequest()
+ pendingOnDismissCall = false
+ }
+ }
}
}
}
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index cd6f675..e755b18 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -71,8 +71,12 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ method public float getButtonExtraLargeIconStartPadding();
method public float getButtonHorizontalPadding();
+ method public float getButtonLargeIconStartPadding();
method public float getButtonVerticalPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithExtraLargeIconContentPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithLargeIconContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getCompactButtonContentPadding();
method public float getCompactButtonHeight();
method public float getCompactButtonHorizontalPadding();
@@ -84,6 +88,7 @@
method public float getEdgeButtonHeightLarge();
method public float getEdgeButtonHeightMedium();
method public float getEdgeButtonHeightSmall();
+ method public float getExtraLargeIconSize();
method public float getHeight();
method public float getIconSize();
method public float getLargeIconSize();
@@ -93,8 +98,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(boolean enabled, optional long borderColor, optional long disabledBorderColor, optional float borderWidth);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ property public final float ButtonExtraLargeIconStartPadding;
property public final float ButtonHorizontalPadding;
+ property public final float ButtonLargeIconStartPadding;
property public final float ButtonVerticalPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithExtraLargeIconContentPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithLargeIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues CompactButtonContentPadding;
property public final float CompactButtonHeight;
property public final float CompactButtonHorizontalPadding;
@@ -105,6 +114,7 @@
property public final float EdgeButtonHeightLarge;
property public final float EdgeButtonHeightMedium;
property public final float EdgeButtonHeightSmall;
+ property public final float ExtraLargeIconSize;
property public final float Height;
property public final float IconSize;
property public final float LargeIconSize;
@@ -256,10 +266,12 @@
public final class CircularProgressIndicatorDefaults {
method public float calculateRecommendedGapSize(float strokeWidth);
method public float getFullScreenPadding();
+ method public float getIndeterminateStrokeWidth();
method @androidx.compose.runtime.Composable public float getLargeStrokeWidth();
method @androidx.compose.runtime.Composable public float getSmallStrokeWidth();
method public float getStartAngle();
property public final float FullScreenPadding;
+ property public final float IndeterminateStrokeWidth;
property public final float StartAngle;
property @androidx.compose.runtime.Composable public final float largeStrokeWidth;
property @androidx.compose.runtime.Composable public final float smallStrokeWidth;
@@ -267,7 +279,8 @@
}
public final class CircularProgressIndicatorKt {
- method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
@androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class ColorScheme {
@@ -479,8 +492,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float iconSizeFor(float size);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
property public final float DefaultButtonSize;
@@ -530,6 +541,19 @@
property public final long uncheckedContentColor;
}
+ public final class IconToggleButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape animatedToggleButtonShape(androidx.compose.foundation.interaction.InteractionSource interactionSource, boolean checked, optional androidx.compose.foundation.shape.CornerSize uncheckedCornerSize, optional androidx.compose.foundation.shape.CornerSize checkedCornerSize, optional androidx.compose.foundation.shape.CornerSize pressedCornerSize, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onPressAnimationSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onReleaseAnimationSpec);
+ method public androidx.compose.foundation.shape.CornerSize getCheckedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getPressedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getUncheckedCornerSize();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ property public final androidx.compose.foundation.shape.CornerSize CheckedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize PressedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize UncheckedCornerSize;
+ field public static final androidx.wear.compose.material3.IconToggleButtonDefaults INSTANCE;
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public final class InlineSliderColors {
ctor public InlineSliderColors(long containerColor, long buttonIconColor, long selectedBarColor, long unselectedBarColor, long barSeparatorColor, long disabledContainerColor, long disabledButtonIconColor, long disabledSelectedBarColor, long disabledUnselectedBarColor, long disabledBarSeparatorColor);
method public long getBarSeparatorColor();
@@ -795,21 +819,25 @@
}
public final class ProgressIndicatorColors {
- ctor public ProgressIndicatorColors(androidx.compose.ui.graphics.Brush indicatorBrush, androidx.compose.ui.graphics.Brush trackBrush, optional androidx.compose.ui.graphics.Brush disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush disabledTrackBrush);
+ ctor public ProgressIndicatorColors(androidx.compose.ui.graphics.Brush indicatorBrush, androidx.compose.ui.graphics.Brush trackBrush, androidx.compose.ui.graphics.Brush overflowTrackBrush, androidx.compose.ui.graphics.Brush disabledIndicatorBrush, androidx.compose.ui.graphics.Brush disabledTrackBrush, androidx.compose.ui.graphics.Brush disabledOverflowTrackBrush);
method public androidx.compose.ui.graphics.Brush getDisabledIndicatorBrush();
+ method public androidx.compose.ui.graphics.Brush getDisabledOverflowTrackBrush();
method public androidx.compose.ui.graphics.Brush getDisabledTrackBrush();
method public androidx.compose.ui.graphics.Brush getIndicatorBrush();
+ method public androidx.compose.ui.graphics.Brush getOverflowTrackBrush();
method public androidx.compose.ui.graphics.Brush getTrackBrush();
property public final androidx.compose.ui.graphics.Brush disabledIndicatorBrush;
+ property public final androidx.compose.ui.graphics.Brush disabledOverflowTrackBrush;
property public final androidx.compose.ui.graphics.Brush disabledTrackBrush;
property public final androidx.compose.ui.graphics.Brush indicatorBrush;
+ property public final androidx.compose.ui.graphics.Brush overflowTrackBrush;
property public final androidx.compose.ui.graphics.Brush trackBrush;
}
public final class ProgressIndicatorDefaults {
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional androidx.compose.ui.graphics.Brush? indicatorBrush, optional androidx.compose.ui.graphics.Brush? trackBrush, optional androidx.compose.ui.graphics.Brush? disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush? disabledTrackBrush);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long disabledIndicatorColor, optional long disabledTrackColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional androidx.compose.ui.graphics.Brush? indicatorBrush, optional androidx.compose.ui.graphics.Brush? trackBrush, optional androidx.compose.ui.graphics.Brush? overflowTrackBrush, optional androidx.compose.ui.graphics.Brush? disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush? disabledTrackBrush, optional androidx.compose.ui.graphics.Brush? disabledOverflowTrackBrush);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long overflowTrackColor, optional long disabledIndicatorColor, optional long disabledTrackColor, optional long disabledOverflowTrackColor);
field public static final androidx.wear.compose.material3.ProgressIndicatorDefaults INSTANCE;
}
@@ -886,10 +914,10 @@
}
public final class ScreenScaffoldKt {
- method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> scrollIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, float bottomButtonHeight, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional androidx.wear.compose.foundation.ScrollInfoProvider? scrollInfoProvider, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.wear.compose.foundation.lazy.LazyColumnState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
@@ -924,7 +952,7 @@
}
public final class SegmentedCircularProgressIndicatorKt {
- method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> completed, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
@@ -1248,8 +1276,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors outlinedTextButtonColors(optional long contentColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
property public final float DefaultButtonSize;
property public final float LargeButtonSize;
property public final float SmallButtonSize;
@@ -1266,17 +1292,32 @@
method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class TextConfiguration {
+ ctor public TextConfiguration(androidx.compose.ui.text.style.TextAlign? textAlign, int overflow, int maxLines);
+ method public int getMaxLines();
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int maxLines;
+ property public final int overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? textAlign;
+ }
+
+ public final class TextConfigurationDefaults {
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int Overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? TextAlign;
+ field public static final androidx.wear.compose.material3.TextConfigurationDefaults INSTANCE;
+ field public static final int MaxLines = 2147483647; // 0x7fffffff
+ }
+
public final class TextKt {
method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> getLocalTextAlign();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> getLocalTextMaxLines();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> getLocalTextOverflow();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> getLocalTextConfiguration();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> LocalTextAlign;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> LocalTextMaxLines;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> LocalTextOverflow;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> LocalTextConfiguration;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
@@ -1300,6 +1341,19 @@
property public final long uncheckedContentColor;
}
+ public final class TextToggleButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape animatedToggleButtonShape(androidx.compose.foundation.interaction.InteractionSource interactionSource, boolean checked, optional androidx.compose.foundation.shape.CornerSize uncheckedCornerSize, optional androidx.compose.foundation.shape.CornerSize checkedCornerSize, optional androidx.compose.foundation.shape.CornerSize pressedCornerSize, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onPressAnimationSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onReleaseAnimationSpec);
+ method public androidx.compose.foundation.shape.CornerSize getCheckedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getPressedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getUncheckedCornerSize();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ property public final androidx.compose.foundation.shape.CornerSize CheckedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize PressedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize UncheckedCornerSize;
+ field public static final androidx.wear.compose.material3.TextToggleButtonDefaults INSTANCE;
+ }
+
@androidx.compose.runtime.Immutable public final class TimePickerColors {
ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
method public long getConfirmButtonContainerColor();
@@ -1379,8 +1433,9 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
- method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.compose.ui.text.TextStyle getArcLarge();
method public androidx.compose.ui.text.TextStyle getArcMedium();
method public androidx.compose.ui.text.TextStyle getArcSmall();
method public androidx.compose.ui.text.TextStyle getBodyExtraSmall();
@@ -1401,6 +1456,7 @@
method public androidx.compose.ui.text.TextStyle getTitleLarge();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ property public final androidx.compose.ui.text.TextStyle arcLarge;
property public final androidx.compose.ui.text.TextStyle arcMedium;
property public final androidx.compose.ui.text.TextStyle arcSmall;
property public final androidx.compose.ui.text.TextStyle bodyExtraSmall;
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index cd6f675..e755b18 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -71,8 +71,12 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledVariantButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ method public float getButtonExtraLargeIconStartPadding();
method public float getButtonHorizontalPadding();
+ method public float getButtonLargeIconStartPadding();
method public float getButtonVerticalPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithExtraLargeIconContentPadding();
+ method public androidx.compose.foundation.layout.PaddingValues getButtonWithLargeIconContentPadding();
method public androidx.compose.foundation.layout.PaddingValues getCompactButtonContentPadding();
method public float getCompactButtonHeight();
method public float getCompactButtonHorizontalPadding();
@@ -84,6 +88,7 @@
method public float getEdgeButtonHeightLarge();
method public float getEdgeButtonHeightMedium();
method public float getEdgeButtonHeightSmall();
+ method public float getExtraLargeIconSize();
method public float getHeight();
method public float getIconSize();
method public float getLargeIconSize();
@@ -93,8 +98,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedButtonBorder(boolean enabled, optional long borderColor, optional long disabledBorderColor, optional float borderWidth);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors outlinedButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
+ property public final float ButtonExtraLargeIconStartPadding;
property public final float ButtonHorizontalPadding;
+ property public final float ButtonLargeIconStartPadding;
property public final float ButtonVerticalPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithExtraLargeIconContentPadding;
+ property public final androidx.compose.foundation.layout.PaddingValues ButtonWithLargeIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues CompactButtonContentPadding;
property public final float CompactButtonHeight;
property public final float CompactButtonHorizontalPadding;
@@ -105,6 +114,7 @@
property public final float EdgeButtonHeightLarge;
property public final float EdgeButtonHeightMedium;
property public final float EdgeButtonHeightSmall;
+ property public final float ExtraLargeIconSize;
property public final float Height;
property public final float IconSize;
property public final float LargeIconSize;
@@ -256,10 +266,12 @@
public final class CircularProgressIndicatorDefaults {
method public float calculateRecommendedGapSize(float strokeWidth);
method public float getFullScreenPadding();
+ method public float getIndeterminateStrokeWidth();
method @androidx.compose.runtime.Composable public float getLargeStrokeWidth();
method @androidx.compose.runtime.Composable public float getSmallStrokeWidth();
method public float getStartAngle();
property public final float FullScreenPadding;
+ property public final float IndeterminateStrokeWidth;
property public final float StartAngle;
property @androidx.compose.runtime.Composable public final float largeStrokeWidth;
property @androidx.compose.runtime.Composable public final float smallStrokeWidth;
@@ -267,7 +279,8 @@
}
public final class CircularProgressIndicatorKt {
- method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize);
+ method @androidx.compose.runtime.Composable public static void CircularProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
@androidx.compose.runtime.Immutable @androidx.compose.runtime.Stable public final class ColorScheme {
@@ -479,8 +492,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public float iconSizeFor(float size);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconButtonColors outlinedIconButtonColors(optional long contentColor, optional long disabledContentColor);
property public final float DefaultButtonSize;
@@ -530,6 +541,19 @@
property public final long uncheckedContentColor;
}
+ public final class IconToggleButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape animatedToggleButtonShape(androidx.compose.foundation.interaction.InteractionSource interactionSource, boolean checked, optional androidx.compose.foundation.shape.CornerSize uncheckedCornerSize, optional androidx.compose.foundation.shape.CornerSize checkedCornerSize, optional androidx.compose.foundation.shape.CornerSize pressedCornerSize, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onPressAnimationSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onReleaseAnimationSpec);
+ method public androidx.compose.foundation.shape.CornerSize getCheckedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getPressedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getUncheckedCornerSize();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ property public final androidx.compose.foundation.shape.CornerSize CheckedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize PressedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize UncheckedCornerSize;
+ field public static final androidx.wear.compose.material3.IconToggleButtonDefaults INSTANCE;
+ }
+
@SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public final class InlineSliderColors {
ctor public InlineSliderColors(long containerColor, long buttonIconColor, long selectedBarColor, long unselectedBarColor, long barSeparatorColor, long disabledContainerColor, long disabledButtonIconColor, long disabledSelectedBarColor, long disabledUnselectedBarColor, long disabledBarSeparatorColor);
method public long getBarSeparatorColor();
@@ -795,21 +819,25 @@
}
public final class ProgressIndicatorColors {
- ctor public ProgressIndicatorColors(androidx.compose.ui.graphics.Brush indicatorBrush, androidx.compose.ui.graphics.Brush trackBrush, optional androidx.compose.ui.graphics.Brush disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush disabledTrackBrush);
+ ctor public ProgressIndicatorColors(androidx.compose.ui.graphics.Brush indicatorBrush, androidx.compose.ui.graphics.Brush trackBrush, androidx.compose.ui.graphics.Brush overflowTrackBrush, androidx.compose.ui.graphics.Brush disabledIndicatorBrush, androidx.compose.ui.graphics.Brush disabledTrackBrush, androidx.compose.ui.graphics.Brush disabledOverflowTrackBrush);
method public androidx.compose.ui.graphics.Brush getDisabledIndicatorBrush();
+ method public androidx.compose.ui.graphics.Brush getDisabledOverflowTrackBrush();
method public androidx.compose.ui.graphics.Brush getDisabledTrackBrush();
method public androidx.compose.ui.graphics.Brush getIndicatorBrush();
+ method public androidx.compose.ui.graphics.Brush getOverflowTrackBrush();
method public androidx.compose.ui.graphics.Brush getTrackBrush();
property public final androidx.compose.ui.graphics.Brush disabledIndicatorBrush;
+ property public final androidx.compose.ui.graphics.Brush disabledOverflowTrackBrush;
property public final androidx.compose.ui.graphics.Brush disabledTrackBrush;
property public final androidx.compose.ui.graphics.Brush indicatorBrush;
+ property public final androidx.compose.ui.graphics.Brush overflowTrackBrush;
property public final androidx.compose.ui.graphics.Brush trackBrush;
}
public final class ProgressIndicatorDefaults {
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional androidx.compose.ui.graphics.Brush? indicatorBrush, optional androidx.compose.ui.graphics.Brush? trackBrush, optional androidx.compose.ui.graphics.Brush? disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush? disabledTrackBrush);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long disabledIndicatorColor, optional long disabledTrackColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional androidx.compose.ui.graphics.Brush? indicatorBrush, optional androidx.compose.ui.graphics.Brush? trackBrush, optional androidx.compose.ui.graphics.Brush? overflowTrackBrush, optional androidx.compose.ui.graphics.Brush? disabledIndicatorBrush, optional androidx.compose.ui.graphics.Brush? disabledTrackBrush, optional androidx.compose.ui.graphics.Brush? disabledOverflowTrackBrush);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ProgressIndicatorColors colors(optional long indicatorColor, optional long trackColor, optional long overflowTrackColor, optional long disabledIndicatorColor, optional long disabledTrackColor, optional long disabledOverflowTrackColor);
field public static final androidx.wear.compose.material3.ProgressIndicatorDefaults INSTANCE;
}
@@ -886,10 +914,10 @@
}
public final class ScreenScaffoldKt {
- method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> scrollIndicator, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, float bottomButtonHeight, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional androidx.wear.compose.foundation.ScrollInfoProvider? scrollInfoProvider, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.wear.compose.foundation.lazy.LazyColumnState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ScreenScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> bottomButton, androidx.wear.compose.foundation.ScrollInfoProvider scrollInfoProvider, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
@@ -924,7 +952,7 @@
}
public final class SegmentedCircularProgressIndicatorKt {
- method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
+ method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional boolean allowProgressOverflow, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
method @androidx.compose.runtime.Composable public static void SegmentedCircularProgressIndicator(@IntRange(from=1L) int segmentCount, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> completed, optional androidx.compose.ui.Modifier modifier, optional float startAngle, optional float endAngle, optional androidx.wear.compose.material3.ProgressIndicatorColors colors, optional float strokeWidth, optional float gapSize, optional boolean enabled);
}
@@ -1248,8 +1276,6 @@
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors outlinedTextButtonColors(optional long contentColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors();
method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
property public final float DefaultButtonSize;
property public final float LargeButtonSize;
property public final float SmallButtonSize;
@@ -1266,17 +1292,32 @@
method @androidx.compose.runtime.Composable public static void TextToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.wear.compose.material3.TextToggleButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
+ public final class TextConfiguration {
+ ctor public TextConfiguration(androidx.compose.ui.text.style.TextAlign? textAlign, int overflow, int maxLines);
+ method public int getMaxLines();
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int maxLines;
+ property public final int overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? textAlign;
+ }
+
+ public final class TextConfigurationDefaults {
+ method public int getOverflow();
+ method public androidx.compose.ui.text.style.TextAlign? getTextAlign();
+ property public final int Overflow;
+ property public final androidx.compose.ui.text.style.TextAlign? TextAlign;
+ field public static final androidx.wear.compose.material3.TextConfigurationDefaults INSTANCE;
+ field public static final int MaxLines = 2147483647; // 0x7fffffff
+ }
+
public final class TextKt {
method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> getLocalTextAlign();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> getLocalTextMaxLines();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> getLocalTextOverflow();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> getLocalTextConfiguration();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextAlign?> LocalTextAlign;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Integer> LocalTextMaxLines;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.style.TextOverflow> LocalTextOverflow;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.material3.TextConfiguration> LocalTextConfiguration;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
}
@@ -1300,6 +1341,19 @@
property public final long uncheckedContentColor;
}
+ public final class TextToggleButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape animatedToggleButtonShape(androidx.compose.foundation.interaction.InteractionSource interactionSource, boolean checked, optional androidx.compose.foundation.shape.CornerSize uncheckedCornerSize, optional androidx.compose.foundation.shape.CornerSize checkedCornerSize, optional androidx.compose.foundation.shape.CornerSize pressedCornerSize, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onPressAnimationSpec, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> onReleaseAnimationSpec);
+ method public androidx.compose.foundation.shape.CornerSize getCheckedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getPressedCornerSize();
+ method public androidx.compose.foundation.shape.CornerSize getUncheckedCornerSize();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.TextToggleButtonColors textToggleButtonColors(optional long checkedContainerColor, optional long checkedContentColor, optional long uncheckedContainerColor, optional long uncheckedContentColor, optional long disabledCheckedContainerColor, optional long disabledCheckedContentColor, optional long disabledUncheckedContainerColor, optional long disabledUncheckedContentColor);
+ property public final androidx.compose.foundation.shape.CornerSize CheckedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize PressedCornerSize;
+ property public final androidx.compose.foundation.shape.CornerSize UncheckedCornerSize;
+ field public static final androidx.wear.compose.material3.TextToggleButtonDefaults INSTANCE;
+ }
+
@androidx.compose.runtime.Immutable public final class TimePickerColors {
ctor public TimePickerColors(long selectedPickerContentColor, long unselectedPickerContentColor, long separatorColor, long pickerLabelColor, long confirmButtonContentColor, long confirmButtonContainerColor);
method public long getConfirmButtonContainerColor();
@@ -1379,8 +1433,9 @@
}
@androidx.compose.runtime.Immutable public final class Typography {
- ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
- method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ ctor public Typography(optional androidx.compose.ui.text.font.FontFamily defaultFontFamily, optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.wear.compose.material3.Typography copy(optional androidx.compose.ui.text.TextStyle arcLarge, optional androidx.compose.ui.text.TextStyle arcMedium, optional androidx.compose.ui.text.TextStyle arcSmall, optional androidx.compose.ui.text.TextStyle displayLarge, optional androidx.compose.ui.text.TextStyle displayMedium, optional androidx.compose.ui.text.TextStyle displaySmall, optional androidx.compose.ui.text.TextStyle titleLarge, optional androidx.compose.ui.text.TextStyle titleMedium, optional androidx.compose.ui.text.TextStyle titleSmall, optional androidx.compose.ui.text.TextStyle labelLarge, optional androidx.compose.ui.text.TextStyle labelMedium, optional androidx.compose.ui.text.TextStyle labelSmall, optional androidx.compose.ui.text.TextStyle bodyLarge, optional androidx.compose.ui.text.TextStyle bodyMedium, optional androidx.compose.ui.text.TextStyle bodySmall, optional androidx.compose.ui.text.TextStyle bodyExtraSmall, optional androidx.compose.ui.text.TextStyle numeralExtraLarge, optional androidx.compose.ui.text.TextStyle numeralLarge, optional androidx.compose.ui.text.TextStyle numeralMedium, optional androidx.compose.ui.text.TextStyle numeralSmall, optional androidx.compose.ui.text.TextStyle numeralExtraSmall);
+ method public androidx.compose.ui.text.TextStyle getArcLarge();
method public androidx.compose.ui.text.TextStyle getArcMedium();
method public androidx.compose.ui.text.TextStyle getArcSmall();
method public androidx.compose.ui.text.TextStyle getBodyExtraSmall();
@@ -1401,6 +1456,7 @@
method public androidx.compose.ui.text.TextStyle getTitleLarge();
method public androidx.compose.ui.text.TextStyle getTitleMedium();
method public androidx.compose.ui.text.TextStyle getTitleSmall();
+ property public final androidx.compose.ui.text.TextStyle arcLarge;
property public final androidx.compose.ui.text.TextStyle arcMedium;
property public final androidx.compose.ui.text.TextStyle arcSmall;
property public final androidx.compose.ui.text.TextStyle bodyExtraSmall;
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+ @Test
+ fun first_pixel() {
+ benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+ }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ CircularProgressIndicator(progress = { 0.5f })
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+}
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
index 43171ec..fb86c7e 100644
--- a/wear/compose/compose-material3/build.gradle
+++ b/wear/compose/compose-material3/build.gradle
@@ -45,8 +45,8 @@
implementation("androidx.compose.material:material-ripple:1.7.0")
implementation("androidx.compose.ui:ui-util:1.7.0")
implementation(project(":wear:compose:compose-material-core"))
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
- implementation("androidx.graphics:graphics-shapes:1.0.0-beta01")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
+ implementation("androidx.graphics:graphics-shapes:1.0.1")
implementation project(':compose:animation:animation-graphics')
androidTestImplementation(project(":compose:ui:ui-test"))
diff --git a/wear/compose/compose-material3/integration-tests/lint-baseline.xml b/wear/compose/compose-material3/integration-tests/lint-baseline.xml
new file mode 100644
index 0000000..785d646
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/lint-baseline.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
+
+ <issue
+ id="AutoboxingStateCreation"
+ message="Prefer `mutableFloatStateOf` instead of `mutableStateOf`"
+ errorLine1=" var topLetterSpacing by remember { mutableStateOf(0.6f) }"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateCreation"
+ message="Prefer `mutableFloatStateOf` instead of `mutableStateOf`"
+ errorLine1=" var bottomLetterSpacing by remember { mutableStateOf(2.0f) }"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" progress = { progress.value },"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" startAngle = startAngle.value,"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" endAngle = endAngle.value,"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `intValue` to avoid unnecessary allocations."
+ errorLine1=" segmentCount = numSegments.value,"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" progress = { progress.value },"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" startAngle = startAngle.value,"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+ <issue
+ id="AutoboxingStateValueProperty"
+ message="Assigning `value` will cause an autoboxing operation. Use `floatValue` to avoid unnecessary allocations."
+ errorLine1=" endAngle = endAngle.value,"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt"/>
+ </issue>
+
+</issues>
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
index 740b637..e466b27 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
@@ -48,7 +48,8 @@
val interactionSource1 = remember { MutableInteractionSource() }
TextButton(
onClick = {},
- shape = TextButtonDefaults.animatedShape(interactionSource1)
+ shape = TextButtonDefaults.animatedShape(interactionSource1),
+ interactionSource = interactionSource1,
) {
Text(text = "ABC")
}
@@ -62,8 +63,9 @@
TextButtonDefaults.animatedShape(
interactionSource2,
shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp)
- )
+ pressedShape = RoundedCornerShape(15.dp),
+ ),
+ interactionSource = interactionSource2,
) {
Text(text = "ABC")
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
new file mode 100644
index 0000000..771a8af
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Home
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.IconToggleButton
+import androidx.wear.compose.material3.IconToggleButtonDefaults
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextButtonDefaults
+import androidx.wear.compose.material3.TextToggleButton
+import androidx.wear.compose.material3.TextToggleButtonDefaults
+
+@Composable
+fun AnimatedShapeToggleButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Default Toggle") } }
+ item {
+ Row {
+ val checked = remember { mutableStateOf(false) }
+
+ val interactionSource1 = remember { MutableInteractionSource() }
+
+ TextToggleButton(
+ onCheckedChange = { checked.value = !checked.value },
+ shape =
+ TextButtonDefaults.animatedShape(
+ interactionSource1,
+ ),
+ checked = checked.value,
+ interactionSource = interactionSource1,
+ ) {
+ Text(text = "ABC")
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ IconToggleButton(
+ onCheckedChange = { checked.value = !checked.value },
+ shape =
+ IconButtonDefaults.animatedShape(
+ interactionSource1,
+ ),
+ checked = checked.value,
+ interactionSource = interactionSource1,
+ ) {
+ Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
+ }
+ }
+ }
+ item { ListHeader { Text("Toggle Variant") } }
+ item {
+ Row {
+ val checked = remember { mutableStateOf(false) }
+
+ val interactionSource1 = remember { MutableInteractionSource() }
+ TextToggleButton(
+ onCheckedChange = { checked.value = !checked.value },
+ shape =
+ TextToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource1,
+ checked = checked.value,
+ ),
+ checked = checked.value,
+ interactionSource = interactionSource1,
+ ) {
+ Text(text = "ABC")
+ }
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ IconToggleButton(
+ onCheckedChange = { checked.value = !checked.value },
+ shape =
+ IconToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource1,
+ checked = checked.value,
+ ),
+ checked = checked.value,
+ interactionSource = interactionSource1,
+ ) {
+ Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
+ }
+ }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index 1f5758e..e5e8942 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -19,11 +19,9 @@
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
@@ -36,32 +34,67 @@
import androidx.wear.compose.material3.ChildButton
import androidx.wear.compose.material3.CompactButton
import androidx.wear.compose.material3.FilledTonalButton
-import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListSubheader
import androidx.wear.compose.material3.OutlinedButton
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.samples.ButtonExtraLargeIconSample
+import androidx.wear.compose.material3.samples.ButtonLargeIconSample
import androidx.wear.compose.material3.samples.ButtonSample
-import androidx.wear.compose.material3.samples.ButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.ChildButtonSample
-import androidx.wear.compose.material3.samples.ChildButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.CompactButtonSample
import androidx.wear.compose.material3.samples.CompactButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.FilledTonalButtonSample
-import androidx.wear.compose.material3.samples.FilledTonalButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.FilledVariantButtonSample
import androidx.wear.compose.material3.samples.OutlinedButtonSample
-import androidx.wear.compose.material3.samples.OutlinedButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.OutlinedCompactButtonSample
import androidx.wear.compose.material3.samples.SimpleChildButtonSample
import androidx.wear.compose.material3.samples.SimpleFilledTonalButtonSample
import androidx.wear.compose.material3.samples.SimpleFilledVariantButtonSample
import androidx.wear.compose.material3.samples.SimpleOutlinedButtonSample
+import androidx.wear.compose.material3.samples.icons.FavoriteIcon
+
+@Composable
+fun BaseButtonDemo() {
+ // This demo shows how to use the Base Button overload, which has a single content slot
+ // that can be used with a trailing lambda. It should vertically center content by default,
+ // but that can easily be changed by using Modifier.align from RowScope in whatever is passed
+ // to the content slot.
+ ScalingLazyDemo {
+ item { ListHeader { Text("Base Button") } }
+ item { ListSubheader { Text("Default alignment") } }
+ item { Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Base Button") } }
+ item { ListSubheader { Text("Top Alignment") } }
+ item {
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
+ Text("Base Button", modifier = Modifier.align(Alignment.Top))
+ }
+ }
+ }
+}
@Composable
fun ButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Button") },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Button") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Centered Button") } }
item {
Button(
onClick = {},
@@ -89,30 +122,65 @@
modifier = Modifier.fillMaxWidth()
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { ButtonSample(modifier = Modifier.fillMaxWidth()) }
item {
Button(
onClick = { /* Do something */ },
label = { Text("Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
item { ListHeader { Text("Long Click") } }
item {
- ButtonWithOnLongClickSample(
+ Button(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Button") },
+ secondaryLabel = { Text("with long click") },
modifier = Modifier.fillMaxWidth(),
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) },
)
}
}
@@ -122,15 +190,9 @@
fun FilledTonalButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleFilledTonalButtonSample() }
item {
- FilledTonalButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) }
- )
- }
- item {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
@@ -138,31 +200,73 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ FilledTonalButton(
+ onClick = { /* Do something */ },
+ label = { Text("Filled Tonal Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = false
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { FilledTonalButtonSample() }
item {
FilledTonalButton(
onClick = { /* Do something */ },
label = { Text("Filled Tonal Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth(),
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ FilledTonalButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Filled Tonal Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@Composable
fun FilledVariantButtonDemo() {
+ val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleFilledVariantButtonSample() }
item {
Button(
@@ -173,7 +277,47 @@
modifier = Modifier.fillMaxWidth()
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ Button(
+ onClick = { /* Do something */ },
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled Variant Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { FilledVariantButtonSample() }
item {
Button(
@@ -181,15 +325,21 @@
colors = ButtonDefaults.filledVariantButtonColors(),
label = { Text("Filled Variant Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Long Click") } }
+ item {
+ Button(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ colors = ButtonDefaults.filledVariantButtonColors(),
+ label = { Text("Filled VariantButton") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -199,15 +349,9 @@
fun OutlinedButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleOutlinedButtonSample() }
item {
- OutlinedButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) }
- )
- }
- item {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
@@ -215,24 +359,65 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ OutlinedButton(
+ onClick = { /* Do something */ },
+ label = { Text("Outlined Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button)") } }
item { OutlinedButtonSample() }
item {
OutlinedButton(
onClick = { /* Do something */ },
label = { Text("Outlined Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ OutlinedButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Outlined Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@@ -240,15 +425,9 @@
fun ChildButtonDemo() {
val context = LocalContext.current
ScalingLazyDemo {
- item { ListHeader { Text("1 slot button") } }
+ item { ListHeader { Text("1 Slot Button") } }
item { SimpleChildButtonSample() }
item {
- ChildButtonWithOnLongClickSample(
- onClickHandler = { showOnClickToast(context) },
- onLongClickHandler = { showOnLongClickToast(context) },
- )
- }
- item {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
@@ -256,24 +435,65 @@
modifier = Modifier.fillMaxWidth(),
)
}
- item { ListHeader { Text("3 slot button") } }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("Secondary label") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("Secondary label") },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("Icon and Label") } }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item {
+ ChildButton(
+ onClick = { /* Do something */ },
+ label = { Text("Child Button") },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
+ enabled = false,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ item { ListHeader { Text("3 Slot Button") } }
item { ChildButtonSample() }
item {
ChildButton(
onClick = { /* Do something */ },
label = { Text("Child Button") },
secondaryLabel = { Text("Secondary label") },
- icon = {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Favorite icon",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- },
+ icon = { FavoriteIcon(ButtonDefaults.IconSize) },
enabled = false,
modifier = Modifier.fillMaxWidth()
)
}
+ item { ListHeader { Text("Long Click") } }
+ item {
+ ChildButton(
+ onClick = { showOnClickToast(context) },
+ onLongClick = { showOnLongClickToast(context) },
+ onLongClickLabel = "Long click",
+ label = { Text("Child Button") },
+ secondaryLabel = { Text("with long click") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@@ -288,7 +508,7 @@
colors = ButtonDefaults.buttonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Compact Button", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Compact Button", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -297,7 +517,7 @@
colors = ButtonDefaults.filledVariantButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Filled Variant", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Filled Variant", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -306,7 +526,7 @@
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
- Text("Filled Tonal", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Filled Tonal", modifier = Modifier.fillMaxWidth())
}
}
item {
@@ -316,7 +536,7 @@
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
modifier = Modifier.fillMaxWidth()
) {
- Text("Outlined", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("Outlined", modifier = Modifier.fillMaxWidth())
}
}
item { ListHeader { Text("Icon and Label") } }
@@ -324,7 +544,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledVariantButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -334,7 +554,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledTonalButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -344,7 +564,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
modifier = Modifier.fillMaxWidth()
@@ -355,7 +575,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.childButtonColors(),
modifier = Modifier.fillMaxWidth()
) {
@@ -366,20 +586,20 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
)
}
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.filledTonalButtonColors(),
)
}
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.outlinedButtonColors(),
border = ButtonDefaults.outlinedButtonBorder(enabled = true),
)
@@ -387,7 +607,7 @@
item {
CompactButton(
onClick = { /* Do something */ },
- icon = { StandardIcon(ButtonDefaults.SmallIconSize) },
+ icon = { FavoriteIcon(ButtonDefaults.SmallIconSize) },
colors = ButtonDefaults.childButtonColors(),
)
}
@@ -409,59 +629,76 @@
item { ListHeader { Text("3 line label") } }
item { MultilineButton(enabled = true) }
item { MultilineButton(enabled = false) }
- item { MultilineButton(enabled = true, icon = { StandardIcon(ButtonDefaults.IconSize) }) }
- item { MultilineButton(enabled = false, icon = { StandardIcon(ButtonDefaults.IconSize) }) }
+ item { MultilineButton(enabled = true, icon = { FavoriteIcon(ButtonDefaults.IconSize) }) }
+ item { MultilineButton(enabled = false, icon = { FavoriteIcon(ButtonDefaults.IconSize) }) }
item { ListHeader { Text("5 line button") } }
item { Multiline3SlotButton(enabled = true) }
item { Multiline3SlotButton(enabled = false) }
item {
- Multiline3SlotButton(enabled = true, icon = { StandardIcon(ButtonDefaults.IconSize) })
+ Multiline3SlotButton(enabled = true, icon = { FavoriteIcon(ButtonDefaults.IconSize) })
}
item {
- Multiline3SlotButton(enabled = false, icon = { StandardIcon(ButtonDefaults.IconSize) })
+ Multiline3SlotButton(enabled = false, icon = { FavoriteIcon(ButtonDefaults.IconSize) })
}
}
}
@Composable
-fun AvatarButtonDemo() {
- ScalingLazyDemo {
- item { ListHeader { Text("Label + Avatar") } }
- item { AvatarButton(enabled = true) }
- item { AvatarButton(enabled = false) }
- item { ListHeader { Text("Primary/Secondary + Avatar") } }
- item { Avatar3SlotButton(enabled = true) }
- item { Avatar3SlotButton(enabled = false) }
- }
-}
-
-@Composable
fun ButtonBackgroundImageDemo() {
ScalingLazyDemo {
item { ListHeader { Text("Button (Image Background)") } }
item { ButtonBackgroundImage(painterResource(R.drawable.card_background), enabled = true) }
item { ButtonBackgroundImage(painterResource(R.drawable.card_background), enabled = false) }
+ item { ListHeader { Text("2 Slot Button") } }
+ item {
+ Button(
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
+ onClick = { /* Do something */ },
+ label = { Text("Label", maxLines = 1) },
+ secondaryLabel = { Text("Secondary label", maxLines = 1) },
+ colors =
+ ButtonDefaults.imageBackgroundButtonColors(
+ painterResource(R.drawable.card_background)
+ )
+ )
+ }
+ item {
+ Button(
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
+ onClick = { /* Do something */ },
+ label = { Text("Label", maxLines = 1) },
+ secondaryLabel = { Text("Secondary label", maxLines = 1) },
+ enabled = false,
+ colors =
+ ButtonDefaults.imageBackgroundButtonColors(
+ painterResource(R.drawable.card_background)
+ )
+ )
+ }
}
}
@Composable
-private fun AvatarButton(enabled: Boolean) =
- MultilineButton(
- enabled = enabled,
- colors = ButtonDefaults.filledTonalButtonColors(),
- icon = { AvatarIcon() },
- label = { Text("Primary text") }
- )
+fun AppButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Large Icon") } }
+ item { ButtonLargeIcon(enabled = true) }
+ item { ButtonLargeIcon(enabled = false) }
+ item { ButtonLargeIconSample(enabled = true) }
+ item { ButtonLargeIconSample(enabled = false) }
+ }
+}
@Composable
-private fun Avatar3SlotButton(enabled: Boolean) =
- Multiline3SlotButton(
- enabled = enabled,
- colors = ButtonDefaults.filledTonalButtonColors(),
- icon = { AvatarIcon() },
- label = { Text("Primary text") },
- secondaryLabel = { Text("Secondary label") }
- )
+fun AvatarButtonDemo() {
+ ScalingLazyDemo {
+ item { ListHeader { Text("Extra Large Icon") } }
+ item { ButtonExtraLargeIcon(enabled = true) }
+ item { ButtonExtraLargeIcon(enabled = false) }
+ item { ButtonExtraLargeIconSample(enabled = true) }
+ item { ButtonExtraLargeIconSample(enabled = false) }
+ }
+}
@Composable
private fun MultilineButton(
@@ -522,9 +759,33 @@
@Composable
private fun ButtonBackgroundImage(painter: Painter, enabled: Boolean) =
Button(
- modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height),
+ modifier = Modifier.sizeIn(maxHeight = ButtonDefaults.Height).fillMaxWidth(),
onClick = { /* Do something */ },
label = { Text("Label", maxLines = 1) },
enabled = enabled,
colors = ButtonDefaults.imageBackgroundButtonColors(painter)
)
+
+@Composable
+private fun ButtonLargeIcon(enabled: Boolean = true) {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.LargeIconSize) },
+ enabled = enabled,
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding
+ )
+}
+
+@Composable
+private fun ButtonExtraLargeIcon(enabled: Boolean = true) {
+ Button(
+ onClick = { /* Do something */ },
+ label = { Text("Button") },
+ icon = { FavoriteIcon(ButtonDefaults.ExtraLargeIconSize) },
+ enabled = enabled,
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding
+ )
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
index 784d022..1c98c7f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CheckboxButtonDemo.kt
@@ -29,7 +29,7 @@
import androidx.wear.compose.material3.CheckboxButton
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.Text
@Composable
@@ -113,7 +113,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel = {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index 14317c3..d93f73d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.material3.demos
-import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -29,8 +28,6 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -41,13 +38,9 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
-import androidx.wear.compose.foundation.rotary.rotaryScrollable
import androidx.wear.compose.integration.demos.common.AdaptiveScreen
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Card
@@ -57,6 +50,7 @@
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TextButton
import androidx.wear.compose.material3.TextButtonDefaults
+import androidx.wear.compose.material3.samples.icons.CheckIcon
@Composable
fun EdgeButtonBelowLazyColumnDemo() {
@@ -153,66 +147,6 @@
}
}
-@OptIn(ExperimentalWearFoundationApi::class)
-@Composable
-fun EdgeButtonBelowColumnDemo() {
- val labels =
- listOf(
- "Hi",
- "Hello World",
- "Hello world again?",
- "More content as we add stuff",
- "I don't know if this will fit now, testing",
- "Really long text that it's going to take multiple lines",
- "And now we are really pushing it because the screen is really small",
- )
- val selectedLabel = remember { mutableIntStateOf(0) }
- val bottomButtonHeight = ButtonDefaults.EdgeButtonHeightLarge
-
- AdaptiveScreen {
- val scrollState = rememberScrollState()
- val focusRequester = rememberActiveFocusRequester()
-
- ScreenScaffold(
- scrollState = scrollState,
- bottomButton = {
- EdgeButton(
- onClick = {},
- buttonHeight = bottomButtonHeight,
- colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
- ) {
- Text(labels[selectedLabel.intValue], color = Color.White)
- }
- },
- bottomButtonHeight = bottomButtonHeight
- ) {
- Column(
- modifier =
- Modifier.verticalScroll(scrollState)
- .rotaryScrollable(
- RotaryScrollableDefaults.behavior(
- scrollableState = scrollState,
- flingBehavior = ScrollableDefaults.flingBehavior()
- ),
- focusRequester = focusRequester
- ),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- repeat(labels.size) {
- Card(
- onClick = { selectedLabel.intValue = it },
- modifier = Modifier.fillMaxWidth(0.9f)
- ) {
- Text(labels[it])
- }
- }
- Spacer(Modifier.height(bottomButtonHeight))
- }
- }
- }
-}
-
@Suppress("PrimitiveInCollection")
@Composable
fun EdgeButtonMultiDemo() {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index 0d75c07..a28a8f0 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -46,6 +46,7 @@
import androidx.wear.compose.material3.samples.IconButtonWithImageSample
import androidx.wear.compose.material3.samples.IconButtonWithOnLongClickSample
import androidx.wear.compose.material3.samples.OutlinedIconButtonSample
+import androidx.wear.compose.material3.samples.icons.FavoriteIcon
import androidx.wear.compose.material3.touchTargetAwareSize
@Composable
@@ -57,7 +58,7 @@
Row {
IconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
- IconButton(onClick = {}, enabled = false) { StandardIcon(ButtonDefaults.IconSize) }
+ IconButton(onClick = {}, enabled = false) { FavoriteIcon(ButtonDefaults.IconSize) }
}
}
item { ListHeader { Text("Filled Tonal") } }
@@ -66,7 +67,7 @@
FilledTonalIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
FilledTonalIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -76,7 +77,7 @@
FilledIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
FilledIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -90,7 +91,7 @@
enabled = false,
colors = IconButtonDefaults.filledVariantIconButtonColors()
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -100,7 +101,7 @@
OutlinedIconButtonSample()
Spacer(modifier = Modifier.width(5.dp))
OutlinedIconButton(onClick = {}, enabled = false) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -128,7 +129,7 @@
),
interactionSource = interactionSource1
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
Spacer(modifier = Modifier.width(5.dp))
val interactionSource2 = remember { MutableInteractionSource() }
@@ -143,7 +144,7 @@
),
interactionSource = interactionSource2
) {
- StandardIcon(ButtonDefaults.IconSize)
+ FavoriteIcon(ButtonDefaults.IconSize)
}
}
}
@@ -222,6 +223,6 @@
modifier = Modifier.touchTargetAwareSize(size),
onClick = { /* Do something */ }
) {
- StandardIcon(IconButtonDefaults.iconSizeFor(size))
+ FavoriteIcon(IconButtonDefaults.iconSizeFor(size))
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
index 73f8f8e..8500205 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ProgressIndicatorDemo.kt
@@ -21,9 +21,15 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,17 +40,25 @@
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.CircularProgressIndicator
import androidx.wear.compose.material3.CircularProgressIndicatorDefaults
+import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.IconButtonDefaults
+import androidx.wear.compose.material3.InlineSlider
+import androidx.wear.compose.material3.InlineSliderDefaults
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.ProgressIndicatorDefaults
+import androidx.wear.compose.material3.SegmentedCircularProgressIndicator
+import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.samples.FullScreenProgressIndicatorSample
+import androidx.wear.compose.material3.samples.IndeterminateProgressIndicatorSample
import androidx.wear.compose.material3.samples.LinearProgressIndicatorSample
import androidx.wear.compose.material3.samples.MediaButtonProgressIndicatorSample
import androidx.wear.compose.material3.samples.OverflowProgressIndicatorSample
import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
import androidx.wear.compose.material3.samples.SegmentedProgressIndicatorSample
+import androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
import androidx.wear.compose.material3.samples.SmallValuesProgressIndicatorSample
val ProgressIndicatorDemos =
@@ -77,10 +91,22 @@
ComposableDemo("Small progress values") {
Centralize { SmallValuesProgressIndicatorSample() }
},
+ ComposableDemo("Indeterminate progress") {
+ Centralize { IndeterminateProgressIndicatorSample() }
+ },
ComposableDemo("Segmented progress") { Centralize { SegmentedProgressIndicatorSample() } },
ComposableDemo("Progress segments on/off") {
Centralize { SegmentedProgressIndicatorOnOffSample() }
},
+ ComposableDemo("Small segmented progress") {
+ Centralize { SmallSegmentedProgressIndicatorSample() }
+ },
+ ComposableDemo("Custom circular progress") {
+ Centralize { CircularProgressCustomisableFullScreenDemo() }
+ },
+ ComposableDemo("Custom segmented progress") {
+ Centralize { SegmentedProgressCustomisableFullScreenDemo() }
+ },
ComposableDemo("Linear progress indicator") {
Centralize { LinearProgressIndicatorSamples() }
},
@@ -111,3 +137,225 @@
}
}
}
+
+@Composable
+fun CircularProgressCustomisableFullScreenDemo() {
+ val progress = remember { mutableFloatStateOf(0.4f) }
+ val startAngle = remember { mutableFloatStateOf(360f) }
+ val endAngle = remember { mutableFloatStateOf(360f) }
+ val enabled = remember { mutableStateOf(true) }
+ val overflowAllowed = remember { mutableStateOf(true) }
+ val hasLargeStroke = remember { mutableStateOf(true) }
+ val hasCustomColors = remember { mutableStateOf(false) }
+ val colors =
+ if (hasCustomColors.value) {
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = Color.Green,
+ trackColor = Color.Green.copy(alpha = 0.5f),
+ overflowTrackColor = Color.Green.copy(alpha = 0.7f),
+ )
+ } else {
+ ProgressIndicatorDefaults.colors()
+ }
+ val strokeWidth =
+ if (hasLargeStroke.value) CircularProgressIndicatorDefaults.largeStrokeWidth
+ else CircularProgressIndicatorDefaults.smallStrokeWidth
+
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ ProgressIndicatorCustomizer(
+ progress = progress,
+ startAngle = startAngle,
+ endAngle = endAngle,
+ enabled = enabled,
+ overflowAllowed = overflowAllowed,
+ hasLargeStroke = hasLargeStroke,
+ hasCustomColors = hasCustomColors,
+ )
+
+ CircularProgressIndicator(
+ progress = { progress.value },
+ startAngle = startAngle.value,
+ endAngle = endAngle.value,
+ enabled = enabled.value,
+ allowProgressOverflow = overflowAllowed.value,
+ strokeWidth = strokeWidth,
+ colors = colors,
+ )
+ }
+}
+
+@Composable
+fun SegmentedProgressCustomisableFullScreenDemo() {
+ val progress = remember { mutableFloatStateOf(0f) }
+ val startAngle = remember { mutableFloatStateOf(0f) }
+ val endAngle = remember { mutableFloatStateOf(0f) }
+ val enabled = remember { mutableStateOf(true) }
+ val overflowAllowed = remember { mutableStateOf(true) }
+ val hasCustomColors = remember { mutableStateOf(false) }
+ val hasLargeStroke = remember { mutableStateOf(true) }
+ val numSegments = remember { mutableIntStateOf(5) }
+ val colors =
+ if (hasCustomColors.value) {
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = Color.Green,
+ trackColor = Color.Green.copy(alpha = 0.5f),
+ overflowTrackColor = Color.Green.copy(alpha = 0.7f),
+ )
+ } else {
+ ProgressIndicatorDefaults.colors()
+ }
+ val strokeWidth =
+ if (hasLargeStroke.value) CircularProgressIndicatorDefaults.largeStrokeWidth
+ else CircularProgressIndicatorDefaults.smallStrokeWidth
+
+ Box(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(CircularProgressIndicatorDefaults.FullScreenPadding)
+ .fillMaxSize()
+ ) {
+ ProgressIndicatorCustomizer(
+ progress = progress,
+ startAngle = startAngle,
+ endAngle = endAngle,
+ enabled = enabled,
+ hasLargeStroke = hasLargeStroke,
+ hasCustomColors = hasCustomColors,
+ numSegments = numSegments,
+ overflowAllowed = overflowAllowed
+ )
+
+ SegmentedCircularProgressIndicator(
+ segmentCount = numSegments.value,
+ progress = { progress.value },
+ startAngle = startAngle.value,
+ endAngle = endAngle.value,
+ enabled = enabled.value,
+ allowProgressOverflow = overflowAllowed.value,
+ strokeWidth = strokeWidth,
+ colors = colors,
+ )
+ }
+}
+
+@OptIn(ExperimentalWearMaterial3Api::class)
+@Composable
+fun ProgressIndicatorCustomizer(
+ progress: MutableState<Float>,
+ startAngle: MutableState<Float>,
+ endAngle: MutableState<Float>,
+ enabled: MutableState<Boolean>,
+ hasLargeStroke: MutableState<Boolean>,
+ hasCustomColors: MutableState<Boolean>,
+ overflowAllowed: MutableState<Boolean>,
+ numSegments: MutableState<Int>? = null,
+) {
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize().padding(12.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ item { Text(String.format("Progress: %.0f%%", progress.value * 100)) }
+ item {
+ InlineSlider(
+ value = progress.value,
+ onValueChange = { progress.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..2f,
+ steps = 9,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ segmented = false
+ )
+ }
+ if (numSegments != null) {
+ item { Text("Segments: ${numSegments.value}") }
+ item {
+ InlineSlider(
+ value = numSegments.value.toFloat(),
+ onValueChange = { numSegments.value = it.toInt() },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 1f..12f,
+ steps = 10,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ }
+ item { Text("Start Angle: ${startAngle.value.toInt()}") }
+ item {
+ InlineSlider(
+ value = startAngle.value,
+ onValueChange = { startAngle.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..360f,
+ steps = 7,
+ segmented = false,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ item { Text("End angle: ${endAngle.value.toInt()}") }
+ item {
+ InlineSlider(
+ value = endAngle.value,
+ onValueChange = { endAngle.value = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..360f,
+ steps = 7,
+ segmented = false,
+ colors =
+ InlineSliderDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.background,
+ ),
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = enabled.value,
+ onCheckedChange = { enabled.value = it },
+ label = { Text("Enabled") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = hasLargeStroke.value,
+ onCheckedChange = { hasLargeStroke.value = it },
+ label = { Text("Large stroke") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = hasCustomColors.value,
+ onCheckedChange = { hasCustomColors.value = it },
+ label = { Text("Custom colors") },
+ )
+ }
+ item {
+ SwitchButton(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ checked = overflowAllowed.value,
+ onCheckedChange = { overflowAllowed.value = it },
+ label = { Text("Overflow") },
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
index 34aa8bb..7ff3e1c 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/RadioButtonDemo.kt
@@ -32,7 +32,7 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.RadioButton
import androidx.wear.compose.material3.Text
@@ -139,7 +139,7 @@
Text(
primary,
Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
index 55249c0..82e0e68 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitCheckboxButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitCheckboxButton
import androidx.wear.compose.material3.Text
@@ -99,7 +99,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
index db4de04..b5493b5 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitRadioButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitRadioButton
import androidx.wear.compose.material3.Text
@@ -105,7 +105,7 @@
Text(
primary,
Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
index 5388dd2..eaa636d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SplitSwitchButtonDemo.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SplitSwitchButton
import androidx.wear.compose.material3.Text
@@ -90,7 +90,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines
)
},
secondaryLabel =
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
index b92ffdf..6ba5e6f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SwitchButtonDemo.kt
@@ -28,7 +28,7 @@
import androidx.compose.ui.Modifier
import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.LocalTextMaxLines
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.SwitchButton
import androidx.wear.compose.material3.Text
@@ -113,7 +113,7 @@
Text(
primary,
modifier = Modifier.fillMaxWidth(),
- maxLines = primaryMaxLines ?: LocalTextMaxLines.current,
+ maxLines = primaryMaxLines ?: LocalTextConfiguration.current.maxLines,
)
},
secondaryLabel = {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
index 631150f..8bca579 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TypographyDemo.kt
@@ -16,31 +16,57 @@
package androidx.wear.compose.material3.demos
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.foundation.CurvedDirection
import androidx.wear.compose.foundation.CurvedLayout
import androidx.wear.compose.foundation.CurvedTextStyle
import androidx.wear.compose.integration.demos.common.Centralize
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material3.CurvedTextDefaults
+import androidx.wear.compose.material3.ExperimentalWearMaterial3Api
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.InlineSlider
+import androidx.wear.compose.material3.InlineSliderDefaults
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextToggleButton
import androidx.wear.compose.material3.curvedText
+@OptIn(ExperimentalWearMaterial3Api::class)
var TypographyDemos =
listOf(
DemoCategory(
"Arc",
listOf(
ComposableDemo("Arc Small") {
- val curvedStyle = CurvedTextStyle(MaterialTheme.typography.arcSmall)
- CurvedLayout { curvedText("Arc Small", style = curvedStyle) }
+ ArcWithLetterSpacing(MaterialTheme.typography.arcSmall, "Arc Small")
},
ComposableDemo("Arc Medium") {
- val curvedStyle = CurvedTextStyle(MaterialTheme.typography.arcMedium)
- CurvedLayout { curvedText("Arc Medium", style = curvedStyle) }
- }
+ ArcWithLetterSpacing(MaterialTheme.typography.arcMedium, "Arc Medium")
+ },
+ ComposableDemo("Arc Large") {
+ ArcWithLetterSpacing(MaterialTheme.typography.arcLarge, "Arc Large")
+ },
)
),
DemoCategory(
@@ -124,3 +150,71 @@
)
),
)
+
+@OptIn(ExperimentalWearMaterial3Api::class)
+@Composable
+private fun ArcWithLetterSpacing(arcStyle: TextStyle, label: String) {
+ var topLetterSpacing by remember { mutableStateOf(0.6f) }
+ var bottomLetterSpacing by remember { mutableStateOf(2.0f) }
+ val topCurvedStyle = CurvedTextStyle(arcStyle).copy(letterSpacing = topLetterSpacing.sp)
+ val bottomCurvedStyle = CurvedTextStyle(arcStyle).copy(letterSpacing = bottomLetterSpacing.sp)
+ val mmms = "MMMMMMMMMMMMMMMMMMMM"
+ var useMMMs by remember { mutableStateOf(true) }
+
+ Box {
+ CurvedLayout {
+ curvedText(
+ if (useMMMs) mmms else label,
+ style = topCurvedStyle,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ CurvedLayout(anchor = 90f, angularDirection = CurvedDirection.Angular.Reversed) {
+ curvedText(
+ if (useMMMs) mmms else label,
+ style = bottomCurvedStyle,
+ maxSweepAngle = CurvedTextDefaults.StaticContentMaxSweepAngle,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Column(
+ modifier = Modifier.fillMaxSize().padding(32.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ "Top=$topLetterSpacing, bottom = $bottomLetterSpacing",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ InlineSlider(
+ value = topLetterSpacing,
+ onValueChange = { topLetterSpacing = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..4f,
+ steps = 39,
+ segmented = false
+ )
+ InlineSlider(
+ value = bottomLetterSpacing,
+ onValueChange = { bottomLetterSpacing = it },
+ increaseIcon = { Icon(InlineSliderDefaults.Increase, "Increase") },
+ decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "Decrease") },
+ valueRange = 0f..4f,
+ steps = 39,
+ segmented = false
+ )
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ TextToggleButton(
+ checked = useMMMs,
+ onCheckedChange = { useMMMs = !useMMMs },
+ modifier = Modifier.height(36.dp)
+ ) {
+ Text(text = "MMM")
+ }
+ }
+ }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 2f9ce14..5e2a401 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -43,6 +43,22 @@
Material3DemoCategory(
"Material 3",
listOf(
+ Material3DemoCategory(title = "Typography", TypographyDemos),
+ Material3DemoCategory(
+ "Button",
+ listOf(
+ ComposableDemo("Base Button") { BaseButtonDemo() },
+ ComposableDemo("Filled Button") { ButtonDemo() },
+ ComposableDemo("Filled Tonal Button") { FilledTonalButtonDemo() },
+ ComposableDemo("Filled Variant Button") { FilledVariantButtonDemo() },
+ ComposableDemo("Outlined Button") { OutlinedButtonDemo() },
+ ComposableDemo("Child Button") { ChildButtonDemo() },
+ ComposableDemo("Multiline Button") { MultilineButtonDemo() },
+ ComposableDemo("App Button") { AppButtonDemo() },
+ ComposableDemo("Avatar Button") { AvatarButtonDemo() },
+ ComposableDemo("Button (Image Background)") { ButtonBackgroundImageDemo() },
+ )
+ ),
ComposableDemo("Color Scheme") { ColorSchemeDemos() },
Material3DemoCategory("Curved Text", CurvedTextDemos),
Material3DemoCategory("Alert Dialog", AlertDialogs),
@@ -51,19 +67,6 @@
ComposableDemo("Scaffold") { ScaffoldSample() },
Material3DemoCategory("ScrollAway", ScrollAwayDemos),
ComposableDemo("Haptics") { Centralize { HapticsDemos() } },
- Material3DemoCategory(
- "Button",
- listOf(
- ComposableDemo("Filled Button") { ButtonDemo() },
- ComposableDemo("Filled Tonal Button") { FilledTonalButtonDemo() },
- ComposableDemo("Filled Variant Button") { FilledVariantButtonDemo() },
- ComposableDemo("Outlined Button") { OutlinedButtonDemo() },
- ComposableDemo("Child Button") { ChildButtonDemo() },
- ComposableDemo("Multiline Button") { MultilineButtonDemo() },
- ComposableDemo("Avatar Button") { AvatarButtonDemo() },
- ComposableDemo("Button (Image Background)") { ButtonBackgroundImageDemo() },
- )
- ),
ComposableDemo("Compact Button") { CompactButtonDemo() },
ComposableDemo("Icon Button") { IconButtonDemo() },
ComposableDemo("Image Button") { ImageButtonDemo() },
@@ -75,7 +78,6 @@
ComposableDemo("Sizes and Colors") { EdgeButtonMultiDemo() },
ComposableDemo("Configurable") { EdgeButtonConfigurableDemo() },
ComposableDemo("Simple Edge Button below SLC") { EdgeButtonListSample() },
- ComposableDemo("Edge Button Below C") { EdgeButtonBelowColumnDemo() },
ComposableDemo("Edge Button Below LC") { EdgeButtonBelowLazyColumnDemo() },
ComposableDemo("Edge Button Below SLC") {
EdgeButtonBelowScalingLazyColumnDemo()
@@ -93,6 +95,7 @@
Material3DemoCategory("Time Text", TimeTextDemos),
ComposableDemo("Card") { CardDemo() },
ComposableDemo("Animated Shape Buttons") { AnimatedShapeButtonDemo() },
+ ComposableDemo("Animated Shape Toggle Buttons") { AnimatedShapeToggleButtonDemo() },
ComposableDemo("Text Toggle Button") { TextToggleButtonDemo() },
ComposableDemo("Icon Toggle Button") { IconToggleButtonDemo() },
ComposableDemo("Checkbox Button") { CheckboxButtonDemo() },
@@ -150,7 +153,6 @@
},
)
),
- Material3DemoCategory(title = "Typography", TypographyDemos),
Material3DemoCategory(
"Animated Text",
if (Build.VERSION.SDK_INT > 31) {
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
index b751fc8..64906ea 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
@@ -21,9 +21,9 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
@@ -42,23 +42,6 @@
@Sampled
@Composable
-fun ButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth(),
-) {
- Button(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun ButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
Button(
onClick = { /* Do something */ },
@@ -66,7 +49,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -77,6 +60,53 @@
@Sampled
@Composable
+fun ButtonLargeIconSample(modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean = true) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.LargeIconSize)
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding,
+ modifier = modifier
+ )
+}
+
+@Sampled
+@Composable
+fun ButtonExtraLargeIconSample(
+ modifier: Modifier = Modifier.fillMaxWidth(),
+ enabled: Boolean = true
+) {
+ // When customising the icon size, it is recommended to also specify
+ // the associated content padding
+ Button(
+ onClick = { /* Do something */ },
+ enabled = enabled,
+ label = { Text("Button") },
+ secondaryLabel = { Text("Secondary label") },
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_favorite_rounded),
+ contentDescription = "Favorite icon",
+ modifier = Modifier.size(ButtonDefaults.ExtraLargeIconSize)
+ )
+ },
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding,
+ modifier = modifier
+ )
+}
+
+@Sampled
+@Composable
fun SimpleFilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
FilledTonalButton(
onClick = { /* Do something */ },
@@ -87,23 +117,6 @@
@Sampled
@Composable
-fun FilledTonalButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- FilledTonalButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Filled Tonal Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun FilledTonalButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
FilledTonalButton(
onClick = { /* Do something */ },
@@ -111,7 +124,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -141,7 +154,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -162,23 +175,6 @@
@Sampled
@Composable
-fun OutlinedButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- OutlinedButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Outlined Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun OutlinedButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = { /* Do something */ },
@@ -186,7 +182,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -207,23 +203,6 @@
@Sampled
@Composable
-fun ChildButtonWithOnLongClickSample(
- onClickHandler: () -> Unit,
- onLongClickHandler: () -> Unit,
- modifier: Modifier = Modifier.fillMaxWidth()
-) {
- ChildButton(
- onClick = onClickHandler,
- onLongClick = onLongClickHandler,
- onLongClickLabel = "Long click",
- label = { Text("Child Button") },
- secondaryLabel = { Text("with long click") },
- modifier = modifier,
- )
-}
-
-@Sampled
-@Composable
fun ChildButtonSample(modifier: Modifier = Modifier.fillMaxWidth()) {
ChildButton(
onClick = { /* Do something */ },
@@ -231,7 +210,7 @@
secondaryLabel = { Text("Secondary label") },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@@ -247,7 +226,7 @@
onClick = { /* Do something */ },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.SmallIconSize)
)
@@ -281,7 +260,7 @@
onClick = { /* Do something */ },
icon = {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(ButtonDefaults.SmallIconSize)
)
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconToggleButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconToggleButtonSample.kt
index 84892fb..7c9dd74 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconToggleButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/IconToggleButtonSample.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
@@ -25,13 +26,43 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.IconButtonDefaults
import androidx.wear.compose.material3.IconToggleButton
+import androidx.wear.compose.material3.IconToggleButtonDefaults
@Sampled
@Composable
fun IconToggleButtonSample() {
+ val interactionSource = remember { MutableInteractionSource() }
var checked by remember { mutableStateOf(true) }
- IconToggleButton(checked = checked, onCheckedChange = { checked = !checked }) {
+ IconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ interactionSource = interactionSource,
+ shape =
+ IconButtonDefaults.animatedShape(
+ interactionSource = interactionSource,
+ ),
+ ) {
+ Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
+ }
+}
+
+@Sampled
+@Composable
+fun IconToggleButtonVariantSample() {
+ val interactionSource = remember { MutableInteractionSource() }
+ var checked by remember { mutableStateOf(true) }
+ IconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ interactionSource = interactionSource,
+ shape =
+ IconToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = checked
+ ),
+ ) {
Icon(imageVector = Icons.Filled.Favorite, contentDescription = "Favorite icon")
}
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/LazyColumnScrollTransformSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/LazyColumnScrollTransformSample.kt
index fa93ea6..cb75fa4 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/LazyColumnScrollTransformSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/LazyColumnScrollTransformSample.kt
@@ -18,9 +18,7 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -32,8 +30,12 @@
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.LazyColumn
import androidx.wear.compose.foundation.lazy.items
+import androidx.wear.compose.foundation.lazy.rememberLazyColumnState
+import androidx.wear.compose.material3.AppScaffold
+import androidx.wear.compose.material3.EdgeButton
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.ScreenScaffold
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.lazy.scrollTransform
import androidx.wear.compose.material3.lazy.targetMorphingHeight
@@ -43,28 +45,37 @@
@Composable
fun LazyColumnScalingMorphingEffectSample() {
val allIngredients = listOf("2 eggs", "tomato", "cheese", "bread")
-
- LazyColumn(modifier = Modifier.background(Color.Black).padding(horizontal = 10.dp)) {
- item {
- // No modifier is applied - no Material 3 Motion.
- ListHeader { Text("Ingredients") }
- }
-
- items(allIngredients) { ingredient ->
- Text(
- ingredient,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyLarge,
+ val state = rememberLazyColumnState()
+ AppScaffold {
+ ScreenScaffold(state, bottomButton = { EdgeButton(onClick = {}) { Text("Okay") } }) {
+ LazyColumn(
+ state = state,
modifier =
- Modifier.fillMaxWidth()
- // Apply Material 3 Motion transformations.
- .scrollTransform(
- this,
- backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
- shape = RoundedCornerShape(10.dp)
- )
- .padding(10.dp)
- )
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(horizontal = 10.dp)
+ ) {
+ item {
+ // No modifier is applied - no Material 3 Motion.
+ ListHeader { Text("Ingredients") }
+ }
+
+ items(allIngredients) { ingredient ->
+ Text(
+ ingredient,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier =
+ Modifier.fillMaxWidth()
+ // Apply Material 3 Motion transformations.
+ .scrollTransform(
+ this,
+ backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
+ shape = MaterialTheme.shapes.small
+ )
+ .padding(10.dp)
+ )
+ }
+ }
}
}
}
@@ -84,11 +95,14 @@
MenuItem("Black tea", 2f),
MenuItem("London fog", 2.6f),
)
-
- MaterialTheme {
- Box(modifier = Modifier.aspectRatio(1f).background(Color.Black)) {
+ val state = rememberLazyColumnState()
+ AppScaffold {
+ ScreenScaffold(state, bottomButton = { EdgeButton(onClick = {}) { Text("Okay") } }) {
LazyColumn(
- modifier = Modifier.padding(horizontal = 10.dp),
+ state = state,
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.background)
+ .padding(horizontal = 10.dp),
) {
item {
// No modifier is applied - no Material 3 Motion transformations.
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
index 4af6a52..47a6bd2 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ProgressIndicatorSample.kt
@@ -34,7 +34,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@@ -61,11 +60,6 @@
progress = { 0.25f },
startAngle = 120f,
endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
)
}
}
@@ -119,21 +113,11 @@
.fillMaxSize()
) {
CircularProgressIndicator(
- // The progress is limited by 100%, 120% ends up being 20% with the track brush
- // indicating overflow.
- progress = { 0.2f },
+ // Overflow value of 120%
+ progress = { 1.2f },
+ allowProgressOverflow = true,
startAngle = 120f,
endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- trackBrush =
- Brush.linearGradient(
- listOf(
- MaterialTheme.colorScheme.primary,
- MaterialTheme.colorScheme.surfaceContainer
- )
- )
- )
)
}
}
@@ -161,6 +145,14 @@
@Sampled
@Composable
+fun IndeterminateProgressIndicatorSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+}
+
+@Sampled
+@Composable
fun SegmentedProgressIndicatorSample() {
Box(
modifier =
@@ -171,11 +163,6 @@
SegmentedCircularProgressIndicator(
segmentCount = 5,
progress = { 0.5f },
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
)
}
}
@@ -192,11 +179,18 @@
SegmentedCircularProgressIndicator(
segmentCount = 5,
completed = { it % 2 != 0 },
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Green.copy(alpha = 0.5f)
- )
+ )
+ }
+}
+
+@Sampled
+@Composable
+fun SmallSegmentedProgressIndicatorSample() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ SegmentedCircularProgressIndicator(
+ segmentCount = 8,
+ completed = { it % 2 != 0 },
+ modifier = Modifier.align(Alignment.Center).size(80.dp)
)
}
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TextToggleButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TextToggleButtonSample.kt
index 4da883e..3e6de06 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TextToggleButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/TextToggleButtonSample.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.material3.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -27,13 +28,42 @@
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TextButtonDefaults
import androidx.wear.compose.material3.TextToggleButton
+import androidx.wear.compose.material3.TextToggleButtonDefaults
import androidx.wear.compose.material3.touchTargetAwareSize
@Sampled
@Composable
fun TextToggleButtonSample() {
+ val interactionSource = remember { MutableInteractionSource() }
var checked by remember { mutableStateOf(true) }
- TextToggleButton(checked = checked, onCheckedChange = { checked = !checked }) {
+ TextToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ interactionSource = interactionSource,
+ shape =
+ TextButtonDefaults.animatedShape(
+ interactionSource = interactionSource,
+ ),
+ ) {
+ Text(text = if (checked) "On" else "Off")
+ }
+}
+
+@Sampled
+@Composable
+fun TextToggleButtonVariantSample() {
+ val interactionSource = remember { MutableInteractionSource() }
+ var checked by remember { mutableStateOf(true) }
+ TextToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = !checked },
+ interactionSource = interactionSource,
+ shape =
+ TextToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = checked
+ )
+ ) {
Text(text = if (checked) "On" else "Off")
}
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
similarity index 70%
rename from wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt
rename to wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
index b791a52..99bf58a 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/Icons.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/icons/SampleIcons.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open 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,41 +14,39 @@
* limitations under the License.
*/
-package androidx.wear.compose.material3.demos
+package androidx.wear.compose.material3.samples.icons
import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.samples.R
@Composable
-internal fun StandardIcon(size: Dp) {
+fun FavoriteIcon(size: Dp) {
Icon(
- Icons.Filled.Favorite,
+ painter = painterResource(R.drawable.ic_favorite_rounded),
contentDescription = "Favorite icon",
modifier = Modifier.size(size)
)
}
@Composable
-internal fun AvatarIcon() {
+fun AvatarIcon() {
Icon(
- Icons.Filled.AccountCircle,
+ painter = painterResource(R.drawable.ic_account_circle),
contentDescription = "Account",
modifier = Modifier.size(ButtonDefaults.LargeIconSize)
)
}
@Composable
-internal fun CheckIcon() {
+fun CheckIcon() {
Icon(
- Icons.Filled.Check,
+ painter = painterResource(R.drawable.ic_check_rounded),
contentDescription = "Check",
modifier = Modifier.size(ButtonDefaults.IconSize)
)
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml
new file mode 100644
index 0000000..21441c1
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_account_circle.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M5.85,17.1C6.7,16.45 7.65,15.942 8.7,15.575C9.75,15.192 10.85,15 12,15C13.15,15 14.25,15.192 15.3,15.575C16.35,15.942 17.3,16.45 18.15,17.1C18.733,16.417 19.183,15.642 19.5,14.775C19.833,13.908 20,12.983 20,12C20,9.783 19.217,7.9 17.65,6.35C16.1,4.783 14.217,4 12,4C9.783,4 7.892,4.783 6.325,6.35C4.775,7.9 4,9.783 4,12C4,12.983 4.158,13.908 4.475,14.775C4.808,15.642 5.267,16.417 5.85,17.1ZM12,13C11.017,13 10.183,12.667 9.5,12C8.833,11.317 8.5,10.483 8.5,9.5C8.5,8.517 8.833,7.692 9.5,7.025C10.183,6.342 11.017,6 12,6C12.983,6 13.808,6.342 14.475,7.025C15.158,7.692 15.5,8.517 15.5,9.5C15.5,10.483 15.158,11.317 14.475,12C13.808,12.667 12.983,13 12,13ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml
new file mode 100644
index 0000000..19c9634
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_check_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M9.55,15.15L18.025,6.675C18.225,6.475 18.458,6.375 18.725,6.375C18.992,6.375 19.225,6.475 19.425,6.675C19.625,6.875 19.725,7.117 19.725,7.4C19.725,7.667 19.625,7.9 19.425,8.1L10.25,17.3C10.05,17.5 9.817,17.6 9.55,17.6C9.283,17.6 9.05,17.5 8.85,17.3L4.55,13C4.35,12.8 4.25,12.567 4.25,12.3C4.267,12.017 4.375,11.775 4.575,11.575C4.775,11.375 5.008,11.275 5.275,11.275C5.558,11.275 5.8,11.375 6,11.575L9.55,15.15Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml
new file mode 100644
index 0000000..d52b7e5
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/res/drawable/ic_favorite_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,20.325C11.767,20.325 11.525,20.283 11.275,20.2C11.042,20.117 10.833,19.983 10.65,19.8L8.925,18.225C7.158,16.608 5.558,15.008 4.125,13.425C2.708,11.825 2,10.067 2,8.15C2,6.583 2.525,5.275 3.575,4.225C4.625,3.175 5.933,2.65 7.5,2.65C8.383,2.65 9.217,2.842 10,3.225C10.783,3.592 11.45,4.1 12,4.75C12.55,4.1 13.217,3.592 14,3.225C14.783,2.842 15.617,2.65 16.5,2.65C18.067,2.65 19.375,3.175 20.425,4.225C21.475,5.275 22,6.583 22,8.15C22,10.067 21.292,11.825 19.875,13.425C18.458,15.025 16.85,16.633 15.05,18.25L13.35,19.8C13.167,19.983 12.95,20.117 12.7,20.2C12.467,20.283 12.233,20.325 12,20.325Z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index e049442..b593267 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -16,19 +16,14 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -64,12 +59,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -78,12 +73,12 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
screenSize = screenSize,
- titleText = "Network error"
+ titleText = "Network error",
+ messageText = null
)
@Test
@@ -92,11 +87,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
screenSize = screenSize,
+ messageText = null,
)
@Test
@@ -105,11 +100,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = false,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
@Test
@@ -118,11 +113,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -132,11 +127,11 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = false,
showContent = false,
showTwoButtons = true,
scrollToBottom = false,
- screenSize = screenSize
+ screenSize = screenSize,
+ messageText = null
)
}
@@ -146,7 +141,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = false,
showTwoButtons = false,
scrollToBottom = false,
@@ -162,7 +156,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = false,
@@ -178,7 +171,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = false,
scrollToBottom = true,
@@ -194,7 +186,6 @@
testName = testName,
screenshotRule = screenshotRule,
showIcon = true,
- showText = true,
showContent = true,
showTwoButtons = true,
scrollToBottom = true,
@@ -202,34 +193,47 @@
)
}
+ @Test
+ fun alert_title_longMessageText_bottomButton(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = false,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
+ @Test
+ fun alert_title_longMessageText_confirmDismissButtons(@TestParameter screenSize: ScreenSize) {
+ rule.verifyAlertDialogScreenshot(
+ testName = testName,
+ screenshotRule = screenshotRule,
+ showIcon = false,
+ showContent = false,
+ showTwoButtons = true,
+ scrollToBottom = false,
+ screenSize = screenSize,
+ messageText = longMessageText
+ )
+ }
+
private fun ComposeContentTestRule.verifyAlertDialogScreenshot(
testName: TestName,
screenshotRule: AndroidXScreenshotTestRule,
showIcon: Boolean,
- showText: Boolean,
showContent: Boolean,
showTwoButtons: Boolean,
scrollToBottom: Boolean,
screenSize: ScreenSize,
+ messageText: String? = "Your battery is low. Turn on battery saver.",
titleText: String = "Mobile network is not currently available"
) {
- setContentWithTheme() {
- val originalConfiguration = LocalConfiguration.current
- val originalContext = LocalContext.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSize.size
- screenHeightDp = screenSize.size
- screenLayout = Configuration.SCREENLAYOUT_ROUND_YES
- }
- }
- originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
-
- CompositionLocalProvider(
- LocalContext provides originalContext,
- LocalConfiguration provides fixedScreenSizeConfiguration,
- ) {
+ setContentWithTheme {
+ ScreenConfiguration(screenSize.size) {
AlertDialogHelper(
modifier = Modifier.size(screenSize.size.dp).testTag(TEST_TAG),
title = { Text(titleText) },
@@ -239,8 +243,8 @@
} else null,
showTwoButtons = showTwoButtons,
text =
- if (showText) {
- { Text("Your battery is low. Turn on battery saver.") }
+ if (messageText != null) {
+ { Text(messageText) }
} else null,
content =
if (showContent) {
@@ -308,3 +312,6 @@
)
}
}
+
+internal const val longMessageText =
+ "Allow Map to access your location even when you're not using the app? Your location is used to automatically map places to activities."
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 6e59369..63cb2df 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -263,7 +263,7 @@
var expectedContentColor: Color = Color.Unspecified
var expectedTextStyle: TextStyle = TextStyle.Default
var expectedTextAlign: TextAlign? = null
- var expectedTextMaxLines: Int = 0
+ var expectedTextMaxLines = 0
var actualContentColor: Color = Color.Unspecified
var actualTextStyle: TextStyle = TextStyle.Default
@@ -281,8 +281,8 @@
Text("Title")
actualContentColor = LocalContentColor.current
actualTextStyle = LocalTextStyle.current
- actualTextAlign = LocalTextAlign.current
- actualTextMaxLines = LocalTextMaxLines.current
+ actualTextAlign = LocalTextConfiguration.current.textAlign
+ actualTextMaxLines = LocalTextConfiguration.current.maxLines
},
bottomButton = {},
onDismissRequest = {},
@@ -317,7 +317,7 @@
Text("Text")
actualContentColor = LocalContentColor.current
actualTextStyle = LocalTextStyle.current
- actualTextAlign = LocalTextAlign.current
+ actualTextAlign = LocalTextConfiguration.current.textAlign
},
bottomButton = {},
onDismissRequest = {},
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonGroupScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonGroupScreenshotTest.kt
index ca670d5..bfd690a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonGroupScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonGroupScreenshotTest.kt
@@ -19,8 +19,6 @@
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
@@ -58,7 +56,7 @@
@Test
fun button_group_3_items_different_sizes() =
- verifyScreenshot(numItems = 3, weight2 = 2f, weight3 = 3f)
+ verifyScreenshot(numItems = 3, minWidth1 = 24.dp, weight2 = 2f, weight3 = 3f)
private fun verifyScreenshot(
numItems: Int = 2,
@@ -73,12 +71,10 @@
) {
require(numItems in 1..3)
rule.setContentWithTheme {
- val interactionSource1 = remember { MutableInteractionSource() }
- val interactionSource2 = remember { MutableInteractionSource() }
- val interactionSource3 = remember { MutableInteractionSource() }
- Box(
- modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
- ) {
+ ScreenConfiguration(SCREEN_SIZE_SMALL) {
+ val interactionSource1 = remember { MutableInteractionSource() }
+ val interactionSource2 = remember { MutableInteractionSource() }
+ val interactionSource3 = remember { MutableInteractionSource() }
ButtonGroup(
Modifier.testTag(TEST_TAG),
spacing = spacing,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
index 4b23eab..5baf0a4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
@@ -21,9 +21,13 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -32,6 +36,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -43,11 +48,11 @@
import androidx.wear.compose.material3.ChildButton
import androidx.wear.compose.material3.CompactButton
import androidx.wear.compose.material3.FilledTonalButton
+import androidx.wear.compose.material3.Icon
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.OutlinedButton
import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
import androidx.wear.compose.material3.TEST_TAG
-import androidx.wear.compose.material3.TestIcon
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.setContentWithTheme
import org.junit.Rule
@@ -65,9 +70,25 @@
@get:Rule val testName = TestName()
- @Test fun button_enabled() = verifyScreenshot() { BaseButton() }
+ @Test fun button_enabled() = verifyScreenshot { BaseButton() }
- @Test fun button_disabled() = verifyScreenshot() { BaseButton(enabled = false) }
+ @Test fun button_disabled() = verifyScreenshot { BaseButton(enabled = false) }
+
+ @Test
+ fun button_default_alignment() = verifyScreenshot {
+ // Uses the base Button overload, should be vertically centered by default
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth().testTag(TEST_TAG)) {
+ Text("Button")
+ }
+ }
+
+ @Test
+ fun button_top_alignment() = verifyScreenshot {
+ // Uses RowScope to override the default vertical alignment to be top
+ Button(onClick = {}, modifier = Modifier.fillMaxWidth().testTag(TEST_TAG)) {
+ Text("Button", modifier = Modifier.align(Alignment.Top))
+ }
+ }
@Test
fun three_slot_button_ltr() =
@@ -111,7 +132,31 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
+ )
+ }
+
+ @Test
+ fun button_large_icon() = verifyScreenshot {
+ Button(
+ onClick = {},
+ modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
+ label = { Text("Label", modifier = Modifier.fillMaxWidth()) },
+ secondaryLabel = { Text("Secondary label", modifier = Modifier.fillMaxWidth()) },
+ icon = { ButtonIcon(size = ButtonDefaults.LargeIconSize) },
+ contentPadding = ButtonDefaults.ButtonWithLargeIconContentPadding
+ )
+ }
+
+ @Test
+ fun button_extra_large_icon() = verifyScreenshot {
+ Button(
+ onClick = {},
+ modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
+ label = { Text("Label", modifier = Modifier.fillMaxWidth()) },
+ secondaryLabel = { Text("Secondary label", modifier = Modifier.fillMaxWidth()) },
+ icon = { ButtonIcon(size = ButtonDefaults.ExtraLargeIconSize) },
+ contentPadding = ButtonDefaults.ButtonWithExtraLargeIconContentPadding
)
}
@@ -130,7 +175,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -149,7 +194,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -168,7 +213,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
)
}
@@ -191,7 +236,7 @@
onClick = {},
modifier = Modifier.fillMaxWidth().testTag(TEST_TAG),
label = { Text("Label only", modifier = Modifier.fillMaxWidth()) },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
)
}
@@ -209,7 +254,7 @@
onClick = {},
label = { Text("Three Slot Button") },
secondaryLabel = { Text("Secondary Label") },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.IconSize) },
modifier = Modifier.testTag(TEST_TAG)
)
}
@@ -233,7 +278,7 @@
backgroundImagePainter = painterResource(R.drawable.backgroundimage1),
forcedSize = size
),
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
modifier = Modifier.testTag(TEST_TAG)
)
}
@@ -243,7 +288,7 @@
CompactButton(
onClick = {},
label = { Text("Compact Button") },
- icon = { TestIcon() },
+ icon = { ButtonIcon(size = ButtonDefaults.SmallIconSize) },
enabled = enabled,
modifier = Modifier.testTag(TEST_TAG)
)
@@ -269,4 +314,18 @@
.captureToImage()
.assertAgainstGolden(screenshotRule, testName.methodName)
}
+
+ @Composable
+ private fun ButtonIcon(
+ size: Dp,
+ modifier: Modifier = Modifier,
+ iconLabel: String = "ButtonIcon",
+ ) {
+ val testImage = Icons.Outlined.AccountCircle
+ Icon(
+ imageVector = testImage,
+ contentDescription = iconLabel,
+ modifier = modifier.testTag(iconLabel).size(size)
+ )
+ }
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index ddc439a..2bc38ef 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -854,8 +854,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -871,8 +873,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -888,8 +892,10 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -904,7 +910,7 @@
rule.setContentWithTheme {
Button(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -919,8 +925,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -936,8 +944,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -953,8 +963,10 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -969,7 +981,7 @@
rule.setContentWithTheme {
FilledTonalButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -984,8 +996,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -1001,8 +1015,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -1018,8 +1034,10 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -1034,7 +1052,7 @@
rule.setContentWithTheme {
OutlinedButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -1049,8 +1067,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -1066,8 +1086,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -1083,8 +1105,10 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelAlignment = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -1099,7 +1123,7 @@
rule.setContentWithTheme {
ChildButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
@@ -1113,7 +1137,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
)
}
@@ -1127,7 +1151,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
)
}
@@ -1141,7 +1165,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
icon = {},
)
}
@@ -1156,7 +1180,7 @@
rule.setContentWithTheme {
CompactButton(
onClick = {},
- label = { labelAlignment = LocalTextAlign.current },
+ label = { labelAlignment = LocalTextConfiguration.current.textAlign },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
index 9f3afc7..ff443c2 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CheckboxButtonTest.kt
@@ -440,8 +440,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -458,8 +460,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -476,8 +480,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -494,8 +500,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -512,8 +520,10 @@
CheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -530,8 +540,10 @@
SplitCheckboxButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt
index 73e047e..ae7588d 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationScreenshotTest.kt
@@ -16,18 +16,13 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -181,21 +176,7 @@
content: @Composable (modifier: Modifier) -> Unit
) {
setContentWithTheme {
- val originalConfiguration = LocalConfiguration.current
- val originalContext = LocalContext.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSize.size
- screenHeightDp = screenSize.size
- }
- }
- originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
-
- CompositionLocalProvider(
- LocalContext provides originalContext,
- LocalConfiguration provides fixedScreenSizeConfiguration,
- ) {
+ ScreenConfiguration(screenSize.size) {
content(Modifier.size(screenSize.size.dp).testTag(TEST_TAG))
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt
index 7f334a8..fbebf51 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerScreenshotTest.kt
@@ -16,18 +16,12 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -40,7 +34,6 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
@@ -323,23 +316,8 @@
) {
val screenSizeDp = if (isLargeScreen) SCREENSHOT_SIZE_LARGE else SCREENSHOT_SIZE
setContentWithTheme {
- val originalConfiguration = LocalConfiguration.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSizeDp
- screenHeightDp = screenSizeDp
- }
- }
- CompositionLocalProvider(
- LocalLayoutDirection provides layoutDirection,
- LocalConfiguration provides fixedScreenSizeConfiguration
- ) {
- Box(
- modifier =
- Modifier.size(screenSizeDp.dp)
- .background(MaterialTheme.colorScheme.background)
- ) {
+ ScreenConfiguration(screenSizeDp) {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
content()
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
index d37e1e2..7d9b092 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
@@ -17,7 +17,6 @@
package androidx.wear.compose.material3
import android.os.Build
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
@@ -55,23 +54,18 @@
@get:Rule val testName = TestName()
@Test
- fun edge_button_default() =
- verifyScreenshot() {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
- EdgeButton(
- onClick = { /* Do something */ },
- modifier = Modifier.testTag(TEST_TAG)
- ) {
- BasicText("Text")
- }
+ fun edge_button_default() = verifyScreenshot {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
+ EdgeButton(onClick = { /* Do something */ }, modifier = Modifier.testTag(TEST_TAG)) {
+ BasicText("Text")
}
}
+ }
@Test
- fun edge_button_xsmall() =
- verifyScreenshot() {
- BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall)
- }
+ fun edge_button_xsmall() = verifyScreenshot {
+ BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall)
+ }
@Test
fun edge_button_small() =
@@ -101,41 +95,34 @@
}
@Test
- fun edge_button_small_space_limited() =
- verifyScreenshot() {
- BasicEdgeButton(
- buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
- constrainedHeight = 30.dp
- )
- }
+ fun edge_button_small_space_limited() = verifyScreenshot {
+ BasicEdgeButton(
+ buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
+ constrainedHeight = 30.dp
+ )
+ }
@Test
- fun edge_button_small_slightly_limited() =
- verifyScreenshot() {
- BasicEdgeButton(
- buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
- constrainedHeight = 40.dp
- )
- }
+ fun edge_button_small_slightly_limited() = verifyScreenshot {
+ BasicEdgeButton(
+ buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
+ constrainedHeight = 40.dp
+ )
+ }
private val LONG_TEXT =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, " +
"sed do eiusmod tempor incididunt ut labore et dolore."
@Test
- fun edge_button_xsmall_long_text() =
- verifyScreenshot() {
- BasicEdgeButton(
- buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall,
- text = LONG_TEXT
- )
- }
+ fun edge_button_xsmall_long_text() = verifyScreenshot {
+ BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall, text = LONG_TEXT)
+ }
@Test
- fun edge_button_large_long_text() =
- verifyScreenshot() {
- BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightLarge, text = LONG_TEXT)
- }
+ fun edge_button_large_long_text() = verifyScreenshot {
+ BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightLarge, text = LONG_TEXT)
+ }
@Composable
private fun BasicEdgeButton(
@@ -164,11 +151,8 @@
content: @Composable () -> Unit
) {
rule.setContentWithTheme {
- CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
- Box(
- modifier =
- Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
- ) {
+ ScreenConfiguration(SCREEN_SIZE_SMALL) {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
content()
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
index 176847a..f95bc67b 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
@@ -339,7 +339,7 @@
status = Status.Enabled,
colors = { IconButtonDefaults.iconButtonColors() },
expectedContainerColor = { Color.Transparent },
- expectedContentColor = { MaterialTheme.colorScheme.onSurface }
+ expectedContentColor = { MaterialTheme.colorScheme.primary }
)
}
@@ -415,7 +415,7 @@
status = Status.Enabled,
colors = { IconButtonDefaults.filledTonalIconButtonColors() },
expectedContainerColor = { MaterialTheme.colorScheme.surfaceContainer },
- expectedContentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
+ expectedContentColor = { MaterialTheme.colorScheme.primary }
)
}
@@ -441,7 +441,7 @@
status = Status.Enabled,
colors = { IconButtonDefaults.outlinedIconButtonColors() },
expectedContainerColor = { Color.Transparent },
- expectedContentColor = { MaterialTheme.colorScheme.onSurface }
+ expectedContentColor = { MaterialTheme.colorScheme.primary }
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonScreenshotTest.kt
index 04eb12e..630a7be 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonScreenshotTest.kt
@@ -17,18 +17,33 @@
package androidx.wear.compose.material3
import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Star
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -84,17 +99,95 @@
content = { sampleIconToggleButton(modifier = Modifier.offset(10.dp)) }
)
+ @Ignore("TODO: b/345199060 work out how to show pressed state in test")
+ @Test
+ fun animatedIconToggleButtonPressed() {
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+ Box(
+ modifier =
+ Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)
+ ) {
+ val interactionSource = remember {
+ MutableInteractionSource().apply {
+ tryEmit(PressInteraction.Press(Offset(0f, 0f)))
+ }
+ }
+ sampleIconToggleButton(
+ checked = false,
+ shape =
+ IconToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = false
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ }
+ }
+
+ rule.mainClock.autoAdvance = false
+ rule.mainClock.advanceTimeBy(500)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(rule = screenshotRule, goldenIdentifier = testName.methodName)
+ }
+
+ @Test
+ fun animatedIconToggleButtonChecked() =
+ rule.verifyScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ val interactionSource = remember { MutableInteractionSource() }
+ sampleIconToggleButton(
+ checked = true,
+ shape =
+ IconToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = true
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ )
+
+ @Test
+ fun animatedIconToggleButtonUnchecked() =
+ rule.verifyScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ val interactionSource = remember { MutableInteractionSource() }
+ sampleIconToggleButton(
+ checked = false,
+ shape =
+ IconToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = false
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ )
+
@Composable
private fun sampleIconToggleButton(
enabled: Boolean = true,
checked: Boolean = true,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ shape: Shape = TextButtonDefaults.shape,
+ interactionSource: MutableInteractionSource? = null
) {
IconToggleButton(
checked = checked,
onCheckedChange = {},
enabled = enabled,
- modifier = modifier.testTag(TEST_TAG)
+ modifier = modifier.testTag(TEST_TAG),
+ shape = shape,
+ interactionSource = interactionSource
) {
Icon(imageVector = Icons.Outlined.Star, contentDescription = "Favourite")
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
index 9c0a3f0..6ccc0ff 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconToggleButtonTest.kt
@@ -392,7 +392,7 @@
rule.verifyIconToggleButtonColors(
status = Status.Enabled,
checked = true,
- colors = { IconButtonDefaults.iconToggleButtonColors() },
+ colors = { IconToggleButtonDefaults.iconToggleButtonColors() },
containerColor = { MaterialTheme.colorScheme.primary },
contentColor = { MaterialTheme.colorScheme.onPrimary }
)
@@ -403,7 +403,7 @@
rule.verifyIconToggleButtonColors(
status = Status.Enabled,
checked = false,
- colors = { IconButtonDefaults.iconToggleButtonColors() },
+ colors = { IconToggleButtonDefaults.iconToggleButtonColors() },
containerColor = { MaterialTheme.colorScheme.surfaceContainer },
contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
)
@@ -414,7 +414,7 @@
rule.verifyIconToggleButtonColors(
status = Status.Disabled,
checked = false,
- colors = { IconButtonDefaults.iconToggleButtonColors() },
+ colors = { IconToggleButtonDefaults.iconToggleButtonColors() },
containerColor = {
MaterialTheme.colorScheme.onSurface.toDisabledColor(DisabledContainerAlpha)
},
@@ -427,7 +427,7 @@
rule.verifyIconToggleButtonColors(
status = Status.Disabled,
checked = true,
- colors = { IconButtonDefaults.iconToggleButtonColors() },
+ colors = { IconToggleButtonDefaults.iconToggleButtonColors() },
containerColor = {
MaterialTheme.colorScheme.onSurface.toDisabledColor(DisabledContainerAlpha)
},
@@ -443,7 +443,9 @@
status = Status.Enabled,
checked = true,
colors = {
- IconButtonDefaults.iconToggleButtonColors(checkedContainerColor = overrideColor)
+ IconToggleButtonDefaults.iconToggleButtonColors(
+ checkedContainerColor = overrideColor
+ )
},
containerColor = { overrideColor },
contentColor = { MaterialTheme.colorScheme.onPrimary }
@@ -459,7 +461,7 @@
status = Status.Enabled,
checked = true,
colors = {
- IconButtonDefaults.iconToggleButtonColors(checkedContentColor = overrideColor)
+ IconToggleButtonDefaults.iconToggleButtonColors(checkedContentColor = overrideColor)
},
containerColor = { MaterialTheme.colorScheme.primary },
contentColor = { overrideColor }
@@ -475,7 +477,9 @@
status = Status.Enabled,
checked = false,
colors = {
- IconButtonDefaults.iconToggleButtonColors(uncheckedContainerColor = overrideColor)
+ IconToggleButtonDefaults.iconToggleButtonColors(
+ uncheckedContainerColor = overrideColor
+ )
},
containerColor = { overrideColor },
contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
@@ -491,7 +495,9 @@
status = Status.Enabled,
checked = false,
colors = {
- IconButtonDefaults.iconToggleButtonColors(uncheckedContentColor = overrideColor)
+ IconToggleButtonDefaults.iconToggleButtonColors(
+ uncheckedContentColor = overrideColor
+ )
},
containerColor = { MaterialTheme.colorScheme.surfaceContainer },
contentColor = { overrideColor }
@@ -507,7 +513,7 @@
status = Status.Disabled,
checked = true,
colors = {
- IconButtonDefaults.iconToggleButtonColors(
+ IconToggleButtonDefaults.iconToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledCheckedContainerColor = overrideColor
)
@@ -526,7 +532,7 @@
status = Status.Disabled,
checked = true,
colors = {
- IconButtonDefaults.iconToggleButtonColors(
+ IconToggleButtonDefaults.iconToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledCheckedContentColor = overrideColor
)
@@ -547,7 +553,7 @@
status = Status.Disabled,
checked = false,
colors = {
- IconButtonDefaults.iconToggleButtonColors(
+ IconToggleButtonDefaults.iconToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledUncheckedContainerColor = overrideColor
)
@@ -566,7 +572,7 @@
status = Status.Disabled,
checked = false,
colors = {
- IconButtonDefaults.iconToggleButtonColors(
+ IconToggleButtonDefaults.iconToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledUncheckedContentColor = overrideColor
)
@@ -643,7 +649,7 @@
@Composable
private fun shapeColor(): Color {
- return IconButtonDefaults.iconToggleButtonColors()
+ return IconToggleButtonDefaults.iconToggleButtonColors()
.containerColor(enabled = true, checked = true)
.value
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index c49f71f..e3af5a4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.material3
+import android.content.res.Configuration
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
@@ -26,11 +27,13 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.testutils.assertContainsColor
import androidx.compose.ui.Alignment
@@ -41,6 +44,8 @@
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -89,6 +94,33 @@
SQUARE_DEVICE(false)
}
+@Composable
+fun ScreenConfiguration(screenSizeDp: Int, content: @Composable () -> Unit) {
+ val originalConfiguration = LocalConfiguration.current
+ val originalContext = LocalContext.current
+
+ val fixedScreenSizeConfiguration =
+ remember(originalConfiguration) {
+ Configuration(originalConfiguration).apply {
+ screenWidthDp = screenSizeDp
+ screenHeightDp = screenSizeDp
+ }
+ }
+ originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
+
+ CompositionLocalProvider(
+ LocalContext provides originalContext,
+ LocalConfiguration provides fixedScreenSizeConfiguration
+ ) {
+ Box(
+ modifier =
+ Modifier.size(screenSizeDp.dp).background(MaterialTheme.colorScheme.background),
+ ) {
+ content()
+ }
+ }
+}
+
/**
* Valid characters for golden identifiers are [A-Za-z0-9_-] TestParameterInjector adds '[' +
* parameter_values + ']' to the test name.
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt
index 74568a6..82d14ee 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogScreenshotTest.kt
@@ -16,15 +16,10 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -79,21 +74,7 @@
) {
rule.mainClock.autoAdvance = false
setContentWithTheme {
- val originalConfiguration = LocalConfiguration.current
- val originalContext = LocalContext.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSize.size
- screenHeightDp = screenSize.size
- }
- }
- originalContext.resources.configuration.updateFrom(fixedScreenSizeConfiguration)
-
- CompositionLocalProvider(
- LocalContext provides originalContext,
- LocalConfiguration provides fixedScreenSizeConfiguration,
- ) {
+ ScreenConfiguration(screenSize.size) {
OpenOnPhoneDialog(
show = true,
modifier = Modifier.size(screenSize.size.dp).testTag(TEST_TAG),
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
index 37798e1..66b4f3a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorScreenshotTest.kt
@@ -16,7 +16,6 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -28,14 +27,11 @@
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
@@ -43,17 +39,18 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
@MediumTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(TestParameterInjector::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
class ProgressIndicatorScreenshotTest {
@get:Rule val rule = createComposeRule()
@@ -63,18 +60,8 @@
@get:Rule val testName = TestName()
@Test
- fun progress_indicator_fullscreen() = verifyProgressIndicatorScreenshot {
- CircularProgressIndicator(
- progress = { 0.25f },
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- )
- }
-
- @Test
- fun progress_indicator_fullscreen_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun progress_indicator_fullscreen(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
CircularProgressIndicator(
progress = { 0.25f },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
@@ -82,26 +69,10 @@
endAngle = 60f,
)
}
- }
@Test
- fun progress_indicator_custom_color() = verifyProgressIndicatorScreenshot {
- CircularProgressIndicator(
- progress = { 0.75f },
- modifier = Modifier.size(200.dp).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- indicatorColor = Color.Green,
- trackColor = Color.Red.copy(alpha = 0.5f)
- )
- )
- }
-
- @Test
- fun progress_indicator_custom_color_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun progress_indicator_custom_color(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
CircularProgressIndicator(
progress = { 0.75f },
modifier = Modifier.size(200.dp).testTag(TEST_TAG),
@@ -114,36 +85,10 @@
)
)
}
- }
@Test
- fun progress_indicator_wrapping_media_button() = verifyProgressIndicatorScreenshot {
- val progressPadding = 4.dp
- Box(
- modifier =
- Modifier.size(IconButtonDefaults.DefaultButtonSize + progressPadding)
- .testTag(TEST_TAG)
- ) {
- CircularProgressIndicator(progress = { 0.75f }, strokeWidth = progressPadding)
- IconButton(
- modifier =
- Modifier.align(Alignment.Center)
- .padding(progressPadding)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.surfaceContainer),
- onClick = {}
- ) {
- Icon(
- imageVector = Icons.Filled.PlayArrow,
- contentDescription = "Play/pause button icon"
- )
- }
- }
- }
-
- @Test
- fun progress_indicator_wrapping_media_button_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun progress_indicator_wrapping_media_button(@TestParameter screenSize: ScreenSize) {
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
val progressPadding = 4.dp
Box(
modifier =
@@ -169,61 +114,33 @@
}
@Test
- fun progress_indicator_overflow() = verifyProgressIndicatorScreenshot {
- CircularProgressIndicator(
- progress = { 0.2f },
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- trackBrush =
- Brush.linearGradient(
- listOf(
- MaterialTheme.colorScheme.surfaceContainer,
- MaterialTheme.colorScheme.primary
- )
- )
- )
- )
- }
-
- @Test
- fun progress_indicator_overflow_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun progress_indicator_overflow(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
CircularProgressIndicator(
- progress = { 0.2f },
+ progress = { 1.2f },
modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
startAngle = 120f,
endAngle = 60f,
- colors =
- ProgressIndicatorDefaults.colors(
- trackBrush =
- Brush.linearGradient(
- listOf(
- MaterialTheme.colorScheme.surfaceContainer,
- MaterialTheme.colorScheme.primary
- )
- )
- )
+ allowProgressOverflow = true
)
}
- }
@Test
- fun progress_indicator_disabled() = verifyProgressIndicatorScreenshot {
- CircularProgressIndicator(
- progress = { 0.75f },
- modifier = Modifier.size(200.dp).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- enabled = false,
- )
- }
+ fun progress_indicator_overflow_disabled(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
+ CircularProgressIndicator(
+ progress = { 1.2f },
+ modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
+ startAngle = 120f,
+ endAngle = 60f,
+ allowProgressOverflow = true,
+ enabled = false
+ )
+ }
@Test
- fun progress_indicator_disabled_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun progress_indicator_disabled(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
CircularProgressIndicator(
progress = { 0.75f },
modifier = Modifier.size(200.dp).testTag(TEST_TAG),
@@ -232,22 +149,10 @@
enabled = false,
)
}
- }
@Test
- fun segmented_progress_indicator_with_progress() = verifyProgressIndicatorScreenshot {
- SegmentedCircularProgressIndicator(
- progress = { 0.5f },
- segmentCount = 5,
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- )
- }
-
- @Test
- fun segmented_progress_indicator_with_progress_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun segmented_progress_indicator_with_progress(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
progress = { 0.5f },
segmentCount = 5,
@@ -256,23 +161,37 @@
endAngle = 60f,
)
}
- }
@Test
- fun segmented_progress_indicator_with_progress_disabled() = verifyProgressIndicatorScreenshot {
- SegmentedCircularProgressIndicator(
- progress = { 0.5f },
- segmentCount = 5,
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- enabled = false,
- )
- }
+ fun segmented_progress_indicator_overflow(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
+ SegmentedCircularProgressIndicator(
+ progress = { 1.2f },
+ segmentCount = 5,
+ modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
+ startAngle = 120f,
+ endAngle = 60f,
+ allowProgressOverflow = true,
+ )
+ }
@Test
- fun segmented_progress_indicator_with_progress_disabled_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun segmented_progress_indicator_overflow_disabled(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
+ SegmentedCircularProgressIndicator(
+ progress = { 1.2f },
+ segmentCount = 5,
+ modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
+ startAngle = 120f,
+ endAngle = 60f,
+ allowProgressOverflow = true,
+ enabled = false,
+ )
+ }
+
+ @Test
+ fun segmented_progress_indicator_with_progress_disabled(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
progress = { 0.5f },
segmentCount = 5,
@@ -282,22 +201,10 @@
enabled = false,
)
}
- }
@Test
- fun segmented_progress_indicator_on_off() = verifyProgressIndicatorScreenshot {
- SegmentedCircularProgressIndicator(
- segmentCount = 6,
- completed = { it % 2 == 0 },
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- )
- }
-
- @Test
- fun segmented_progress_indicator_on_off_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun segmented_progress_indicator_on_off(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
segmentCount = 6,
completed = { it % 2 == 0 },
@@ -306,23 +213,10 @@
endAngle = 60f,
)
}
- }
@Test
- fun segmented_progress_indicator_on_off_disabled() = verifyProgressIndicatorScreenshot {
- SegmentedCircularProgressIndicator(
- segmentCount = 6,
- completed = { it % 2 == 0 },
- modifier = Modifier.aspectRatio(1f).testTag(TEST_TAG),
- startAngle = 120f,
- endAngle = 60f,
- enabled = false,
- )
- }
-
- @Test
- fun segmented_progress_indicator_on_off_disabled_large_screen() {
- verifyProgressIndicatorScreenshot(isLargeScreen = true) {
+ fun segmented_progress_indicator_on_off_disabled(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
SegmentedCircularProgressIndicator(
segmentCount = 6,
completed = { it % 2 == 0 },
@@ -332,35 +226,34 @@
enabled = false,
)
}
- }
+
+ @Test
+ fun progress_indicator_indeterminate(@TestParameter screenSize: ScreenSize) =
+ verifyProgressIndicatorScreenshot(screenSize = screenSize) {
+ CircularProgressIndicator(
+ modifier =
+ Modifier.size(
+ CircularProgressIndicatorDefaults.IndeterminateCircularIndicatorDiameter
+ )
+ .testTag(TEST_TAG),
+ )
+ }
private fun verifyProgressIndicatorScreenshot(
- isLargeScreen: Boolean = false,
+ screenSize: ScreenSize,
content: @Composable () -> Unit
) {
- val screenSizeDp = if (isLargeScreen) SCREEN_SIZE_LARGE else SCREEN_SIZE_SMALL
-
- rule.setContentWithTheme(modifier = Modifier.background(Color.Black)) {
- val originalConfiguration = LocalConfiguration.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSizeDp
- screenHeightDp = screenSizeDp
- }
+ rule.setContentWithTheme {
+ ScreenConfiguration(screenSize.size) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+ content()
}
-
- CompositionLocalProvider(
- LocalLayoutDirection provides LayoutDirection.Ltr,
- LocalConfiguration provides fixedScreenSizeConfiguration
- ) {
- Box(modifier = Modifier.size(screenSizeDp.dp).background(Color.Black)) { content() }
}
}
rule
.onNodeWithTag(TEST_TAG)
.captureToImage()
- .assertAgainstGolden(screenshotRule, testName.methodName)
+ .assertAgainstGolden(screenshotRule, testName.goldenIdentifier())
}
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
index 2165861..2031ed4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
@@ -77,7 +77,7 @@
fun contains_progress_color() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 1f },
colors =
ProgressIndicatorDefaults.colors(
@@ -100,7 +100,7 @@
fun contains_progress_incomplete_color() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 0f },
colors =
ProgressIndicatorDefaults.colors(
@@ -123,7 +123,7 @@
fun change_start_end_angle() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 0.5f },
startAngle = 0f,
endAngle = 180f,
@@ -152,7 +152,7 @@
fun set_small_progress_value() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 0.02f },
colors =
ProgressIndicatorDefaults.colors(
@@ -178,7 +178,7 @@
fun set_small_stroke_width() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.smallStrokeWidth,
colors =
@@ -204,7 +204,7 @@
fun set_large_stroke_width() {
setContentWithTheme {
CircularProgressIndicator(
- modifier = Modifier.testTag(TEST_TAG),
+ modifier = Modifier.size(SCREEN_SIZE_LARGE.dp).testTag(TEST_TAG),
progress = { 0.5f },
strokeWidth = CircularProgressIndicatorDefaults.largeStrokeWidth,
colors =
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
index 16b3d25..4b6a973 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
@@ -479,8 +479,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -496,8 +498,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -513,8 +517,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -530,8 +536,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -547,8 +555,10 @@
rule.setContentWithTheme {
RadioButtonWithDefaults(
selected = true,
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -564,8 +574,10 @@
rule.setContentWithTheme {
SplitRadioButtonWithDefaults(
selected = true,
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScaffoldTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScaffoldTest.kt
index ab8ec90..659d34c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScaffoldTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ScaffoldTest.kt
@@ -44,7 +44,9 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.filters.SdkSuppress
+import androidx.wear.compose.foundation.lazy.LazyColumn as WearLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.rememberLazyColumnState
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import com.google.common.truth.Truth.assertThat
import junit.framework.TestCase.assertEquals
@@ -123,6 +125,21 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
+ fun displays_scroll_indicator_initially_when_scrollable_lazycolumn() {
+ val scrollIndicatorColor = Color.Red
+
+ rule.setContentWithTheme {
+ TestScreenScaffoldWithLazyColumn(
+ scrollIndicatorColor = scrollIndicatorColor,
+ timeTextColor = Color.Blue
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(scrollIndicatorColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
fun hides_scroll_indicator_after_delay() {
val scrollIndicatorColor = Color.Red
@@ -146,6 +163,29 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
+ fun hides_scroll_indicator_after_delay_lazycolumn() {
+ val scrollIndicatorColor = Color.Red
+
+ rule.setContentWithTheme {
+ TestScreenScaffoldWithLazyColumn(
+ scrollIndicatorColor = scrollIndicatorColor,
+ timeTextColor = Color.Blue
+ )
+ }
+
+ // After a 2500 delay, the scroll indicator is animated away. Allow a little longer for the
+ // animation to complete.
+ rule.mainClock.autoAdvance = false
+ rule.mainClock.advanceTimeBy(4000)
+
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertDoesNotContainColor(scrollIndicatorColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
fun shows_time_text_after_delay() {
val timeTextColor = Color.Red
@@ -163,6 +203,28 @@
rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(timeTextColor)
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun shows_time_text_after_delay_lazycolumn() {
+ val timeTextColor = Color.Red
+
+ rule.setContentWithTheme {
+ TestScreenScaffoldWithLazyColumn(
+ scrollIndicatorColor = Color.Blue,
+ timeTextColor = timeTextColor
+ )
+ }
+
+ rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp(durationMillis = 10) }
+
+ // After a 2500 delay, the time text is animated back in. Allow a little longer for the
+ // animation to complete.
+ rule.mainClock.autoAdvance = false
+ rule.mainClock.advanceTimeBy(4000)
+
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(timeTextColor)
+ }
+
@Test
fun no_initial_room_for_bottom_button() {
var spaceAvailable: Int = Int.MAX_VALUE
@@ -183,6 +245,28 @@
}
@Test
+ fun no_initial_room_for_bottom_button_wear_lazy_column() {
+ var spaceAvailable: Int = Int.MAX_VALUE
+
+ rule.setContentWithTheme {
+ // Ensure we use the same size no mater where this is run.
+ Box(Modifier.size(300.dp)) {
+ TestScreenScaffoldWithLazyColumn(
+ scrollIndicatorColor = Color.Blue,
+ timeTextColor = Color.Red
+ ) {
+ BoxWithConstraints {
+ // Check how much space we have for the bottom button
+ spaceAvailable = constraints.maxHeight
+ }
+ }
+ }
+ }
+
+ assertEquals(0, spaceAvailable)
+ }
+
+ @Test
fun plenty_of_room_for_bottom_button_after_scroll() {
var spaceAvailable: Int = Int.MAX_VALUE
var expectedSpace = 0f
@@ -216,7 +300,7 @@
rule.setContentWithTheme {
// Ensure we use the same size no mater where this is run.
Box(Modifier.size(300.dp)) {
- TestBottomButtonLC() {
+ TestBottomButtonLC {
BoxWithConstraints {
// Check how much space we have for the bottom button
spaceAvailable = constraints.maxHeight
@@ -293,6 +377,43 @@
}
@Composable
+ private fun TestScreenScaffoldWithLazyColumn(
+ scrollIndicatorColor: Color,
+ timeTextColor: Color,
+ bottomButton: @Composable BoxScope.() -> Unit = {}
+ ) {
+ AppScaffold {
+ val scrollState = rememberLazyColumnState()
+ ScreenScaffold(
+ modifier = Modifier.testTag(TEST_TAG),
+ scrollState = scrollState,
+ scrollIndicator = {
+ Box(
+ modifier =
+ Modifier.size(20.dp)
+ .align(Alignment.CenterEnd)
+ .background(scrollIndicatorColor)
+ )
+ },
+ timeText = { Box(Modifier.size(20.dp).background(timeTextColor)) },
+ bottomButton = bottomButton
+ ) {
+ WearLazyColumn(
+ state = scrollState,
+ modifier = Modifier.fillMaxSize().background(Color.Black).testTag(SCROLL_TAG)
+ ) {
+ items(10) {
+ Button(
+ onClick = {},
+ label = { Text("Item ${it + 1}") },
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
private fun TestBottomButtonLC(
verticalPadding: Dp = 0.dp,
bottomButton: @Composable BoxScope.() -> Unit = {}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
index 47d4a80..2182465 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SegmentedCircularProgressIndicatorTest.kt
@@ -312,6 +312,98 @@
rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(Color.Green)
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun progress_overflow_contains_overflow_color() {
+ val customIndicatorColor = Color.Yellow
+ val customTrackColor = Color.Red
+ val customOverflowTrackColor = Color.Blue
+
+ setContentWithTheme {
+ SegmentedCircularProgressIndicator(
+ progress = { 1.5f },
+ segmentCount = 5,
+ modifier = Modifier.testTag(TEST_TAG),
+ colors =
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = customIndicatorColor,
+ trackColor = customTrackColor,
+ overflowTrackColor = customOverflowTrackColor
+ ),
+ allowProgressOverflow = true
+ )
+ }
+ rule.waitForIdle()
+ // When overflow is allowed then over-achieved (>100%) progress values the track should be
+ // in overflowTrackColor and the indicator should still be in indicatorColor.
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(customTrackColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIndicatorColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customOverflowTrackColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun progress_overflow_not_allowed_contains_only_indicator_color() {
+ val customIndicatorColor = Color.Yellow
+ val customTrackColor = Color.Red
+ val customOverflowTrackColor = Color.Blue
+
+ setContentWithTheme {
+ SegmentedCircularProgressIndicator(
+ progress = { 1.5f },
+ segmentCount = 5,
+ modifier = Modifier.testTag(TEST_TAG),
+ colors =
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = customIndicatorColor,
+ trackColor = customTrackColor,
+ overflowTrackColor = customOverflowTrackColor
+ ),
+ allowProgressOverflow = false
+ )
+ }
+ rule.waitForIdle()
+ // When progress overflow is disabled, then overflow progress values should be coerced to 1
+ // and overflowTrackColor should not appear, only customIndicatorColor.
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertDoesNotContainColor(customOverflowTrackColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(customTrackColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIndicatorColor)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun progress_overflow_200_percent_contains_only_indicator_color() {
+ val customIndicatorColor = Color.Yellow
+ val customTrackColor = Color.Red
+ val customOverflowTrackColor = Color.Blue
+
+ setContentWithTheme {
+ SegmentedCircularProgressIndicator(
+ progress = { 2.0f },
+ segmentCount = 5,
+ modifier = Modifier.testTag(TEST_TAG),
+ colors =
+ ProgressIndicatorDefaults.colors(
+ indicatorColor = customIndicatorColor,
+ trackColor = customTrackColor,
+ overflowTrackColor = customOverflowTrackColor,
+ ),
+ )
+ }
+ rule.waitForIdle()
+ // For 200% over-achieved progress the indicator should take the whole progress
+ // circle, just like for 100%.
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(customTrackColor)
+ rule
+ .onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertDoesNotContainColor(customOverflowTrackColor)
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(customIndicatorColor)
+ }
+
private fun setContentWithTheme(composable: @Composable BoxScope.() -> Unit) {
// Use constant size modifier to limit relative color percentage ranges.
rule.setContentWithTheme(modifier = Modifier.size(204.dp), composable = composable)
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
index e79d354..7300690 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SwitchButtonTest.kt
@@ -436,8 +436,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -454,8 +456,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelTextAlign = LocalTextAlign.current },
- secondaryLabel = { secondaryLabelTextAlign = LocalTextAlign.current },
+ label = { labelTextAlign = LocalTextConfiguration.current.textAlign },
+ secondaryLabel = {
+ secondaryLabelTextAlign = LocalTextConfiguration.current.textAlign
+ },
)
}
@@ -472,8 +476,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -490,8 +496,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelOverflow = LocalTextOverflow.current },
- secondaryLabel = { secondaryLabelOverflow = LocalTextOverflow.current },
+ label = { labelOverflow = LocalTextConfiguration.current.overflow },
+ secondaryLabel = {
+ secondaryLabelOverflow = LocalTextConfiguration.current.overflow
+ },
)
}
@@ -508,8 +516,10 @@
SwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
@@ -526,8 +536,10 @@
SplitSwitchButtonWithDefaults(
checked = true,
onCheckedChange = {},
- label = { labelMaxLines = LocalTextMaxLines.current },
- secondaryLabel = { secondaryLabelMaxLines = LocalTextMaxLines.current },
+ label = { labelMaxLines = LocalTextConfiguration.current.maxLines },
+ secondaryLabel = {
+ secondaryLabelMaxLines = LocalTextConfiguration.current.maxLines
+ },
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
index f130f2a..70ae3f4 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
@@ -418,7 +418,7 @@
status = Status.Enabled,
colors = { TextButtonDefaults.filledTonalTextButtonColors() },
expectedContainerColor = { MaterialTheme.colorScheme.surfaceContainer },
- expectedContentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
+ expectedContentColor = { MaterialTheme.colorScheme.onSurface }
)
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
index f4bcb12..738ce58 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextScreenshotTest.kt
@@ -36,11 +36,13 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
-import androidx.wear.compose.material3.LocalTextAlign
+import androidx.wear.compose.material3.LocalTextConfiguration
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
import androidx.wear.compose.material3.TEST_TAG
import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TextConfiguration
+import androidx.wear.compose.material3.TextConfigurationDefaults
import androidx.wear.compose.material3.setContentWithTheme
import org.junit.Rule
import org.junit.Test
@@ -62,12 +64,30 @@
@Test
fun text_align_follows_composition_local_center_alignment() = verifyScreenshot {
- CompositionLocalProvider(LocalTextAlign provides TextAlign.Center) { sampleText() }
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ overflow = TextConfigurationDefaults.Overflow,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
+ sampleText()
+ }
}
@Test
fun text_align_follows_composition_local_end_alignment() = verifyScreenshot {
- CompositionLocalProvider(LocalTextAlign provides TextAlign.End) { sampleText() }
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.End,
+ overflow = TextConfigurationDefaults.Overflow,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
+ sampleText()
+ }
}
@Composable
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
index 607d7d3e..b852a5b 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextTest.kt
@@ -266,7 +266,14 @@
"working otherwise it would not be a good test."
var result: TextLayoutResult? = null
rule.setContent {
- CompositionLocalProvider(LocalTextMaxLines provides maxLines) {
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ maxLines = maxLines,
+ textAlign = TextConfigurationDefaults.TextAlign,
+ overflow = TextConfigurationDefaults.Overflow,
+ )
+ ) {
Text(
text = text,
onTextLayout = { textLayoutResult -> result = textLayoutResult },
@@ -323,7 +330,14 @@
"working otherwise it would not be a good test."
var result: TextLayoutResult? = null
rule.setContent {
- CompositionLocalProvider(LocalTextOverflow provides TextOverflow.Ellipsis) {
+ CompositionLocalProvider(
+ LocalTextConfiguration provides
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextConfigurationDefaults.TextAlign,
+ maxLines = TextConfigurationDefaults.MaxLines,
+ )
+ ) {
Text(
text = text,
maxLines = 1,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonScreenshotTest.kt
index fb4421f..0d58261 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonScreenshotTest.kt
@@ -17,9 +17,14 @@
package androidx.wear.compose.material3
import android.os.Build
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
@@ -27,6 +32,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -82,17 +88,83 @@
content = { sampleTextToggleButton(modifier = Modifier.offset(10.dp)) }
)
+ @Ignore("TODO: b/345199060 work out how to show pressed state in test")
+ @Test
+ fun animatedTextToggleButtonPressed() =
+ rule.verifyScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ val interactionSource = remember {
+ MutableInteractionSource().apply {
+ tryEmit(PressInteraction.Press(Offset(0f, 0f)))
+ }
+ }
+ sampleTextToggleButton(
+ checked = false,
+ shape =
+ TextToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = false
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ )
+
+ @Test
+ fun animatedTextToggleButtonChecked() =
+ rule.verifyScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ val interactionSource = remember { MutableInteractionSource() }
+ sampleTextToggleButton(
+ checked = true,
+ shape =
+ TextToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = true
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ )
+
+ @Test
+ fun animatedTextToggleButtonUnchecked() =
+ rule.verifyScreenshot(
+ methodName = testName.methodName,
+ screenshotRule = screenshotRule,
+ content = {
+ val interactionSource = remember { MutableInteractionSource() }
+ sampleTextToggleButton(
+ checked = false,
+ shape =
+ TextToggleButtonDefaults.animatedToggleButtonShape(
+ interactionSource = interactionSource,
+ checked = false
+ ),
+ interactionSource = interactionSource
+ )
+ }
+ )
+
@Composable
private fun sampleTextToggleButton(
enabled: Boolean = true,
checked: Boolean = true,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ shape: Shape = TextButtonDefaults.shape,
+ interactionSource: MutableInteractionSource? = null
) {
TextToggleButton(
checked = checked,
onCheckedChange = {},
enabled = enabled,
- modifier = modifier.testTag(TEST_TAG)
+ modifier = modifier.testTag(TEST_TAG),
+ shape = shape,
+ interactionSource = interactionSource
) {
Text(text = if (checked) "ON" else "OFF")
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
index 166a54c..18123fe5 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextToggleButtonTest.kt
@@ -382,7 +382,7 @@
rule.verifyTextToggleButtonColors(
status = Status.Enabled,
checked = true,
- colors = { TextButtonDefaults.textToggleButtonColors() },
+ colors = { TextToggleButtonDefaults.textToggleButtonColors() },
containerColor = { MaterialTheme.colorScheme.primary },
contentColor = { MaterialTheme.colorScheme.onPrimary }
)
@@ -393,7 +393,7 @@
rule.verifyTextToggleButtonColors(
status = Status.Enabled,
checked = false,
- colors = { TextButtonDefaults.textToggleButtonColors() },
+ colors = { TextToggleButtonDefaults.textToggleButtonColors() },
containerColor = { MaterialTheme.colorScheme.surfaceContainer },
contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
)
@@ -404,7 +404,7 @@
rule.verifyTextToggleButtonColors(
status = Status.Disabled,
checked = false,
- colors = { TextButtonDefaults.textToggleButtonColors() },
+ colors = { TextToggleButtonDefaults.textToggleButtonColors() },
containerColor = {
MaterialTheme.colorScheme.onSurface.toDisabledColor(DisabledContainerAlpha)
},
@@ -417,7 +417,7 @@
rule.verifyTextToggleButtonColors(
status = Status.Disabled,
checked = true,
- colors = { TextButtonDefaults.textToggleButtonColors() },
+ colors = { TextToggleButtonDefaults.textToggleButtonColors() },
containerColor = {
MaterialTheme.colorScheme.onSurface.toDisabledColor(DisabledContainerAlpha)
},
@@ -433,7 +433,7 @@
status = Status.Enabled,
checked = true,
colors = {
- TextButtonDefaults.textToggleButtonColors(checkedContainerColor = override)
+ TextToggleButtonDefaults.textToggleButtonColors(checkedContainerColor = override)
},
containerColor = { override },
contentColor = { MaterialTheme.colorScheme.onPrimary }
@@ -448,7 +448,9 @@
rule.verifyTextToggleButtonColors(
status = Status.Enabled,
checked = true,
- colors = { TextButtonDefaults.textToggleButtonColors(checkedContentColor = override) },
+ colors = {
+ TextToggleButtonDefaults.textToggleButtonColors(checkedContentColor = override)
+ },
containerColor = { MaterialTheme.colorScheme.primary },
contentColor = { override }
)
@@ -463,7 +465,7 @@
status = Status.Enabled,
checked = false,
colors = {
- TextButtonDefaults.textToggleButtonColors(uncheckedContainerColor = override)
+ TextToggleButtonDefaults.textToggleButtonColors(uncheckedContainerColor = override)
},
containerColor = { override },
contentColor = { MaterialTheme.colorScheme.onSurfaceVariant }
@@ -479,7 +481,7 @@
status = Status.Enabled,
checked = false,
colors = {
- TextButtonDefaults.textToggleButtonColors(uncheckedContentColor = override)
+ TextToggleButtonDefaults.textToggleButtonColors(uncheckedContentColor = override)
},
containerColor = { MaterialTheme.colorScheme.surfaceContainer },
contentColor = { override }
@@ -495,7 +497,9 @@
status = Status.Disabled,
checked = true,
colors = {
- TextButtonDefaults.textToggleButtonColors(disabledCheckedContainerColor = override)
+ TextToggleButtonDefaults.textToggleButtonColors(
+ disabledCheckedContainerColor = override
+ )
},
containerColor = { override },
contentColor = { MaterialTheme.colorScheme.onSurface.toDisabledColor() }
@@ -511,7 +515,7 @@
status = Status.Disabled,
checked = true,
colors = {
- TextButtonDefaults.textToggleButtonColors(
+ TextToggleButtonDefaults.textToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledCheckedContentColor = override
)
@@ -532,7 +536,7 @@
status = Status.Disabled,
checked = false,
colors = {
- TextButtonDefaults.textToggleButtonColors(
+ TextToggleButtonDefaults.textToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledUncheckedContainerColor = override
)
@@ -551,7 +555,7 @@
status = Status.Disabled,
checked = false,
colors = {
- TextButtonDefaults.textToggleButtonColors(
+ TextToggleButtonDefaults.textToggleButtonColors(
// Apply the content color override for the content alpha to be applied
disabledUncheckedContentColor = override
)
@@ -602,7 +606,7 @@
@Composable
private fun shapeColor(): Color {
- return TextButtonDefaults.textToggleButtonColors()
+ return TextToggleButtonDefaults.textToggleButtonColors()
.containerColor(enabled = true, checked = true)
.value
}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt
index ed1aaf9..3511aa3 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TimePickerScreenshotTest.kt
@@ -16,24 +16,16 @@
package androidx.wear.compose.material3
-import android.content.res.Configuration
import android.os.Build
import androidx.annotation.RequiresApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
@@ -155,30 +147,9 @@
isLargeScreen: Boolean = false,
content: @Composable () -> Unit
) {
- val screenSizeDp = if (isLargeScreen) SCREENSHOT_SIZE_LARGE else SCREENSHOT_SIZE
- setContentWithTheme {
- val originalConfiguration = LocalConfiguration.current
- val fixedScreenSizeConfiguration =
- remember(originalConfiguration) {
- Configuration(originalConfiguration).apply {
- screenWidthDp = screenSizeDp
- screenHeightDp = screenSizeDp
- }
- }
- CompositionLocalProvider(LocalConfiguration provides fixedScreenSizeConfiguration) {
- Box(
- modifier =
- Modifier.size(screenSizeDp.dp)
- .background(MaterialTheme.colorScheme.background)
- ) {
- content()
- }
- }
- }
+ val screenSizeDp = if (isLargeScreen) SCREEN_SIZE_LARGE else SCREEN_SIZE_SMALL
+ setContentWithTheme { ScreenConfiguration(screenSizeDp) { content() } }
onNodeWithTag(testTag).captureToImage().assertAgainstGolden(screenshotRule, methodName)
}
}
-
-private const val SCREENSHOT_SIZE = 192
-private const val SCREENSHOT_SIZE_LARGE = 228
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index f0cc000..5c2bdd2 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
@@ -338,7 +339,7 @@
alertButtonsParams: AlertButtonsParams,
content: (ScalingLazyListScope.() -> Unit)?
) {
- val state = rememberScalingLazyListState()
+ val state = rememberScalingLazyListState(initialCenterItemIndex = 0)
Dialog(
showDialog = show,
@@ -401,8 +402,12 @@
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
LocalTextStyle provides MaterialTheme.typography.titleMedium,
- LocalTextAlign provides TextAlign.Center,
- LocalTextMaxLines provides AlertDialogDefaults.titleMaxLines,
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ maxLines = AlertDialogDefaults.titleMaxLines,
+ overflow = TextOverflow.Ellipsis
+ ),
content = content
)
}
@@ -433,7 +438,12 @@
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
LocalTextStyle provides MaterialTheme.typography.bodyMedium,
- LocalTextAlign provides TextAlign.Center,
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = TextConfigurationDefaults.MaxLines
+ ),
content = content
)
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimatedToggleRoundedCornerShape.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimatedToggleRoundedCornerShape.kt
new file mode 100644
index 0000000..f660f8f
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimatedToggleRoundedCornerShape.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * A animated [RoundedCornerShape]. Animation is driven by changes to the [cornerSize] lambda.
+ * [currentShapeSize] is provided as Size is received here, but must affect the animation.
+ *
+ * @param currentShapeSize MutableState coordinating the current size.
+ * @param cornerSize a lambda resolving to the current Corner size.
+ */
+@Stable
+internal class AnimatedToggleRoundedCornerShape(
+ private val currentShapeSize: MutableState<Size?>,
+ private val cornerSize: () -> CornerSize,
+) : Shape {
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density,
+ ): Outline {
+ val cornerRadius = cornerSize().toPx(size, density)
+
+ currentShapeSize.value = size
+
+ return Outline.Rounded(
+ roundRect =
+ RoundRect(rect = size.toRect(), radiusX = cornerRadius, radiusY = cornerRadius)
+ )
+ }
+}
+
+/**
+ * Returns a Shape that will internally animate between the unchecked, checked and pressed shape as
+ * the button is pressed and checked/unchecked.
+ */
+@Composable
+internal fun rememberAnimatedToggleRoundedCornerShape(
+ uncheckedCornerSize: CornerSize,
+ checkedCornerSize: CornerSize,
+ pressedCornerSize: CornerSize,
+ pressed: Boolean,
+ checked: Boolean,
+ onPressAnimationSpec: FiniteAnimationSpec<Float>,
+ onReleaseAnimationSpec: FiniteAnimationSpec<Float>,
+): Shape {
+ val toggleState =
+ when {
+ pressed -> ToggleState.Pressed
+ checked -> ToggleState.Checked
+ else -> ToggleState.Unchecked
+ }
+
+ val transition = updateTransition(toggleState, label = "Toggle State")
+ val density = LocalDensity.current
+
+ val currentShapeSize = remember { mutableStateOf<Size?>(null) }
+
+ val observedSize = currentShapeSize.value
+
+ if (observedSize != null) {
+ val sizePx =
+ transition.animateFloat(
+ label = "Corner Size",
+ transitionSpec = {
+ when {
+ targetState isTransitioningTo ToggleState.Pressed -> onPressAnimationSpec
+ else -> onReleaseAnimationSpec
+ }
+ },
+ ) { newState ->
+ newState
+ .cornerSize(uncheckedCornerSize, checkedCornerSize, pressedCornerSize)
+ .toPx(observedSize, density)
+ }
+
+ return remember(sizePx) {
+ AnimatedToggleRoundedCornerShape(
+ currentShapeSize = currentShapeSize,
+ ) {
+ CornerSize(sizePx.value)
+ }
+ }
+ } else {
+ return remember(toggleState, uncheckedCornerSize, checkedCornerSize, pressedCornerSize) {
+ AnimatedToggleRoundedCornerShape(
+ currentShapeSize = currentShapeSize,
+ ) {
+ toggleState.cornerSize(
+ uncheckedCornerSize,
+ checkedCornerSize,
+ pressedCornerSize,
+ )
+ }
+ }
+ }
+}
+
+private fun ToggleState.cornerSize(
+ uncheckedCornerSize: CornerSize,
+ checkedCornerSize: CornerSize,
+ pressedCornerSize: CornerSize,
+) =
+ when (this) {
+ ToggleState.Unchecked -> uncheckedCornerSize
+ ToggleState.Checked -> checkedCornerSize
+ ToggleState.Pressed -> pressedCornerSize
+ }
+
+internal enum class ToggleState {
+ Unchecked,
+ Checked,
+ Pressed,
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 0cf422b..1fddec4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -92,10 +92,6 @@
* Example of a [Button]:
*
* @sample androidx.wear.compose.material3.samples.SimpleButtonSample
- *
- * Example of a [Button] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.ButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -175,10 +171,6 @@
* Example of a [FilledTonalButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleFilledTonalButtonSample
- *
- * Example of a [FilledTonalButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.FilledTonalButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -257,10 +249,6 @@
* Example of an [OutlinedButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleOutlinedButtonSample
- *
- * Example of a [OutlinedButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.OutlinedButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -339,10 +327,6 @@
* Example of a [ChildButton]:
*
* @sample androidx.wear.compose.material3.samples.SimpleChildButtonSample
- *
- * Example of a [ChildButton] with onLongClick:
- *
- * @sample androidx.wear.compose.material3.samples.ChildButtonWithOnLongClickSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -423,6 +407,14 @@
* Example of a [Button] with an icon and secondary label:
*
* @sample androidx.wear.compose.material3.samples.ButtonSample
+ *
+ * Example of a [Button] with a large icon and adjusted content padding:
+ *
+ * @sample androidx.wear.compose.material3.samples.ButtonLargeIconSample
+ *
+ * Example of a [Button] with an extra large icon and adjusted content padding:
+ *
+ * @sample androidx.wear.compose.material3.samples.ButtonExtraLargeIconSample
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param onLongClick Called when this button is long clicked (long-pressed). When this callback is
@@ -479,9 +471,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2
+ ),
content = secondaryLabel
),
icon = icon,
@@ -496,11 +491,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3
+ ),
content = label
)
)
@@ -594,9 +592,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -611,11 +612,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -704,9 +708,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -721,11 +728,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -814,9 +824,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled),
textStyle = FilledButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
icon = icon,
@@ -831,11 +844,14 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = FilledButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign =
- if (icon != null || secondaryLabel != null) TextAlign.Start
- else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign =
+ if (icon != null || secondaryLabel != null) TextAlign.Start
+ else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
)
)
@@ -957,9 +973,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled),
textStyle = CompactButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- textAlign = if (icon != null) TextAlign.Start else TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = if (icon != null) TextAlign.Start else TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ ),
label
)
)
@@ -1216,7 +1235,7 @@
/**
* Creates a [ButtonColors] for a [Button] with an image background, typically with a scrim over
* the image to ensure that the content is visible. Uses a default content color of
- * [ColorScheme.onSurface].
+ * [ColorScheme.onBackground].
*
* @param backgroundImagePainter The [Painter] to use to draw the background of the [Button]
* @param backgroundImageScrimBrush The [Brush] to use to paint a scrim over the background
@@ -1249,7 +1268,10 @@
)
),
contentColor: Color = ImageButtonTokens.ContentColor.value,
- secondaryContentColor: Color = ImageButtonTokens.SecondaryContentColor.value,
+ secondaryContentColor: Color =
+ ImageButtonTokens.SecondaryContentColor.value.copy(
+ alpha = ImageButtonTokens.SecondaryContentOpacity
+ ),
iconColor: Color = ImageButtonTokens.IconColor.value,
disabledContentColor: Color =
ImageButtonTokens.DisabledContentColor.value.toDisabledColor(
@@ -1274,13 +1296,13 @@
)
}
- val disabledContentAlpha = ImageButtonTokens.DisabledContentOpacity
+ val disabledContainerAlpha = ImageButtonTokens.DisabledContainerOpacity
val disabledBackgroundPainter =
- remember(backgroundImagePainter, backgroundImageScrimBrush, disabledContentAlpha) {
+ remember(backgroundImagePainter, backgroundImageScrimBrush, disabledContainerAlpha) {
androidx.wear.compose.materialcore.ImageWithScrimPainter(
imagePainter = backgroundImagePainter,
brush = backgroundImageScrimBrush,
- alpha = disabledContentAlpha,
+ alpha = disabledContainerAlpha,
forcedSize = forcedSize,
)
}
@@ -1363,7 +1385,16 @@
disabledIconColor = disabledIconColor
)
+ /** The recommended horizontal padding used by [Button] by default */
val ButtonHorizontalPadding = 14.dp
+
+ /** The recommended start padding to be used with [Button] with a large icon */
+ val ButtonLargeIconStartPadding = 12.dp
+
+ /** The recommended start padding to be used with [Button] with an extra large icon */
+ val ButtonExtraLargeIconStartPadding = 8.dp
+
+ /** The recommended vertical padding used by [Button] by default */
val ButtonVerticalPadding = 6.dp
/** The default content padding used by [Button] */
@@ -1373,12 +1404,36 @@
vertical = ButtonVerticalPadding,
)
+ /** The default content padding used by [Button] with a large icon */
+ val ButtonWithLargeIconContentPadding: PaddingValues =
+ PaddingValues(
+ start = ButtonLargeIconStartPadding,
+ top = ButtonVerticalPadding,
+ end = ButtonHorizontalPadding,
+ bottom = ButtonVerticalPadding
+ )
+
+ /** The default content padding used by [Button] with an extra large icon */
+ val ButtonWithExtraLargeIconContentPadding: PaddingValues =
+ PaddingValues(
+ start = ButtonExtraLargeIconStartPadding,
+ top = ButtonVerticalPadding,
+ end = ButtonHorizontalPadding,
+ bottom = ButtonVerticalPadding
+ )
+
+ /** The size of the icon when used inside a "[CompactButton]. */
+ val SmallIconSize: Dp = CompactButtonTokens.IconSize
+
/** The default size of the icon when used inside a [Button]. */
val IconSize: Dp = FilledButtonTokens.IconSize
- /** The size of the icon when used inside a Large "Avatar" [Button]. */
+ /** The recommended icon size when used in [Button]s for icons such as an app icon */
val LargeIconSize: Dp = FilledButtonTokens.IconLargeSize
+ /** The recommended icon size when used in [Button]s for icons such as an avatar icon */
+ val ExtraLargeIconSize: Dp = FilledButtonTokens.IconExtraLargeSize
+
/**
* The default height applied for the [Button]. Note that you can override it by applying
* Modifier.heightIn directly on [Button].
@@ -1419,9 +1474,6 @@
/** The height to be applied for a large [EdgeButton]. */
val EdgeButtonHeightLarge = 96.dp
- /** The size of the icon when used inside a "[CompactButton]. */
- val SmallIconSize: Dp = CompactButtonTokens.IconSize
-
/**
* The default padding to be provided around a [CompactButton] in order to ensure that its
* tappable area meets minimum UX guidance.
@@ -1778,8 +1830,12 @@
val borderModifier =
if (border != null) modifier.border(border = border, shape = shape) else modifier
Row(
+ verticalAlignment = Alignment.CenterVertically,
+ // Fill the container height but not its width as buttons have fixed size height but we
+ // want them to be able to fit their content
modifier =
borderModifier
+ .fillMaxHeight()
.clip(shape = shape)
.width(intrinsicSize = IntrinsicSize.Max)
.paint(
@@ -1834,25 +1890,18 @@
contentPadding = contentPadding,
interactionSource = interactionSource,
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- // Fill the container height but not its width as buttons have fixed size height but we
- // want them to be able to fit their content
- modifier = Modifier.fillMaxHeight()
- ) {
- if (icon != null) {
- Box(
- modifier = Modifier.wrapContentSize(align = Alignment.Center),
- content = provideScopeContent(colors.iconColor(enabled), icon)
- )
- Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
- }
- Column {
- Row(content = labelContent)
- if (secondaryLabelContent != null) {
- Spacer(modifier = Modifier.size(2.dp))
- Row(content = secondaryLabelContent)
- }
+ if (icon != null) {
+ Box(
+ modifier = Modifier.wrapContentSize(align = Alignment.Center),
+ content = provideScopeContent(colors.iconColor(enabled), icon)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ }
+ Column {
+ Row(content = labelContent)
+ if (secondaryLabelContent != null) {
+ Spacer(modifier = Modifier.size(2.dp))
+ Row(content = secondaryLabelContent)
}
}
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
index e05a9f2..722f667 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ButtonGroup.kt
@@ -30,7 +30,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -38,6 +37,7 @@
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexed
+import androidx.wear.compose.materialcore.screenHeightDp
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.collectLatest
@@ -46,7 +46,7 @@
import kotlinx.coroutines.launch
/** Scope for the children of a [ButtonGroup] */
-public class ButtonGroupScope {
+class ButtonGroupScope {
internal val items = mutableListOf<ButtonGroupItem>()
/**
@@ -225,7 +225,7 @@
*/
@Composable
fun fullWidthPaddings(): PaddingValues {
- val screenHeight = LocalConfiguration.current.screenHeightDp.dp
+ val screenHeight = screenHeightDp().dp
return PaddingValues(
horizontal = screenHeight * FullWidthHorizontalPaddingPercentage / 100,
vertical = 0.dp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
index 119ca2e..0d92dfa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CheckboxButton.kt
@@ -130,9 +130,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked),
textStyle = CheckboxButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
),
toggleControl = {
@@ -158,9 +161,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled = enabled, checked),
textStyle = CheckboxButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ ),
content = secondaryLabel
),
background = { isEnabled, isChecked ->
@@ -286,9 +292,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked = checked),
textStyle = SplitCheckboxButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ ),
content = label
),
secondaryLabel =
@@ -296,9 +305,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, checked = checked),
textStyle = SplitCheckboxButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
index e35b8fb..507df53 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/CircularProgressIndicator.kt
@@ -16,18 +16,34 @@
package androidx.wear.compose.material3
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.rotate
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material3.tokens.MotionTokens
import androidx.wear.compose.materialcore.isSmallScreen
+import kotlin.math.PI
import kotlin.math.asin
import kotlin.math.max
import kotlin.math.min
@@ -55,8 +71,13 @@
* Progress indicators express the proportion of completion of an ongoing task.
*
* @param progress The progress of this progress indicator where 0.0 represents no progress and 1.0
- * represents completion. Values outside of this range are coerced into the range 0..1.
+ * represents completion.
* @param modifier Modifier to be applied to the CircularProgressIndicator.
+ * @param allowProgressOverflow When progress overflow is allowed, values smaller than 0.0 will be
+ * coerced to 0, while values larger than 1.0 will be wrapped around and shown as overflow with a
+ * different track color [ProgressIndicatorColors.overflowTrackBrush]. For example values 1.2, 2.2
+ * etc will be shown as 20% progress with the overflow color. When progress overflow is not
+ * allowed, progress values will be coerced into the range 0..1.
* @param startAngle The starting position of the progress arc, measured clockwise in degrees (0
* to 360) from the 3 o'clock position. For example, 0 and 360 represent 3 o'clock, 90 and 180
* represent 6 o'clock and 9 o'clock respectively. Default is 270 degrees
@@ -79,6 +100,7 @@
fun CircularProgressIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
+ allowProgressOverflow: Boolean = false,
startAngle: Float = CircularProgressIndicatorDefaults.StartAngle,
endAngle: Float = startAngle,
colors: ProgressIndicatorColors = ProgressIndicatorDefaults.colors(),
@@ -86,7 +108,6 @@
gapSize: Dp = CircularProgressIndicatorDefaults.calculateRecommendedGapSize(strokeWidth),
enabled: Boolean = true,
) {
- val coercedProgress = { progress().coerceIn(0f, 1f) }
// Canvas internally uses Spacer.drawBehind.
// Using Spacer.drawWithCache to optimize the stroke allocations.
Spacer(
@@ -95,8 +116,11 @@
.fillMaxSize()
.focusable()
.drawWithCache {
+ val currentProgress = progress()
+ val coercedProgress = coerceProgress(currentProgress, allowProgressOverflow)
val fullSweep = 360f - ((startAngle - endAngle) % 360 + 360) % 360
- var progressSweep = fullSweep * coercedProgress()
+ var progressSweep = fullSweep * coercedProgress
+ val hasOverflow = allowProgressOverflow && currentProgress > 1.0f
val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
val minSize = min(size.height, size.width)
// Sweep angle between two progress indicator segments.
@@ -123,7 +147,7 @@
startAngle = startAngle + progressSweep,
sweep = fullSweep - progressSweep,
gapSweep = gapSweep,
- brush = colors.trackBrush(enabled),
+ brush = colors.trackBrush(enabled, hasOverflow),
stroke = stroke
)
}
@@ -131,6 +155,77 @@
)
}
+/**
+ * Indeterminate Material Design circular progress indicator.
+ *
+ * Indeterminate progress indicator expresses an unspecified wait time and spins indefinitely.
+ *
+ * Example of indeterminate progress indicator:
+ *
+ * @sample androidx.wear.compose.material3.samples.IndeterminateProgressIndicatorSample
+ * @param modifier Modifier to be applied to the CircularProgressIndicator.
+ * @param colors [ProgressIndicatorColors] that will be used to resolve the indicator and track
+ * color for this progress indicator.
+ * @param strokeWidth The stroke width for the progress indicator. The recommended values is
+ * [CircularProgressIndicatorDefaults.IndeterminateStrokeWidth].
+ * @param gapSize The size (in Dp) of the gap between the ends of the progress indicator and the
+ * track. The stroke endcaps are not included in this distance.
+ */
+@Composable
+fun CircularProgressIndicator(
+ modifier: Modifier = Modifier,
+ colors: ProgressIndicatorColors = ProgressIndicatorDefaults.colors(),
+ strokeWidth: Dp = CircularProgressIndicatorDefaults.IndeterminateStrokeWidth,
+ gapSize: Dp = CircularProgressIndicatorDefaults.calculateRecommendedGapSize(strokeWidth),
+) {
+ val stroke =
+ with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) }
+
+ val infiniteTransition = rememberInfiniteTransition()
+ // A global rotation that does a 360 degrees rotation in 6 seconds.
+ val globalRotation =
+ infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = CircularGlobalRotationDegreesTarget,
+ animationSpec = circularIndeterminateGlobalRotationAnimationSpec
+ )
+
+ // An additional rotation that moves by 90 degrees in 500ms and then rest for 1 second.
+ val additionalRotation =
+ infiniteTransition.animateFloat(
+ initialValue = 0f,
+ targetValue = CircularAdditionalRotationDegreesTarget,
+ animationSpec = circularIndeterminateRotationAnimationSpec
+ )
+
+ // Indicator progress animation that will be changing the progress up and down as the indicator
+ // rotates.
+ val progressAnimation =
+ infiniteTransition.animateFloat(
+ initialValue = CircularIndeterminateMinProgress,
+ targetValue = CircularIndeterminateMaxProgress,
+ animationSpec = circularIndeterminateProgressAnimationSpec
+ )
+
+ Canvas(
+ modifier.size(CircularProgressIndicatorDefaults.IndeterminateCircularIndicatorDiameter)
+ ) {
+ val sweep = progressAnimation.value * 360f
+ val adjustedGapSize = gapSize + strokeWidth
+ val gapSizeSweep = (adjustedGapSize.value / (PI * size.width.toDp().value).toFloat()) * 360f
+
+ rotate(globalRotation.value + additionalRotation.value) {
+ drawCircularIndicator(
+ sweep + min(sweep, gapSizeSweep),
+ 360f - sweep - min(sweep, gapSizeSweep) * 2,
+ colors.trackBrush,
+ stroke
+ )
+ drawCircularIndicator(startAngle = 0f, sweep, colors.indicatorBrush, stroke)
+ }
+ }
+}
+
/** Contains default values for [CircularProgressIndicator]. */
object CircularProgressIndicatorDefaults {
/** Large stroke width for circular progress indicator. */
@@ -157,4 +252,93 @@
/** Padding used for displaying [CircularProgressIndicator] full screen. */
val FullScreenPadding = PaddingDefaults.edgePadding
+
+ /** Diameter of the indicator circle for indeterminate progress. */
+ internal val IndeterminateCircularIndicatorDiameter = 24.dp
+
+ /** Default stroke width for indeterminate [CircularProgressIndicator]. */
+ val IndeterminateStrokeWidth = 3.dp
}
+
+private fun DrawScope.drawCircularIndicator(
+ startAngle: Float,
+ sweep: Float,
+ brush: Brush,
+ stroke: Stroke
+) {
+ // To draw this circle we need a rect with edges that line up with the midpoint of the stroke.
+ // To do this we need to remove half the stroke width from the total diameter for both sides.
+ val diameterOffset = stroke.width / 2
+ val arcDimen = size.width - 2 * diameterOffset
+ drawArc(
+ brush = brush,
+ startAngle = startAngle,
+ sweepAngle = sweep,
+ useCenter = false,
+ topLeft = Offset(diameterOffset, diameterOffset),
+ size = Size(arcDimen, arcDimen),
+ style = stroke
+ )
+}
+
+/** A global animation spec for indeterminate circular progress indicator. */
+internal val circularIndeterminateGlobalRotationAnimationSpec
+ get() =
+ infiniteRepeatable<Float>(
+ animation = tween(CircularAnimationProgressDuration, easing = LinearEasing)
+ )
+
+/**
+ * An animation spec for indeterminate circular progress indicators that infinitely rotates a 360
+ * degrees.
+ */
+internal val circularIndeterminateRotationAnimationSpec
+ get() =
+ infiniteRepeatable(
+ animation =
+ keyframes {
+ durationMillis = CircularAnimationProgressDuration // 6000ms
+ 90f at
+ CircularAnimationAdditionalRotationDuration using
+ MotionTokens
+ .EasingEmphasizedDecelerate // MotionTokens.EasingEmphasizedDecelerateCubicBezier // 300ms
+ 90f at CircularAnimationAdditionalRotationDelay // hold till 1500ms
+ 180f at
+ CircularAnimationAdditionalRotationDuration +
+ CircularAnimationAdditionalRotationDelay // 1800ms
+ 180f at CircularAnimationAdditionalRotationDelay * 2 // hold till 3000ms
+ 270f at
+ CircularAnimationAdditionalRotationDuration +
+ CircularAnimationAdditionalRotationDelay * 2 // 3300ms
+ 270f at CircularAnimationAdditionalRotationDelay * 3 // hold till 4500ms
+ 360f at
+ CircularAnimationAdditionalRotationDuration +
+ CircularAnimationAdditionalRotationDelay * 3 // 4800ms
+ 360f at CircularAnimationProgressDuration // hold till 6000ms
+ }
+ )
+
+/** An animation spec for indeterminate circular progress indicators progress motion. */
+internal val circularIndeterminateProgressAnimationSpec
+ get() =
+ infiniteRepeatable(
+ animation =
+ keyframes {
+ durationMillis = CircularAnimationProgressDuration // 6000ms
+ CircularIndeterminateMaxProgress at
+ CircularAnimationProgressDuration / 2 using
+ CircularProgressEasing // 3000ms
+ CircularIndeterminateMinProgress at CircularAnimationProgressDuration
+ }
+ )
+
+// The indeterminate circular indicator easing constants for its motion
+internal val CircularProgressEasing = MotionTokens.EasingStandard
+internal const val CircularIndeterminateMinProgress = 0.1f
+internal const val CircularIndeterminateMaxProgress = 0.87f
+
+internal const val CircularAnimationProgressDuration = 6000
+internal const val CircularAnimationAdditionalRotationDelay = 1500
+internal const val CircularAnimationAdditionalRotationDuration = 300
+internal const val CircularAdditionalRotationDegreesTarget = 360f
+internal const val CircularGlobalRotationDegreesTarget = 1080f
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
index 0a94d14..502cafaa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
@@ -45,6 +45,7 @@
import androidx.compose.ui.platform.LocalAccessibilityManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.wear.compose.foundation.CurvedDirection
@@ -185,8 +186,12 @@
CompositionLocalProvider(
LocalContentColor provides colors.textColor,
LocalTextStyle provides MaterialTheme.typography.titleMedium,
- LocalTextAlign provides TextAlign.Center,
- LocalTextMaxLines provides ConfirmationDefaults.LinearContentMaxLines
+ LocalTextConfiguration provides
+ TextConfiguration(
+ textAlign = TextAlign.Center,
+ maxLines = ConfirmationDefaults.LinearContentMaxLines,
+ overflow = TextOverflow.Ellipsis
+ ),
) {
if (text != null) {
Spacer(Modifier.height(ConfirmationDefaults.LinearContentSpacing))
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
index faccf84..469df92 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
@@ -55,7 +55,6 @@
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextAlign
@@ -70,6 +69,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.lerp
+import androidx.wear.compose.materialcore.screenWidthDp
import kotlin.math.roundToInt
import kotlin.math.sqrt
@@ -140,7 +140,7 @@
val easing = CubicBezierEasing(0.25f, 0f, 0.75f, 1.0f)
val density = LocalDensity.current
- val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
+ val screenWidthDp = screenWidthDp().dp
val contentShapeHelper =
remember(buttonHeight) {
@@ -221,7 +221,7 @@
val alpha =
easing
.transform(
- (height - contentFadeEndPx).toFloat() /
+ (height - contentFadeEndPx) /
((contentFadeStartPx - contentFadeEndPx))
)
.coerceIn(0f, 1f)
@@ -258,9 +258,12 @@
provideScopeContent(
colors.contentColor(enabled = enabled),
MaterialTheme.typography.labelMedium,
- TextOverflow.Ellipsis,
- maxLines = 3, // TODO(): Change according to buttonHeight
- TextAlign.Center,
+ textConfiguration =
+ TextConfiguration(
+ TextAlign.Center,
+ TextOverflow.Ellipsis,
+ maxLines = 3, // TODO(): Change according to buttonHeight
+ ),
content
)
)
@@ -306,8 +309,6 @@
fun contentWidthDp() = with(density) { contentWindow.width.toDp() }
- fun contentHeightDp() = with(density) { contentWindow.height.toDp() }
-
fun update(size: Size) {
lastSize.value = size
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
index 1d4e8e4..da21502 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
@@ -17,14 +17,17 @@
package androidx.wear.compose.material3
import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -352,9 +355,14 @@
* [IconToggleButton] can be enabled or disabled. A disabled button will not respond to click
* events. When enabled, the checked and unchecked events are propagated by [onCheckedChange].
*
- * A simple icon toggle button using the default colors
+ * A simple icon toggle button using the default colors, animated when pressed.
*
* @sample androidx.wear.compose.material3.samples.IconToggleButtonSample
+ *
+ * A simple icon toggle button using the default colors, animated when pressed and with different
+ * shapes for the checked and unchecked states.
+ *
+ * @sample androidx.wear.compose.material3.samples.IconToggleButtonVariantSample
* @param checked Boolean flag indicating whether this toggle button is currently checked.
* @param onCheckedChange Callback to be invoked when this toggle button is clicked.
* @param modifier Modifier to be applied to the toggle button.
@@ -377,7 +385,7 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
+ colors: IconToggleButtonColors = IconToggleButtonDefaults.iconToggleButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = IconButtonDefaults.shape,
border: BorderStroke? = null,
@@ -417,6 +425,13 @@
/**
* Creates a [Shape] with a animation between two CornerBasedShapes.
*
+ * A simple icon button using the default colors, animated when pressed.
+ *
+ * @sample androidx.wear.compose.material3.samples.IconButtonWithCornerAnimationSample
+ *
+ * A simple icon toggle button using the default colors, animated when pressed.
+ *
+ * @sample androidx.wear.compose.material3.samples.IconToggleButtonSample
* @param interactionSource the interaction source applied to the Button.
* @param shape The normal shape of the IconButton.
* @param pressedShape The pressed shape of the IconButton.
@@ -611,62 +626,6 @@
)
/**
- * Creates an [IconToggleButtonColors] for a [IconToggleButton]
- * - by default, a colored background with a contrasting content color.
- *
- * If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
- * [DisabledContainerAlpha]) value applied.
- */
- @Composable
- fun iconToggleButtonColors() = MaterialTheme.colorScheme.defaultIconToggleButtonColors
-
- /**
- * Creates a [IconToggleButtonColors] for a [IconToggleButton]
- * - by default, a colored background with a contrasting content color.
- *
- * If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
- * [DisabledContainerAlpha]) value applied.
- *
- * @param checkedContainerColor The container color of this [IconToggleButton] when enabled and
- * checked
- * @param checkedContentColor The content color of this [IconToggleButton] when enabled and
- * checked
- * @param uncheckedContainerColor The container color of this [IconToggleButton] when enabled
- * and unchecked
- * @param uncheckedContentColor The content color of this [IconToggleButton] when enabled and
- * unchecked
- * @param disabledCheckedContainerColor The container color of this [IconToggleButton] when
- * checked and not enabled
- * @param disabledCheckedContentColor The content color of this [IconToggleButton] when checked
- * and not enabled
- * @param disabledUncheckedContainerColor The container color of this [IconToggleButton] when
- * unchecked and not enabled
- * @param disabledUncheckedContentColor The content color of this [IconToggleButton] when
- * unchecked and not enabled
- */
- @Composable
- fun iconToggleButtonColors(
- checkedContainerColor: Color = Color.Unspecified,
- checkedContentColor: Color = Color.Unspecified,
- uncheckedContainerColor: Color = Color.Unspecified,
- uncheckedContentColor: Color = Color.Unspecified,
- disabledCheckedContainerColor: Color = Color.Unspecified,
- disabledCheckedContentColor: Color = Color.Unspecified,
- disabledUncheckedContainerColor: Color = Color.Unspecified,
- disabledUncheckedContentColor: Color = Color.Unspecified,
- ): IconToggleButtonColors =
- MaterialTheme.colorScheme.defaultIconToggleButtonColors.copy(
- checkedContainerColor = checkedContainerColor,
- checkedContentColor = checkedContentColor,
- uncheckedContainerColor = uncheckedContainerColor,
- uncheckedContentColor = uncheckedContentColor,
- disabledCheckedContainerColor = disabledCheckedContainerColor,
- disabledCheckedContentColor = disabledCheckedContentColor,
- disabledUncheckedContainerColor = disabledUncheckedContainerColor,
- disabledUncheckedContentColor = disabledUncheckedContentColor,
- )
-
- /**
* The recommended size of an icon when used inside an icon button with size [SmallButtonSize]
* or [ExtraSmallButtonSize]. Use [iconSizeFor] to easily determine the icon size.
*/
@@ -801,45 +760,6 @@
)
.also { defaultIconButtonColorsCached = it }
}
-
- private val ColorScheme.defaultIconToggleButtonColors: IconToggleButtonColors
- get() {
- return defaultIconToggleButtonColorsCached
- ?: IconToggleButtonColors(
- checkedContainerColor =
- fromToken(IconToggleButtonTokens.CheckedContainerColor),
- checkedContentColor = fromToken(IconToggleButtonTokens.CheckedContentColor),
- uncheckedContainerColor =
- fromToken(IconToggleButtonTokens.UncheckedContainerColor),
- uncheckedContentColor =
- fromToken(IconToggleButtonTokens.UncheckedContentColor),
- disabledCheckedContainerColor =
- fromToken(IconToggleButtonTokens.DisabledCheckedContainerColor)
- .toDisabledColor(
- disabledAlpha =
- IconToggleButtonTokens.DisabledCheckedContainerOpacity
- ),
- disabledCheckedContentColor =
- fromToken(IconToggleButtonTokens.DisabledCheckedContentColor)
- .toDisabledColor(
- disabledAlpha =
- IconToggleButtonTokens.DisabledCheckedContentOpacity
- ),
- disabledUncheckedContainerColor =
- fromToken(IconToggleButtonTokens.DisabledUncheckedContainerColor)
- .toDisabledColor(
- disabledAlpha =
- IconToggleButtonTokens.DisabledUncheckedContainerOpacity
- ),
- disabledUncheckedContentColor =
- fromToken(IconToggleButtonTokens.DisabledUncheckedContentColor)
- .toDisabledColor(
- disabledAlpha =
- IconToggleButtonTokens.DisabledUncheckedContentOpacity
- ),
- )
- .also { defaultIconToggleButtonColorsCached = it }
- }
}
/**
@@ -920,6 +840,154 @@
}
}
+/** Contains the default values used by [IconToggleButton]. */
+object IconToggleButtonDefaults {
+
+ /**
+ * Creates a [Shape] with an animation between three [CornerSize]s based on the pressed state
+ * and checked/unchecked.
+ *
+ * A simple icon toggle button using the default colors, animated on Press and Check/Uncheck:
+ *
+ * @sample androidx.wear.compose.material3.samples.IconToggleButtonVariantSample
+ * @param interactionSource the interaction source applied to the Button.
+ * @param checked the current checked/unchecked state.
+ * @param uncheckedCornerSize the size of the corner when unchecked.
+ * @param checkedCornerSize the size of the corner when checked.
+ * @param pressedCornerSize the size of the corner when pressed.
+ * @param onPressAnimationSpec the spec for press animation.
+ * @param onReleaseAnimationSpec the spec for release animation.
+ */
+ @Composable
+ fun animatedToggleButtonShape(
+ interactionSource: InteractionSource,
+ checked: Boolean,
+ uncheckedCornerSize: CornerSize = UncheckedCornerSize,
+ checkedCornerSize: CornerSize = CheckedCornerSize,
+ pressedCornerSize: CornerSize = PressedCornerSize,
+ onPressAnimationSpec: FiniteAnimationSpec<Float> =
+ MaterialTheme.motionScheme.rememberFastSpatialSpec(),
+ onReleaseAnimationSpec: FiniteAnimationSpec<Float> =
+ MaterialTheme.motionScheme.slowSpatialSpec(),
+ ): Shape {
+ val pressed = interactionSource.collectIsPressedAsState()
+
+ return rememberAnimatedToggleRoundedCornerShape(
+ uncheckedCornerSize = uncheckedCornerSize,
+ checkedCornerSize = checkedCornerSize,
+ pressedCornerSize = pressedCornerSize,
+ pressed = pressed.value,
+ checked = checked,
+ onPressAnimationSpec = onPressAnimationSpec,
+ onReleaseAnimationSpec = onReleaseAnimationSpec,
+ )
+ }
+
+ /** The recommended size for an Unchecked button when animated. */
+ val UncheckedCornerSize: CornerSize = ShapeTokens.CornerFull.topEnd
+
+ /** The recommended size for a Checked button when animated. */
+ val CheckedCornerSize: CornerSize = CornerSize(percent = 30)
+
+ /** The recommended size for a Pressed button when animated. */
+ val PressedCornerSize: CornerSize = ShapeDefaults.Small.topEnd
+
+ /**
+ * Creates an [IconToggleButtonColors] for a [IconToggleButton]
+ * - by default, a colored background with a contrasting content color.
+ *
+ * If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
+ * [DisabledContainerAlpha]) value applied.
+ */
+ @Composable
+ fun iconToggleButtonColors() = MaterialTheme.colorScheme.defaultIconToggleButtonColors
+
+ /**
+ * Creates a [IconToggleButtonColors] for a [IconToggleButton]
+ * - by default, a colored background with a contrasting content color.
+ *
+ * If the button is disabled, then the colors will have an alpha ([DisabledContentAlpha] and
+ * [DisabledContainerAlpha]) value applied.
+ *
+ * @param checkedContainerColor The container color of this [IconToggleButton] when enabled and
+ * checked
+ * @param checkedContentColor The content color of this [IconToggleButton] when enabled and
+ * checked
+ * @param uncheckedContainerColor The container color of this [IconToggleButton] when enabled
+ * and unchecked
+ * @param uncheckedContentColor The content color of this [IconToggleButton] when enabled and
+ * unchecked
+ * @param disabledCheckedContainerColor The container color of this [IconToggleButton] when
+ * checked and not enabled
+ * @param disabledCheckedContentColor The content color of this [IconToggleButton] when checked
+ * and not enabled
+ * @param disabledUncheckedContainerColor The container color of this [IconToggleButton] when
+ * unchecked and not enabled
+ * @param disabledUncheckedContentColor The content color of this [IconToggleButton] when
+ * unchecked and not enabled
+ */
+ @Composable
+ fun iconToggleButtonColors(
+ checkedContainerColor: Color = Color.Unspecified,
+ checkedContentColor: Color = Color.Unspecified,
+ uncheckedContainerColor: Color = Color.Unspecified,
+ uncheckedContentColor: Color = Color.Unspecified,
+ disabledCheckedContainerColor: Color = Color.Unspecified,
+ disabledCheckedContentColor: Color = Color.Unspecified,
+ disabledUncheckedContainerColor: Color = Color.Unspecified,
+ disabledUncheckedContentColor: Color = Color.Unspecified,
+ ): IconToggleButtonColors =
+ MaterialTheme.colorScheme.defaultIconToggleButtonColors.copy(
+ checkedContainerColor = checkedContainerColor,
+ checkedContentColor = checkedContentColor,
+ uncheckedContainerColor = uncheckedContainerColor,
+ uncheckedContentColor = uncheckedContentColor,
+ disabledCheckedContainerColor = disabledCheckedContainerColor,
+ disabledCheckedContentColor = disabledCheckedContentColor,
+ disabledUncheckedContainerColor = disabledUncheckedContainerColor,
+ disabledUncheckedContentColor = disabledUncheckedContentColor,
+ )
+
+ private val ColorScheme.defaultIconToggleButtonColors: IconToggleButtonColors
+ get() {
+ return defaultIconToggleButtonColorsCached
+ ?: IconToggleButtonColors(
+ checkedContainerColor =
+ fromToken(IconToggleButtonTokens.CheckedContainerColor),
+ checkedContentColor = fromToken(IconToggleButtonTokens.CheckedContentColor),
+ uncheckedContainerColor =
+ fromToken(IconToggleButtonTokens.UncheckedContainerColor),
+ uncheckedContentColor =
+ fromToken(IconToggleButtonTokens.UncheckedContentColor),
+ disabledCheckedContainerColor =
+ fromToken(IconToggleButtonTokens.DisabledCheckedContainerColor)
+ .toDisabledColor(
+ disabledAlpha =
+ IconToggleButtonTokens.DisabledCheckedContainerOpacity
+ ),
+ disabledCheckedContentColor =
+ fromToken(IconToggleButtonTokens.DisabledCheckedContentColor)
+ .toDisabledColor(
+ disabledAlpha =
+ IconToggleButtonTokens.DisabledCheckedContentOpacity
+ ),
+ disabledUncheckedContainerColor =
+ fromToken(IconToggleButtonTokens.DisabledUncheckedContainerColor)
+ .toDisabledColor(
+ disabledAlpha =
+ IconToggleButtonTokens.DisabledUncheckedContainerOpacity
+ ),
+ disabledUncheckedContentColor =
+ fromToken(IconToggleButtonTokens.DisabledUncheckedContentColor)
+ .toDisabledColor(
+ disabledAlpha =
+ IconToggleButtonTokens.DisabledUncheckedContentOpacity
+ ),
+ )
+ .also { defaultIconToggleButtonColorsCached = it }
+ }
+}
+
/**
* Represents the different container and content colors used for [IconToggleButton] in various
* states, that are checked, unchecked, enabled and disabled.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
index fa819c7..eec59d0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
@@ -140,7 +140,7 @@
iconContainer(
iconContainerColor = colors.iconContainerColor,
progressIndicatorColors =
- ProgressIndicatorColors(
+ ProgressIndicatorDefaults.colors(
SolidColor(colors.progressIndicatorColor),
SolidColor(colors.progressTrackColor)
),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
index e920b35..31c70c4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ProgressIndicator.kt
@@ -29,6 +29,7 @@
import androidx.wear.compose.materialcore.toRadians
import kotlin.math.cos
import kotlin.math.min
+import kotlin.math.round
import kotlin.math.sin
/** Contains defaults for Progress Indicators. */
@@ -37,57 +38,76 @@
@Composable fun colors() = MaterialTheme.colorScheme.defaultProgressIndicatorColors
/**
- * Creates a [ProgressIndicatorColors] with modified colors used in [CircularProgressIndicator]
- * and [LinearProgressIndicator].
+ * Creates a [ProgressIndicatorColors] with modified colors.
*
* @param indicatorColor The indicator color.
* @param trackColor The track color.
+ * @param overflowTrackColor The overflow track color.
* @param disabledIndicatorColor The disabled indicator color.
* @param disabledTrackColor The disabled track color.
+ * @param disabledOverflowTrackColor The disabled overflow track color.
*/
@Composable
fun colors(
indicatorColor: Color = Color.Unspecified,
trackColor: Color = Color.Unspecified,
+ overflowTrackColor: Color = Color.Unspecified,
disabledIndicatorColor: Color = Color.Unspecified,
disabledTrackColor: Color = Color.Unspecified,
+ disabledOverflowTrackColor: Color = Color.Unspecified,
) =
MaterialTheme.colorScheme.defaultProgressIndicatorColors.copy(
indicatorColor = indicatorColor,
trackColor = trackColor,
+ overflowTrackColor = overflowTrackColor,
disabledIndicatorColor = disabledIndicatorColor,
disabledTrackColor = disabledTrackColor,
+ disabledOverflowTrackColor = disabledOverflowTrackColor,
)
/**
- * Creates a [ProgressIndicatorColors] with modified brushes used in [CircularProgressIndicator]
- * and [LinearProgressIndicator].
+ * Creates a [ProgressIndicatorColors] with modified brushes.
*
* @param indicatorBrush [Brush] used to draw indicator.
* @param trackBrush [Brush] used to draw track.
+ * @param overflowTrackBrush [Brush] used to draw track for progress overflow.
* @param disabledIndicatorBrush [Brush] used to draw the indicator if the progress is disabled.
* @param disabledTrackBrush [Brush] used to draw the track if the progress is disabled.
+ * @param disabledOverflowTrackBrush [Brush] used to draw the overflow track if the progress is
+ * disabled.
*/
@Composable
fun colors(
indicatorBrush: Brush? = null,
trackBrush: Brush? = null,
+ overflowTrackBrush: Brush? = null,
disabledIndicatorBrush: Brush? = null,
disabledTrackBrush: Brush? = null,
+ disabledOverflowTrackBrush: Brush? = null,
) =
MaterialTheme.colorScheme.defaultProgressIndicatorColors.copy(
indicatorBrush = indicatorBrush,
trackBrush = trackBrush,
+ overflowTrackBrush = overflowTrackBrush,
disabledIndicatorBrush = disabledIndicatorBrush,
disabledTrackBrush = disabledTrackBrush,
+ disabledOverflowTrackBrush = disabledOverflowTrackBrush
)
+ // TODO(b/364538891): add color and alpha tokens for ProgressIndicator
+ private const val OverflowTrackColorAlpha = 0.6f
+
private val ColorScheme.defaultProgressIndicatorColors: ProgressIndicatorColors
get() {
return defaultProgressIndicatorColorsCached
?: ProgressIndicatorColors(
indicatorBrush = SolidColor(fromToken(ColorSchemeKeyTokens.Primary)),
trackBrush = SolidColor(fromToken(ColorSchemeKeyTokens.SurfaceContainer)),
+ overflowTrackBrush =
+ SolidColor(
+ fromToken(ColorSchemeKeyTokens.Primary)
+ .copy(alpha = OverflowTrackColorAlpha)
+ ),
disabledIndicatorBrush =
SolidColor(
fromToken(ColorSchemeKeyTokens.OnSurface)
@@ -98,6 +118,12 @@
fromToken(ColorSchemeKeyTokens.OnSurface)
.toDisabledColor(disabledAlpha = DisabledContainerAlpha)
),
+ disabledOverflowTrackBrush =
+ SolidColor(
+ fromToken(ColorSchemeKeyTokens.Primary)
+ .copy(alpha = OverflowTrackColorAlpha)
+ .toDisabledColor(disabledAlpha = DisabledContainerAlpha)
+ )
)
.also { defaultProgressIndicatorColorsCached = it }
}
@@ -108,44 +134,61 @@
*
* @param indicatorBrush [Brush] used to draw the indicator of progress indicator.
* @param trackBrush [Brush] used to draw the track of progress indicator.
+ * @param overflowTrackBrush [Brush] used to draw the track for progress overflow (>100%).
* @param disabledIndicatorBrush [Brush] used to draw the indicator if the component is disabled.
* @param disabledTrackBrush [Brush] used to draw the track if the component is disabled.
+ * @param disabledOverflowTrackBrush [Brush] used to draw the track if the component is disabled.
*/
class ProgressIndicatorColors(
val indicatorBrush: Brush,
val trackBrush: Brush,
- val disabledIndicatorBrush: Brush = indicatorBrush,
- val disabledTrackBrush: Brush = disabledIndicatorBrush,
+ val overflowTrackBrush: Brush,
+ val disabledIndicatorBrush: Brush,
+ val disabledTrackBrush: Brush,
+ val disabledOverflowTrackBrush: Brush,
) {
internal fun copy(
indicatorColor: Color = Color.Unspecified,
trackColor: Color = Color.Unspecified,
+ overflowTrackColor: Color = Color.Unspecified,
disabledIndicatorColor: Color = Color.Unspecified,
disabledTrackColor: Color = Color.Unspecified,
+ disabledOverflowTrackColor: Color = Color.Unspecified,
) =
ProgressIndicatorColors(
indicatorBrush =
if (indicatorColor.isSpecified) SolidColor(indicatorColor) else indicatorBrush,
trackBrush = if (trackColor.isSpecified) SolidColor(trackColor) else trackBrush,
+ overflowTrackBrush =
+ if (overflowTrackColor.isSpecified) SolidColor(overflowTrackColor)
+ else overflowTrackBrush,
disabledIndicatorBrush =
if (disabledIndicatorColor.isSpecified) SolidColor(disabledIndicatorColor)
else disabledIndicatorBrush,
disabledTrackBrush =
if (disabledTrackColor.isSpecified) SolidColor(disabledTrackColor)
else disabledTrackBrush,
+ disabledOverflowTrackBrush =
+ if (disabledOverflowTrackColor.isSpecified) SolidColor(disabledOverflowTrackColor)
+ else disabledOverflowTrackBrush,
)
internal fun copy(
indicatorBrush: Brush? = null,
trackBrush: Brush? = null,
+ overflowTrackBrush: Brush? = null,
disabledIndicatorBrush: Brush? = null,
disabledTrackBrush: Brush? = null,
+ disabledOverflowTrackBrush: Brush? = null,
) =
ProgressIndicatorColors(
indicatorBrush = indicatorBrush ?: this.indicatorBrush,
trackBrush = trackBrush ?: this.trackBrush,
+ overflowTrackBrush = overflowTrackBrush ?: this.overflowTrackBrush,
disabledIndicatorBrush = disabledIndicatorBrush ?: this.disabledIndicatorBrush,
disabledTrackBrush = disabledTrackBrush ?: this.disabledTrackBrush,
+ disabledOverflowTrackBrush =
+ disabledOverflowTrackBrush ?: this.disabledOverflowTrackBrush,
)
/**
@@ -158,12 +201,17 @@
}
/**
- * Represents the track color, depending on [enabled].
+ * Represents the track color, depending on [enabled] and [hasOverflow] parameters.
*
* @param enabled whether the component is enabled.
+ * @param enabled whether the progress has overflow.
*/
- internal fun trackBrush(enabled: Boolean): Brush {
- return if (enabled) trackBrush else disabledTrackBrush
+ internal fun trackBrush(enabled: Boolean, hasOverflow: Boolean = false): Brush {
+ return if (enabled) {
+ if (hasOverflow) overflowTrackBrush else trackBrush
+ } else {
+ if (hasOverflow) disabledOverflowTrackBrush else disabledTrackBrush
+ }
}
override fun equals(other: Any?): Boolean {
@@ -172,8 +220,10 @@
if (indicatorBrush != other.indicatorBrush) return false
if (trackBrush != other.trackBrush) return false
+ if (overflowTrackBrush != other.overflowTrackBrush) return false
if (disabledIndicatorBrush != other.disabledIndicatorBrush) return false
if (disabledTrackBrush != other.disabledTrackBrush) return false
+ if (disabledOverflowTrackBrush != other.disabledOverflowTrackBrush) return false
return true
}
@@ -181,8 +231,10 @@
override fun hashCode(): Int {
var result = indicatorBrush.hashCode()
result = 31 * result + trackBrush.hashCode()
+ result = 31 * result + overflowTrackBrush.hashCode()
result = 31 * result + disabledIndicatorBrush.hashCode()
result = 31 * result + disabledTrackBrush.hashCode()
+ result = 31 * result + disabledOverflowTrackBrush.hashCode()
return result
}
}
@@ -243,3 +295,25 @@
)
}
}
+
+/**
+ * Coerce a [Float] progress value to [0.0..1.0] range.
+ *
+ * If overflow is enabled, truncate overflow values larger than 1.0 to only the fractional part.
+ * Integer values larger than 0.0 always return 1.0 (full progress) and negative values are coerced
+ * to 0.0. For example: 1.2 will be return 0.2, and 2.0 will return 1.0. If overflow is disabled,
+ * simply coerce all values to [0.0..1.0] range. For example, 1.2 and 2.0 will both return 1.0.
+ *
+ * @param progress The progress value to be coerced to [0.0..1.0] range.
+ * @param allowProgressOverflow If overflow is allowed.
+ */
+internal fun coerceProgress(progress: Float, allowProgressOverflow: Boolean): Float {
+ if (!allowProgressOverflow) return progress.coerceIn(0f, 1f)
+ if (progress <= 0.0f) return 0.0f
+ if (progress <= 1.0f) return progress
+
+ val fraction = progress % 1.0f
+ // Round to 5 decimals to avoid floating point errors.
+ val roundedFraction = round(fraction * 100000f) / 100000f
+ return if (roundedFraction == 0.0f) 1.0f else roundedFraction
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
index a176a12..d1ca985 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Providers.kt
@@ -21,8 +21,6 @@
import androidx.compose.runtime.State
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
internal fun <T> provideScopeContent(
contentColor: Color,
@@ -40,17 +38,13 @@
internal fun <T> provideScopeContent(
contentColor: Color,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)
): (@Composable T.() -> Unit) = {
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -73,18 +67,14 @@
internal fun <T> provideScopeContent(
contentColor: State<Color>,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)
): (@Composable T.() -> Unit) = {
val color = contentColor.value
CompositionLocalProvider(
LocalContentColor provides color,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -132,9 +122,7 @@
internal fun <T> provideNullableScopeContent(
contentColor: State<Color>,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)?
): (@Composable T.() -> Unit)? =
content?.let {
@@ -143,9 +131,7 @@
CompositionLocalProvider(
LocalContentColor provides color,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign,
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
@@ -170,9 +156,7 @@
internal fun <T> provideNullableScopeContent(
contentColor: Color,
textStyle: TextStyle,
- overflow: TextOverflow,
- maxLines: Int,
- textAlign: TextAlign,
+ textConfiguration: TextConfiguration,
content: (@Composable T.() -> Unit)?
): (@Composable T.() -> Unit)? =
content?.let {
@@ -180,9 +164,7 @@
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides textStyle,
- LocalTextOverflow provides overflow,
- LocalTextMaxLines provides maxLines,
- LocalTextAlign provides textAlign,
+ LocalTextConfiguration provides textConfiguration,
) {
content()
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index 858243c..eab5a42 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -164,9 +164,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, selected = selected),
textStyle = RadioButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -174,9 +177,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, selected = selected),
textStyle = RadioButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
)
)
@@ -304,9 +310,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, selected = selected),
textStyle = SplitRadioButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -314,9 +323,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, selected = selected),
textStyle = SplitRadioButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
index c88b35f..cda8be4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
@@ -41,12 +41,11 @@
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.wear.compose.foundation.ActiveFocusListener
import androidx.wear.compose.foundation.ScrollInfoProvider
+import androidx.wear.compose.foundation.lazy.LazyColumnState
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import kotlin.math.roundToInt
@@ -128,15 +127,16 @@
* Example of using AppScaffold and ScreenScaffold:
*
* @sample androidx.wear.compose.material3.samples.ScaffoldSample
- * @param scrollState The scroll state for LazyColumn, used to drive screen transitions such as
- * [TimeText] scroll away and showing/hiding [ScrollIndicator].
+ * @param scrollState The scroll state for [androidx.wear.compose.foundation.lazy.LazyColumn], used
+ * to drive screen transitions such as [TimeText] scroll away and showing/hiding
+ * [ScrollIndicator].
* @param modifier The modifier for the screen scaffold.
* @param timeText Time text (both time and potentially status message) for this screen, if
* different to the time text at the [AppScaffold] level. When null, the time text from the
* [AppScaffold] is displayed for this screen.
* @param scrollIndicator The [ScrollIndicator] to display on this screen, which is expected to be
- * aligned to Center-End. It is recommended to use the Material3 [ScrollIndicator] which is
- * provided by default. No scroll indicator is displayed if null is passed.
+ * aligned to Center-End. It is recommended to use the Material3 [ScrollIndicator]. No scroll
+ * indicator is displayed if null is passed.
* @param bottomButton Optional slot for a Button (usually an [EdgeButton]) that takes the available
* space below a scrolling list. It will scale up and fade in when the user scrolls to the end of
* the list, and scale down and fade out as the user scrolls up.
@@ -144,12 +144,10 @@
*/
@Composable
fun ScreenScaffold(
- scrollState: LazyListState,
+ scrollState: LazyColumnState,
modifier: Modifier = Modifier,
- scrollIndicator: @Composable BoxScope.() -> Unit = {
- ScrollIndicator(scrollState, modifier = Modifier.align(Alignment.CenterEnd))
- },
timeText: (@Composable () -> Unit)? = null,
+ scrollIndicator: (@Composable BoxScope.() -> Unit)? = null,
bottomButton: (@Composable BoxScope.() -> Unit)? = null,
content: @Composable BoxScope.() -> Unit,
) =
@@ -187,12 +185,8 @@
* Example of using AppScaffold and ScreenScaffold:
*
* @sample androidx.wear.compose.material3.samples.ScaffoldSample
- * @param scrollState The scroll state for a Column, used to drive screen transitions such as
- * [TimeText] scroll away and showing/hiding [ScrollIndicator].
- * @param bottomButton Optional slot for a Button (usually an [EdgeButton]) that takes the available
- * space below a scrolling list. It will scale up and fade in when the user scrolls to the end of
- * the list, and scale down and fade out as the user scrolls up.
- * @param bottomButtonHeight the maximum height of the space taken by the bottom button.
+ * @param scrollState The scroll state for [androidx.compose.foundation.lazy.LazyColumn], used to
+ * drive screen transitions such as [TimeText] scroll away and showing/hiding [ScrollIndicator].
* @param modifier The modifier for the screen scaffold.
* @param timeText Time text (both time and potentially status message) for this screen, if
* different to the time text at the [AppScaffold] level. When null, the time text from the
@@ -200,30 +194,40 @@
* @param scrollIndicator The [ScrollIndicator] to display on this screen, which is expected to be
* aligned to Center-End. It is recommended to use the Material3 [ScrollIndicator] which is
* provided by default. No scroll indicator is displayed if null is passed.
+ * @param bottomButton Optional slot for a Button (usually an [EdgeButton]) that takes the available
+ * space below a scrolling list. It will scale up and fade in when the user scrolls to the end of
+ * the list, and scale down and fade out as the user scrolls up.
* @param content The body content for this screen.
*/
@Composable
fun ScreenScaffold(
- scrollState: ScrollState,
- bottomButton: @Composable BoxScope.() -> Unit,
- bottomButtonHeight: Dp,
+ scrollState: LazyListState,
modifier: Modifier = Modifier,
timeText: (@Composable () -> Unit)? = null,
scrollIndicator: (@Composable BoxScope.() -> Unit)? = {
ScrollIndicator(scrollState, modifier = Modifier.align(Alignment.CenterEnd))
},
+ bottomButton: (@Composable BoxScope.() -> Unit)? = null,
content: @Composable BoxScope.() -> Unit,
-) {
- val bottomButtonHeightPx = with(LocalDensity.current) { bottomButtonHeight.toPx() }
- ScreenScaffold(
- bottomButton,
- ScrollInfoProvider(scrollState, bottomButtonHeightPx),
- modifier,
- timeText,
- scrollIndicator,
- content
- )
-}
+) =
+ if (bottomButton != null) {
+ ScreenScaffold(
+ bottomButton,
+ ScrollInfoProvider(scrollState),
+ modifier,
+ timeText,
+ scrollIndicator,
+ content
+ )
+ } else {
+ ScreenScaffold(
+ modifier,
+ timeText,
+ ScrollInfoProvider(scrollState),
+ scrollIndicator,
+ content
+ )
+ }
/**
* [ScreenScaffold] is one of the Wear Material3 scaffold components.
@@ -232,7 +236,9 @@
* coordinate transitions of the [ScrollIndicator] and [TimeText] components.
*
* [ScreenScaffold] displays the [ScrollIndicator] at the center-end of the screen by default and
- * coordinates showing/hiding [TimeText] and [ScrollIndicator] according to [scrollState].
+ * coordinates showing/hiding [TimeText] and [ScrollIndicator] according to [scrollState]. Note that
+ * this version doesn't support a bottom button slot, for that use the overload that takes
+ * [LazyListState] or the one that takes a [ScalingLazyListState].
*
* Example of using AppScaffold and ScreenScaffold:
*
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
index 3057a0b..c212948 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SegmentedCircularProgressIndicator.kt
@@ -41,9 +41,15 @@
* @param segmentCount Number of equal segments that the progress indicator should be divided into.
* Has to be a number equal or greater to 1.
* @param progress The progress of this progress indicator where 0.0 represents no progress and 1.0
- * represents completion. Values outside of this range are coerced into the range 0..1. The
- * progress is applied to the entire [SegmentedCircularProgressIndicator] across all segments.
+ * represents completion. Values smaller than 0.0 will be coerced to 0, while values larger than
+ * 1.0 will be wrapped around and shown as overflow with a different track color. The progress is
+ * applied to the entire [SegmentedCircularProgressIndicator] across all segments.
* @param modifier Modifier to be applied to the SegmentedCircularProgressIndicator.
+ * @param allowProgressOverflow When progress overflow is allowed, values smaller than 0.0 will be
+ * coerced to 0, while values larger than 1.0 will be wrapped around and shown as overflow with a
+ * different track color [ProgressIndicatorColors.overflowTrackBrush]. For example values 1.2, 2.2
+ * etc will be shown as 20% progress with the overflow color. When progress overflow is not
+ * allowed, progress values will be coerced into the range 0..1.
* @param startAngle The starting position of the progress arc, measured clockwise in degrees (0
* to 360) from the 3 o'clock position. For example, 0 and 360 represent 3 o'clock, 90 and 180
* represent 6 o'clock and 9 o'clock respectively. Default is 270 degrees
@@ -64,6 +70,7 @@
@IntRange(from = 1) segmentCount: Int,
progress: () -> Float,
modifier: Modifier = Modifier,
+ allowProgressOverflow: Boolean = false,
startAngle: Float = CircularProgressIndicatorDefaults.StartAngle,
endAngle: Float = startAngle,
colors: ProgressIndicatorColors = ProgressIndicatorDefaults.colors(),
@@ -72,7 +79,7 @@
enabled: Boolean = true,
) =
SegmentedCircularProgressIndicatorImpl(
- segmentParams = SegmentParams.Progress(progress),
+ segmentParams = SegmentParams.Progress(progress, allowProgressOverflow),
modifier = modifier,
segmentCount = segmentCount,
startAngle = startAngle,
@@ -93,6 +100,10 @@
* Example of [SegmentedCircularProgressIndicator] where the segments are turned on/off:
*
* @sample androidx.wear.compose.material3.samples.SegmentedProgressIndicatorOnOffSample
+ *
+ * Example of smaller size [SegmentedCircularProgressIndicator]:
+ *
+ * @sample androidx.wear.compose.material3.samples.SmallSegmentedProgressIndicatorSample
* @param segmentCount Number of equal segments that the progress indicator should be divided into.
* Has to be a number equal or greater to 1.
* @param completed A function that for each segment between 1..[segmentCount] returns true if this
@@ -187,15 +198,23 @@
)
}
is SegmentParams.Progress -> {
- val progressInSegments =
- segmentCount * segmentParams.progress().coerceIn(0f, 1f)
+ val currentProgress = segmentParams.progress()
+ val coercedProgress =
+ coerceProgress(currentProgress, segmentParams.allowOverflow)
+ val progressInSegments = segmentCount * coercedProgress
+ val hasOverflow =
+ segmentParams.allowOverflow && currentProgress > 1.0f
if (segment >= floor(progressInSegments)) {
drawIndicatorSegment(
startAngle = segmentStartAngle,
sweep = segmentSweepAngle,
gapSweep = 0f, // Overlay, no gap
- brush = colors.trackBrush(enabled),
+ brush =
+ colors.trackBrush(
+ enabled = enabled,
+ hasOverflow = hasOverflow
+ ),
stroke = stroke
)
}
@@ -223,5 +242,5 @@
private sealed interface SegmentParams {
data class Completed(val completed: (segmentIndex: Int) -> Boolean) : SegmentParams
- data class Progress(val progress: () -> Float) : SegmentParams
+ data class Progress(val progress: () -> Float, val allowOverflow: Boolean) : SegmentParams
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
index 416c536..f736828 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt
@@ -137,9 +137,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked),
textStyle = SwitchButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
toggleControl = {
@@ -171,9 +174,12 @@
provideNullableScopeContent(
contentColor = colors.secondaryContentColor(enabled = enabled, checked),
textStyle = SwitchButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
background = { isEnabled, isChecked ->
@@ -300,9 +306,12 @@
provideScopeContent(
contentColor = colors.contentColor(enabled = enabled, checked = checked),
textStyle = SplitSwitchButtonTokens.LabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 3,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 3,
+ textAlign = TextAlign.Start,
+ ),
content = label
),
secondaryLabel =
@@ -310,9 +319,12 @@
contentColor =
colors.secondaryContentColor(enabled = enabled, checked = checked),
textStyle = SplitSwitchButtonTokens.SecondaryLabelFont.value,
- overflow = TextOverflow.Ellipsis,
- maxLines = 2,
- textAlign = TextAlign.Start,
+ textConfiguration =
+ TextConfiguration(
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ textAlign = TextAlign.Start,
+ ),
content = secondaryLabel
),
spacerSize = SwitchButtonDefaults.LabelSpacerSize
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
index 54fddbc..3f242a7 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Text.kt
@@ -98,11 +98,11 @@
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
- textAlign: TextAlign? = LocalTextAlign.current,
+ textAlign: TextAlign? = LocalTextConfiguration.current.textAlign,
lineHeight: TextUnit = TextUnit.Unspecified,
- overflow: TextOverflow = LocalTextOverflow.current,
+ overflow: TextOverflow = LocalTextConfiguration.current.overflow,
softWrap: Boolean = true,
- maxLines: Int = LocalTextMaxLines.current,
+ maxLines: Int = LocalTextConfiguration.current.maxLines,
minLines: Int = 1,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
@@ -192,11 +192,11 @@
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
- textAlign: TextAlign? = LocalTextAlign.current,
+ textAlign: TextAlign? = LocalTextConfiguration.current.textAlign,
lineHeight: TextUnit = TextUnit.Unspecified,
- overflow: TextOverflow = LocalTextOverflow.current,
+ overflow: TextOverflow = LocalTextConfiguration.current.overflow,
softWrap: Boolean = true,
- maxLines: Int = LocalTextMaxLines.current,
+ maxLines: Int = LocalTextConfiguration.current.maxLines,
minLines: Int = 1,
inlineContent: Map<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
@@ -250,27 +250,60 @@
}
/**
- * CompositionLocal containing the preferred max lines that will be used by [Text] components by
- * default. Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
- * [RadioButton] will use [LocalTextMaxLines] to set values with which to style child text
+ * CompositionLocal containing the preferred [TextConfiguration] that will be used by [Text]
+ * components by default consisting of text alignment, overflow specification and max lines.
+ * Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
+ * [RadioButton] use [LocalTextConfiguration] to set values with which to style child text
* components.
*/
-val LocalTextMaxLines: ProvidableCompositionLocal<Int> =
- compositionLocalOf(structuralEqualityPolicy()) { Int.MAX_VALUE }
+val LocalTextConfiguration: ProvidableCompositionLocal<TextConfiguration> =
+ compositionLocalOf(structuralEqualityPolicy()) {
+ TextConfiguration(
+ TextConfigurationDefaults.TextAlign,
+ TextConfigurationDefaults.Overflow,
+ TextConfigurationDefaults.MaxLines
+ )
+ }
/**
- * CompositionLocal containing the preferred [TextOverflow] that will be used by [Text] components
- * by default. Material3 components related to text such as [Button], [CheckboxButton],
- * [SwitchButton], [RadioButton] will use [LocalTextOverflow] to set values with which to style
- * child text components.
+ * Class representing aspects of [Text] that can be configured with [LocalTextConfiguration].
+ *
+ * @param textAlign The alignment of the text within the lines of the paragraph.
+ * @param overflow How visual overflow should be handled.
+ * @param maxLines The maximum number of lines for the text to span, wrapping if necessary.
*/
-val LocalTextOverflow: ProvidableCompositionLocal<TextOverflow> =
- compositionLocalOf(structuralEqualityPolicy()) { TextOverflow.Clip }
+class TextConfiguration(
+ val textAlign: TextAlign?,
+ val overflow: TextOverflow,
+ val maxLines: Int,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is TextConfiguration) return false
-/**
- * CompositionLocal containing the preferred [TextAlign] that will be used by [Text] components by
- * default. Material3 components related to text such as [Button], [CheckboxButton], [SwitchButton],
- * [RadioButton] will use [LocalTextAlign] to set values with which to style child text components.
- */
-val LocalTextAlign: ProvidableCompositionLocal<TextAlign?> =
- compositionLocalOf(structuralEqualityPolicy()) { null }
+ if (textAlign != other.textAlign) return false
+ if (overflow != other.overflow) return false
+ if (maxLines != other.maxLines) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = textAlign.hashCode()
+ result = 31 * result + overflow.hashCode()
+ result = 31 * result + maxLines.hashCode()
+ return result
+ }
+}
+
+/** Default values for [TextConfiguration] */
+object TextConfigurationDefaults {
+ /** Default text alignment for [Text] */
+ val TextAlign: TextAlign? = null
+
+ /** Default visual text overflow for [Text] */
+ val Overflow: TextOverflow = TextOverflow.Clip
+
+ /** Default max lines for [Text] */
+ const val MaxLines: Int = Int.MAX_VALUE
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
index 20e8047..5753520 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
@@ -17,14 +17,17 @@
package androidx.wear.compose.material3
import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -146,10 +149,15 @@
* [TextToggleButton] can be enabled or disabled. A disabled button will not respond to click
* events. When enabled, the checked and unchecked events are propagated by [onCheckedChange].
*
- * A simple text toggle button using the default colors:
+ * A simple text toggle button using the default colors, animated when pressed.
*
* @sample androidx.wear.compose.material3.samples.TextToggleButtonSample
*
+ * A simple text toggle button using the default colors, animated when pressed and with different
+ * shapes for the checked and unchecked states.
+ *
+ * @sample androidx.wear.compose.material3.samples.TextToggleButtonVariantSample
+ *
* Example of a large text toggle button:
*
* @sample androidx.wear.compose.material3.samples.LargeTextToggleButtonSample
@@ -158,8 +166,8 @@
* @param modifier Modifier to be applied to the toggle button.
* @param enabled Controls the enabled state of the toggle button. When `false`, this toggle button
* will not be clickable.
- * @param colors [ToggleButtonColors] that will be used to resolve the container and content color
- * for this toggle button.
+ * @param colors [TextToggleButtonColors] that will be used to resolve the container and content
+ * color for this toggle button.
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
* emitting [Interaction]s for this toggle button. You can use this to change the toggle button's
* appearance or preview the toggle button in different states. Note that if `null` is provided,
@@ -175,7 +183,7 @@
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: TextToggleButtonColors = TextButtonDefaults.textToggleButtonColors(),
+ colors: TextToggleButtonColors = TextToggleButtonDefaults.textToggleButtonColors(),
interactionSource: MutableInteractionSource? = null,
shape: Shape = TextButtonDefaults.shape,
border: BorderStroke? = null,
@@ -216,9 +224,16 @@
/**
* Creates a [Shape] with a animation between two CornerBasedShapes.
*
+ * A simple text button using the default colors, animated when pressed.
+ *
+ * @sample androidx.wear.compose.material3.samples.TextButtonWithCornerAnimationSample
+ *
+ * A simple text toggle button using the default colors, animated when pressed.
+ *
+ * @sample androidx.wear.compose.material3.samples.TextToggleButtonSample
* @param interactionSource the interaction source applied to the Button.
- * @param shape The normal shape of the IconButton.
- * @param pressedShape The pressed shape of the IconButton.
+ * @param shape The normal shape of the TextButton.
+ * @param pressedShape The pressed shape of the TextButton.
*/
@Composable
fun animatedShape(
@@ -265,7 +280,7 @@
/**
* Creates a [TextButtonColors] as an alternative to the [filledTonal TextButtonColors], giving
* a surface with more chroma to indicate selected or highlighted states that are not primary
- * calls-to-action. If the icon button is disabled then the colors will default to the
+ * calls-to-action. If the text button is disabled then the colors will default to the
* MaterialTheme onSurface color with suitable alpha values applied.
*
* Example of creating a [TextButton] with [filledVariantTextButtonColors]:
@@ -279,7 +294,7 @@
/**
* Creates a [TextButtonColors] as an alternative to the [filledTonal TextButtonColors], giving
* a surface with more chroma to indicate selected or highlighted states that are not primary
- * calls-to-action. If the icon button is disabled then the colors will default to the
+ * calls-to-action. If the text button is disabled then the colors will default to the
* MaterialTheme onSurface color with suitable alpha values applied.
*
* Example of creating a [TextButton] with [filledVariantTextButtonColors]:
@@ -403,60 +418,6 @@
)
/**
- * Creates a [TextToggleButtonColors] for a [TextToggleButton]
- * - by default, a colored background with a contrasting content color. If the button is
- * disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
- * [DisabledContentAlpha]) value applied.
- */
- @Composable
- fun textToggleButtonColors() = MaterialTheme.colorScheme.defaultTextToggleButtonColors
-
- /**
- * Creates a [TextToggleButtonColors] for a [TextToggleButton]
- * - by default, a colored background with a contrasting content color. If the button is
- * disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
- * [DisabledContentAlpha]) value applied.
- *
- * @param checkedContainerColor the container color of this [TextToggleButton] when enabled and
- * checked
- * @param checkedContentColor the content color of this [TextToggleButton] when enabled and
- * checked
- * @param uncheckedContainerColor the container color of this [TextToggleButton] when enabled
- * and unchecked
- * @param uncheckedContentColor the content color of this [TextToggleButton] when enabled and
- * unchecked
- * @param disabledCheckedContainerColor the container color of this [TextToggleButton] when
- * checked and not enabled
- * @param disabledCheckedContentColor the content color of this [TextToggleButton] when checked
- * and not enabled
- * @param disabledUncheckedContainerColor the container color of this [TextToggleButton] when
- * unchecked and not enabled
- * @param disabledUncheckedContentColor the content color of this [TextToggleButton] when
- * unchecked and not enabled
- */
- @Composable
- fun textToggleButtonColors(
- checkedContainerColor: Color = Color.Unspecified,
- checkedContentColor: Color = Color.Unspecified,
- uncheckedContainerColor: Color = Color.Unspecified,
- uncheckedContentColor: Color = Color.Unspecified,
- disabledCheckedContainerColor: Color = Color.Unspecified,
- disabledCheckedContentColor: Color = Color.Unspecified,
- disabledUncheckedContainerColor: Color = Color.Unspecified,
- disabledUncheckedContentColor: Color = Color.Unspecified,
- ): TextToggleButtonColors =
- MaterialTheme.colorScheme.defaultTextToggleButtonColors.copy(
- checkedContainerColor = checkedContainerColor,
- checkedContentColor = checkedContentColor,
- uncheckedContainerColor = uncheckedContainerColor,
- uncheckedContentColor = uncheckedContentColor,
- disabledCheckedContainerColor = disabledCheckedContainerColor,
- disabledCheckedContentColor = disabledCheckedContentColor,
- disabledUncheckedContainerColor = disabledUncheckedContainerColor,
- disabledUncheckedContentColor = disabledUncheckedContentColor,
- )
-
- /**
* The recommended size for a small button. It is recommended to apply this size using
* [Modifier.touchTargetAwareSize].
*/
@@ -579,45 +540,6 @@
)
.also { defaultTextButtonColorsCached = it }
}
-
- private val ColorScheme.defaultTextToggleButtonColors: TextToggleButtonColors
- get() {
- return defaultTextToggleButtonColorsCached
- ?: TextToggleButtonColors(
- checkedContainerColor =
- fromToken(TextToggleButtonTokens.CheckedContainerColor),
- checkedContentColor = fromToken(TextToggleButtonTokens.CheckedContentColor),
- uncheckedContainerColor =
- fromToken(TextToggleButtonTokens.UncheckedContainerColor),
- uncheckedContentColor =
- fromToken(TextToggleButtonTokens.UncheckedContentColor),
- disabledCheckedContainerColor =
- fromToken(TextToggleButtonTokens.DisabledCheckedContainerColor)
- .toDisabledColor(
- disabledAlpha =
- TextToggleButtonTokens.DisabledCheckedContainerOpacity
- ),
- disabledCheckedContentColor =
- fromToken(TextToggleButtonTokens.DisabledCheckedContentColor)
- .toDisabledColor(
- disabledAlpha =
- TextToggleButtonTokens.DisabledCheckedContentOpacity
- ),
- disabledUncheckedContainerColor =
- fromToken(TextToggleButtonTokens.DisabledUncheckedContainerColor)
- .toDisabledColor(
- disabledAlpha =
- TextToggleButtonTokens.DisabledUncheckedContainerOpacity
- ),
- disabledUncheckedContentColor =
- fromToken(TextToggleButtonTokens.DisabledUncheckedContentColor)
- .toDisabledColor(
- disabledAlpha =
- TextToggleButtonTokens.DisabledUncheckedContentOpacity
- ),
- )
- .also { defaultTextToggleButtonColorsCached = it }
- }
}
/**
@@ -697,6 +619,152 @@
}
}
+/** Contains the default values used by [TextToggleButton]. */
+object TextToggleButtonDefaults {
+
+ /**
+ * Creates a [Shape] with an animation between three [CornerSize]s based on the pressed state
+ * and checked/unchecked.
+ *
+ * A simple text toggle button using the default colors, animated on Press and Check/Uncheck:
+ *
+ * @sample androidx.wear.compose.material3.samples.TextToggleButtonVariantSample
+ * @param interactionSource the interaction source applied to the Button.
+ * @param checked the current checked/unchecked state.
+ * @param uncheckedCornerSize the size of the corner when unchecked.
+ * @param checkedCornerSize the size of the corner when checked.
+ * @param pressedCornerSize the size of the corner when pressed.
+ * @param onPressAnimationSpec the spec for press animation.
+ * @param onReleaseAnimationSpec the spec for release animation.
+ */
+ @Composable
+ fun animatedToggleButtonShape(
+ interactionSource: InteractionSource,
+ checked: Boolean,
+ uncheckedCornerSize: CornerSize = UncheckedCornerSize,
+ checkedCornerSize: CornerSize = CheckedCornerSize,
+ pressedCornerSize: CornerSize = PressedCornerSize,
+ onPressAnimationSpec: FiniteAnimationSpec<Float> =
+ MaterialTheme.motionScheme.rememberFastSpatialSpec(),
+ onReleaseAnimationSpec: FiniteAnimationSpec<Float> =
+ MaterialTheme.motionScheme.slowSpatialSpec(),
+ ): Shape {
+ val pressed = interactionSource.collectIsPressedAsState()
+
+ return rememberAnimatedToggleRoundedCornerShape(
+ uncheckedCornerSize = uncheckedCornerSize,
+ checkedCornerSize = checkedCornerSize,
+ pressedCornerSize = pressedCornerSize,
+ pressed = pressed.value,
+ checked = checked,
+ onPressAnimationSpec = onPressAnimationSpec,
+ onReleaseAnimationSpec = onReleaseAnimationSpec,
+ )
+ }
+
+ /** The recommended size for an Unchecked button when animated. */
+ val UncheckedCornerSize: CornerSize = ShapeTokens.CornerFull.topEnd
+
+ /** The recommended size for a Checked button when animated. */
+ val CheckedCornerSize: CornerSize = CornerSize(percent = 30)
+
+ /** The recommended size for a Pressed button when animated. */
+ val PressedCornerSize: CornerSize = ShapeDefaults.Small.topEnd
+
+ /**
+ * Creates a [TextToggleButtonColors] for a [TextToggleButton]
+ * - by default, a colored background with a contrasting content color. If the button is
+ * disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
+ * [DisabledContentAlpha]) value applied.
+ */
+ @Composable
+ fun textToggleButtonColors() = MaterialTheme.colorScheme.defaultTextToggleButtonColors
+
+ /**
+ * Creates a [TextToggleButtonColors] for a [TextToggleButton]
+ * - by default, a colored background with a contrasting content color. If the button is
+ * disabled, then the colors will have an alpha ([DisabledContainerAlpha] or
+ * [DisabledContentAlpha]) value applied.
+ *
+ * @param checkedContainerColor the container color of this [TextToggleButton] when enabled and
+ * checked
+ * @param checkedContentColor the content color of this [TextToggleButton] when enabled and
+ * checked
+ * @param uncheckedContainerColor the container color of this [TextToggleButton] when enabled
+ * and unchecked
+ * @param uncheckedContentColor the content color of this [TextToggleButton] when enabled and
+ * unchecked
+ * @param disabledCheckedContainerColor the container color of this [TextToggleButton] when
+ * checked and not enabled
+ * @param disabledCheckedContentColor the content color of this [TextToggleButton] when checked
+ * and not enabled
+ * @param disabledUncheckedContainerColor the container color of this [TextToggleButton] when
+ * unchecked and not enabled
+ * @param disabledUncheckedContentColor the content color of this [TextToggleButton] when
+ * unchecked and not enabled
+ */
+ @Composable
+ fun textToggleButtonColors(
+ checkedContainerColor: Color = Color.Unspecified,
+ checkedContentColor: Color = Color.Unspecified,
+ uncheckedContainerColor: Color = Color.Unspecified,
+ uncheckedContentColor: Color = Color.Unspecified,
+ disabledCheckedContainerColor: Color = Color.Unspecified,
+ disabledCheckedContentColor: Color = Color.Unspecified,
+ disabledUncheckedContainerColor: Color = Color.Unspecified,
+ disabledUncheckedContentColor: Color = Color.Unspecified,
+ ): TextToggleButtonColors =
+ MaterialTheme.colorScheme.defaultTextToggleButtonColors.copy(
+ checkedContainerColor = checkedContainerColor,
+ checkedContentColor = checkedContentColor,
+ uncheckedContainerColor = uncheckedContainerColor,
+ uncheckedContentColor = uncheckedContentColor,
+ disabledCheckedContainerColor = disabledCheckedContainerColor,
+ disabledCheckedContentColor = disabledCheckedContentColor,
+ disabledUncheckedContainerColor = disabledUncheckedContainerColor,
+ disabledUncheckedContentColor = disabledUncheckedContentColor,
+ )
+
+ private val ColorScheme.defaultTextToggleButtonColors: TextToggleButtonColors
+ get() {
+ return defaultTextToggleButtonColorsCached
+ ?: TextToggleButtonColors(
+ checkedContainerColor =
+ fromToken(TextToggleButtonTokens.CheckedContainerColor),
+ checkedContentColor = fromToken(TextToggleButtonTokens.CheckedContentColor),
+ uncheckedContainerColor =
+ fromToken(TextToggleButtonTokens.UncheckedContainerColor),
+ uncheckedContentColor =
+ fromToken(TextToggleButtonTokens.UncheckedContentColor),
+ disabledCheckedContainerColor =
+ fromToken(TextToggleButtonTokens.DisabledCheckedContainerColor)
+ .toDisabledColor(
+ disabledAlpha =
+ TextToggleButtonTokens.DisabledCheckedContainerOpacity
+ ),
+ disabledCheckedContentColor =
+ fromToken(TextToggleButtonTokens.DisabledCheckedContentColor)
+ .toDisabledColor(
+ disabledAlpha =
+ TextToggleButtonTokens.DisabledCheckedContentOpacity
+ ),
+ disabledUncheckedContainerColor =
+ fromToken(TextToggleButtonTokens.DisabledUncheckedContainerColor)
+ .toDisabledColor(
+ disabledAlpha =
+ TextToggleButtonTokens.DisabledUncheckedContainerOpacity
+ ),
+ disabledUncheckedContentColor =
+ fromToken(TextToggleButtonTokens.DisabledUncheckedContentColor)
+ .toDisabledColor(
+ disabledAlpha =
+ TextToggleButtonTokens.DisabledUncheckedContentOpacity
+ ),
+ )
+ .also { defaultTextToggleButtonColorsCached = it }
+ }
+}
+
/**
* Represents the different container and content colors used for [TextToggleButton] in various
* states, that are checked, unchecked, enabled and disabled.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
index a5bc319..f210918 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
@@ -56,6 +56,9 @@
* Arc text styles are used for curved text making up the signposting on the UI such as time text
* and curved labels, a tailored font axis that specifically optimizes type along a curve.
*
+ * @property arcLarge ArcLarge is for arc headers and titles. Arc is for text along a curved path on
+ * the screen, reserved for short header text strings at the very top or bottom of the screen like
+ * confirmation overlays.
* @property arcMedium ArcMedium is for arc headers and titles. Arc is for text along a curved path
* on the screen, reserved for short header text strings at the very top or bottom of the screen
* like page titles.
@@ -112,6 +115,7 @@
@Immutable
class Typography
internal constructor(
+ val arcLarge: TextStyle,
val arcMedium: TextStyle,
val arcSmall: TextStyle,
val displayLarge: TextStyle,
@@ -135,6 +139,7 @@
) {
constructor(
defaultFontFamily: FontFamily = FontFamily.Default,
+ arcLarge: TextStyle = TypographyTokens.ArcLarge,
arcMedium: TextStyle = TypographyTokens.ArcMedium,
arcSmall: TextStyle = TypographyTokens.ArcSmall,
displayLarge: TextStyle = TypographyTokens.DisplayLarge,
@@ -156,6 +161,7 @@
numeralSmall: TextStyle = TypographyTokens.NumeralSmall,
numeralExtraSmall: TextStyle = TypographyTokens.NumeralExtraSmall,
) : this(
+ arcLarge = arcLarge.withDefaultFontFamily(defaultFontFamily),
arcMedium = arcMedium.withDefaultFontFamily(defaultFontFamily),
arcSmall = arcSmall.withDefaultFontFamily(defaultFontFamily),
displayLarge = displayLarge.withDefaultFontFamily(defaultFontFamily),
@@ -180,6 +186,7 @@
/** Returns a copy of this Typography, optionally overriding some of the values. */
fun copy(
+ arcLarge: TextStyle = this.arcLarge,
arcMedium: TextStyle = this.arcMedium,
arcSmall: TextStyle = this.arcSmall,
displayLarge: TextStyle = this.displayLarge,
@@ -202,6 +209,7 @@
numeralExtraSmall: TextStyle = this.numeralExtraSmall,
): Typography =
Typography(
+ arcLarge,
arcMedium,
arcSmall,
displayLarge,
@@ -228,6 +236,7 @@
if (this === other) return true
if (other !is Typography) return false
+ if (arcLarge != other.arcLarge) return false
if (arcMedium != other.arcMedium) return false
if (arcSmall != other.arcSmall) return false
if (displayLarge != other.displayLarge) return false
@@ -253,7 +262,8 @@
}
override fun hashCode(): Int {
- var result = arcMedium.hashCode()
+ var result = arcLarge.hashCode()
+ result = 31 * result + arcMedium.hashCode()
result = 31 * result + arcSmall.hashCode()
result = 31 * result + displayLarge.hashCode()
result = 31 * result + displayMedium.hashCode()
@@ -278,6 +288,7 @@
override fun toString(): String {
return "Typography(" +
+ "arcLarge=$arcLarge, " +
"arcMedium=$arcMedium, " +
"arcSmall=$arcSmall, " +
"displayLarge=$displayLarge, " +
@@ -327,6 +338,7 @@
/** Helper function for typography tokens. */
internal fun Typography.fromToken(value: TypographyKeyTokens): TextStyle {
return when (value) {
+ TypographyKeyTokens.ArcLarge -> arcLarge
TypographyKeyTokens.ArcMedium -> arcMedium
TypographyKeyTokens.ArcSmall -> arcSmall
TypographyKeyTokens.DisplayLarge -> displayLarge
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
index e01a22d..f2501f5 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledButtonTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_68
+// VERSION: v0_73
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -30,8 +30,9 @@
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
val IconColor = ColorSchemeKeyTokens.OnPrimary
+ val IconExtraLargeSize = 36.0.dp
val IconLargeSize = 32.0.dp
- val IconSize = 24.0.dp
+ val IconSize = 26.0.dp
val LabelColor = ColorSchemeKeyTokens.OnPrimary
val LabelFont = TypographyKeyTokens.LabelMedium
val SecondaryLabelColor = ColorSchemeKeyTokens.OnPrimary
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt
index fa7cca4..681e78f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalIconButtonTokens.kt
@@ -20,7 +20,7 @@
package androidx.wear.compose.material3.tokens
internal object FilledTonalIconButtonTokens {
val ContainerColor = ColorSchemeKeyTokens.SurfaceContainer
- val ContentColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ContentColor = ColorSchemeKeyTokens.Primary
val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
val DisabledContainerOpacity = 0.12f
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalTextButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalTextButtonTokens.kt
index a7c42e8..a220a6a 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalTextButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/FilledTonalTextButtonTokens.kt
@@ -21,7 +21,7 @@
internal object FilledTonalTextButtonTokens {
val ContainerColor = ColorSchemeKeyTokens.SurfaceContainer
val ContainerShape = ShapeKeyTokens.CornerFull
- val ContentColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ContentColor = ColorSchemeKeyTokens.OnSurface
val ContentFont = TypographyKeyTokens.LabelMedium
val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
val DisabledContainerOpacity = 0.12f
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt
index 34cb9fc..c54e56e 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/IconButtonTokens.kt
@@ -27,7 +27,7 @@
val ContainerLargeSize = 60.0.dp
val ContainerShape = ShapeKeyTokens.CornerFull
val ContainerSmallSize = 48.0.dp
- val ContentColor = ColorSchemeKeyTokens.OnSurface
+ val ContentColor = ColorSchemeKeyTokens.Primary
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
val IconDefaultSize = 26.0.dp
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
index 4af2dad..a95e41f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/ImageButtonTokens.kt
@@ -14,17 +14,19 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: 0_72
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
internal object ImageButtonTokens {
val BackgroundImageGradientColor = ColorSchemeKeyTokens.SurfaceContainer
- val ContentColor = ColorSchemeKeyTokens.OnSurface
+ val ContentColor = ColorSchemeKeyTokens.OnBackground
+ val DisabledContainerOpacity = 0.12f
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
val GradientEndOpacity = 0.0f
val GradientStartOpacity = 1.0f
- val IconColor = ColorSchemeKeyTokens.OnSurface
- val SecondaryContentColor = ColorSchemeKeyTokens.OnSurface
+ val IconColor = ColorSchemeKeyTokens.OnBackground
+ val SecondaryContentColor = ColorSchemeKeyTokens.OnBackground
+ val SecondaryContentOpacity = 0.8f
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt
index d0e84d0..02f0728 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/OutlinedIconButtonTokens.kt
@@ -19,7 +19,7 @@
package androidx.wear.compose.material3.tokens
internal object OutlinedIconButtonTokens {
- val ContentColor = ColorSchemeKeyTokens.OnSurface
+ val ContentColor = ColorSchemeKeyTokens.Primary
val DisabledContentColor = ColorSchemeKeyTokens.OnSurface
val DisabledContentOpacity = 0.38f
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
index 0190368..be763e7 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypeScaleTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -22,19 +22,23 @@
import androidx.compose.ui.unit.sp
internal object TypeScaleTokens {
+ val ArcLargeFont = TypefaceTokens.Brand
+ val ArcLargeLineHeight = 22.0.sp
+ val ArcLargeSize = 20.sp
+ val ArcLargeTracking = 0.4.sp
+ val ArcLargeWeight = 600.0f
+ val ArcLargeWidth = 100.0f
val ArcMediumFont = TypefaceTokens.Brand
val ArcMediumLineHeight = 18.0.sp
val ArcMediumSize = 15.sp
- val ArcMediumTracking = 0.2.sp
+ val ArcMediumTracking = 0.6.sp
val ArcMediumWeight = 600.0f
- val ArcMediumWeightProminent = 800.0f
val ArcMediumWidth = 100.0f
val ArcSmallFont = TypefaceTokens.Brand
val ArcSmallLineHeight = 16.0.sp
val ArcSmallSize = 14.sp
- val ArcSmallTracking = 0.2.sp
+ val ArcSmallTracking = 0.6.sp
val ArcSmallWeight = 560.0f
- val ArcSmallWeightProminent = 760.0f
val ArcSmallWidth = 100.0f
val BodyExtraSmallFont = TypefaceTokens.Brand
val BodyExtraSmallLineHeight = 12.0.sp
@@ -65,7 +69,7 @@
val BodySmallWeightProminent = 700.0f
val BodySmallWidth = 110.0f
val DisplayLargeFont = TypefaceTokens.Brand
- val DisplayLargeLineHeight = 44.0.sp
+ val DisplayLargeLineHeight = 46.0.sp
val DisplayLargeSize = 40.sp
val DisplayLargeTracking = 0.2.sp
val DisplayLargeWeight = 500.0f
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
index aeb00d3..0779680 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypefaceTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -24,7 +24,7 @@
internal object TypefaceTokens {
val Brand = FontFamily.SansSerif
+ val Plain = FontFamily.SansSerif
val WeightBold = FontWeight.Bold
- val WeightMedium = FontWeight.Medium
val WeightRegular = FontWeight.Normal
}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
index bb04daa..ea1dec4 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyKeyTokens.kt
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
internal enum class TypographyKeyTokens {
+ ArcLarge,
ArcMedium,
ArcSmall,
BodyExtraSmall,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
index cfb310e..9165f0d 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -26,6 +26,20 @@
import androidx.wear.compose.material3.DefaultTextStyle
internal object TypographyTokens {
+ val ArcLarge =
+ DefaultTextStyle.copy(
+ fontFamily =
+ Font(
+ DeviceFontFamilyName(TypeScaleTokens.ArcLargeFont.name),
+ weight = FontWeight(TypeScaleTokens.ArcLargeWeight.toInt()),
+ variationSettings = TypographyVariableFontsTokens.ArcLargeVariationSettings,
+ )
+ .toFontFamily(),
+ fontWeight = FontWeight(TypeScaleTokens.ArcLargeWeight.toInt()),
+ fontSize = TypeScaleTokens.ArcLargeSize,
+ lineHeight = TypeScaleTokens.ArcLargeLineHeight,
+ letterSpacing = TypeScaleTokens.ArcLargeTracking,
+ )
val ArcMedium =
DefaultTextStyle.copy(
fontFamily =
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
index dd22c78..6169c9f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/TypographyVariableFontsTokens.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-// VERSION: v0_65
+// VERSION: v0_71
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.wear.compose.material3.tokens
@@ -22,10 +22,15 @@
import androidx.compose.ui.text.font.FontVariation
internal object TypographyVariableFontsTokens {
+ val ArcLargeVariationSettings =
+ FontVariation.Settings(
+ FontVariation.Setting("wght", TypeScaleTokens.ArcLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.ArcLargeWidth),
+ )
val ArcMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.ArcMediumWidth),
FontVariation.Setting("wght", TypeScaleTokens.ArcMediumWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.ArcMediumWidth),
)
val ArcSmallVariationSettings =
FontVariation.Settings(
@@ -39,8 +44,8 @@
)
val BodyLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.BodyLargeWeight),
FontVariation.Setting("wdth", TypeScaleTokens.BodyLargeWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.BodyLargeWeight),
)
val BodyMediumVariationSettings =
FontVariation.Settings(
@@ -54,13 +59,13 @@
)
val DisplayLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.DisplayLargeWidth),
FontVariation.Setting("wght", TypeScaleTokens.DisplayLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.DisplayLargeWidth),
)
val DisplayMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.DisplayMediumWeight),
FontVariation.Setting("wdth", TypeScaleTokens.DisplayMediumWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.DisplayMediumWeight),
)
val DisplaySmallVariationSettings =
FontVariation.Settings(
@@ -69,13 +74,13 @@
)
val LabelLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.LabelLargeWeight),
FontVariation.Setting("wdth", TypeScaleTokens.LabelLargeWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.LabelLargeWeight),
)
val LabelMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.LabelMediumWidth),
FontVariation.Setting("wght", TypeScaleTokens.LabelMediumWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.LabelMediumWidth),
)
val LabelSmallVariationSettings =
FontVariation.Settings(
@@ -94,8 +99,8 @@
)
val NumeralLargeVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.NumeralLargeWidth),
FontVariation.Setting("wght", TypeScaleTokens.NumeralLargeWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.NumeralLargeWidth),
)
val NumeralMediumVariationSettings =
FontVariation.Settings(
@@ -104,8 +109,8 @@
)
val NumeralSmallVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wdth", TypeScaleTokens.NumeralSmallWidth),
FontVariation.Setting("wght", TypeScaleTokens.NumeralSmallWeight),
+ FontVariation.Setting("wdth", TypeScaleTokens.NumeralSmallWidth),
)
val TitleLargeVariationSettings =
FontVariation.Settings(
@@ -114,8 +119,8 @@
)
val TitleMediumVariationSettings =
FontVariation.Settings(
- FontVariation.Setting("wght", TypeScaleTokens.TitleMediumWeight),
FontVariation.Setting("wdth", TypeScaleTokens.TitleMediumWidth),
+ FontVariation.Setting("wght", TypeScaleTokens.TitleMediumWeight),
)
val TitleSmallVariationSettings =
FontVariation.Settings(
diff --git a/wear/compose/compose-material3/src/main/res/values-af/strings.xml b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
index 4951307..8ee35d5 100644
--- a/wear/compose/compose-material3/src/main/res/values-af/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-af/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekonde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tydperk"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Maand"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestig"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Het misluk"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sukses"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Maak op foon oop"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-am/strings.xml b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
index da5ab9c..d992a69 100644
--- a/wear/compose/compose-material3/src/main/res/values-am/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-am/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d ሰከንዶች</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ክፍለ ጊዜ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ቀን"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ወር"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ዓመት"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"አረጋግጥ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ቀጣይ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"አልተሳካም"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ተሳክቷል"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ስልክ ላይ ክፈት"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
index 450227f..95f1916 100644
--- a/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ar/strings.xml
@@ -45,13 +45,12 @@
<item quantity="one">ثانية واحدة</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"فترة"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"اليوم"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"الشهر"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"السنة"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأكيد"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"التالي"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"تعذر الإجراء"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"نجحَ الإجراء"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"فتح على الهاتف"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-as/strings.xml b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
index 178eaa4..81be021 100644
--- a/wear/compose/compose-material3/src/main/res/values-as/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-as/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d ছেকেণ্ড</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"পিৰিয়ড"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"দিন"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"মাহ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছৰ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"নিশ্চিত কৰক"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পৰৱৰ্তী"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"বিফল হৈছে"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"সফল"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ফ’নত খোলক"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-az/strings.xml b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
index de43d4f..67de877 100644
--- a/wear/compose/compose-material3/src/main/res/values-az/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-az/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d saniyə</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Müddət"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Gün"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ay"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"İl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Təsdiq edin"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Növbəti"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Alınmadı"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Alındı"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda aç"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
index eca919a..5d034c1 100644
--- a/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-b+sr+Latn/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspelo"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspelo"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-be/strings.xml b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
index dc5e832..60671a0 100644
--- a/wear/compose/compose-material3/src/main/res/values-be/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-be/strings.xml
@@ -39,13 +39,12 @@
<item quantity="other">%d секунды</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Перыяд"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Дзень"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месяц"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Пацвердзіць"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далей"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Памылка"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Выканана"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На тэлефоне"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
index 2866f08..6461c85 100644
--- a/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bg/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потвърждаване"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Напред"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Неуспешно"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успешно"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Отв. на тел."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
index 34a09f1..ee3b71a4 100644
--- a/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bn/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d সেকেন্ড</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"সময়সীমা"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"দিন"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"মাস"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"বছর"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"কনফার্ম করুন"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"পরবর্তী"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"সফল হয়নি"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"সফল হয়েছে"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ফোনে খুলুন"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
index 9e53fc8..eb4a11b 100644
--- a/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-bs/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mjesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrđivanje"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprijed"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neuspješno"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspješno"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvor. na tel."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
index 0c19945..5791ded 100644
--- a/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ca/strings.xml
@@ -41,4 +41,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Any"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirma"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Següent"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ha fallat"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Correcte"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Obre al telèfon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
index 98dc3ea..bfe3053 100644
--- a/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-cs/strings.xml
@@ -39,13 +39,12 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Období"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Den"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Měsíc"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdit"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Další"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nezdařilo se"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Hotovo"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otevřít v telefonu"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-da/strings.xml b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
index d1bd436..607983d 100644
--- a/wear/compose/compose-material3/src/main/res/values-da/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-da/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d sekunder</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Format"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Måned"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekræft"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Næste"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislykket"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Gennemført"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åbn på telefon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-de/strings.xml b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
index 9fdd438..1cb073d 100644
--- a/wear/compose/compose-material3/src/main/res/values-de/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-de/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d Sekunde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Zeitraum"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Tag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Monat"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jahr"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bestätigen"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Weiter"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Fehlgeschlagen"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Abgeschlossen"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Auf Smartphone öffnen"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-el/strings.xml b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
index 20374c9..fd2fba0 100644
--- a/wear/compose/compose-material3/src/main/res/values-el/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-el/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d δευτερόλεπτο</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Περίοδος"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ημέρα"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Μήνας"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Έτος"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Επιβεβαίωση"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Επόμενο"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Αποτυχία"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Επιτυχία"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Στο τηλέφωνο"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
index 58982b2..8c50eea 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rAU/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
index fb5c42c..c3f406c 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rCA/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
index 58982b2..8c50eea 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rGB/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
index 58982b2..8c50eea 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rIN/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d second</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Period"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Day"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Month"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
index 0bff2a4..b6b8b0a 100644
--- a/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-en-rXC/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Year"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirm"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Next"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Failed"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Success"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Open on phone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
index 5dac2b5..1eab19a 100644
--- a/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es-rUS/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Listo"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir en el teléfono"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-es/strings.xml b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
index 7ed6eed..5f76983 100644
--- a/wear/compose/compose-material3/src/main/res/values-es/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-es/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Año"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Siguiente"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Error"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Todo correcto"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ábrelo en el teléfono"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-et/strings.xml b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
index 898d4f1..03c079a 100644
--- a/wear/compose/compose-material3/src/main/res/values-et/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-et/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periood"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Päev"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Kuu"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Aasta"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kinnita"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Järgmine"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ebaõnnestus"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Õnnestus"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ava telefonis"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
index 202e7ee..8c273aa 100644
--- a/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-eu/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Epea"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Eguna"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Hilabetea"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Urtea"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Berretsi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Hurrengoa"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Huts egin du"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Eginda"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ireki telefonoan"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
index 8f9c137..064af35 100644
--- a/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fa/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d ثانیه</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"مدت زمان"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"روز"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ماه"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تأیید کردن"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"بعدی"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"انجام نشد"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"انجام شد"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"باز کردن در تلفن"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
index 0f0b50b..2a33291 100644
--- a/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fi/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekunti</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jakso"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Päivä"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Kuukausi"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Vuosi"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Vahvista"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seuraava"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Epäonnistui"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Onnistui"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Puhelimella"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
index cdc3ad5..4d883e4 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr-rCA/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d secondes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Jour"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mois"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Réussite"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouv. ds tél."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
index bdb1f1b..9be8df2 100644
--- a/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-fr/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d secondes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Période"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Jour"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mois"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Année"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmer"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Suivant"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Échec"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Opération réussie"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Ouvrir sur le téléphone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
index ad5ca33..c956d9e 100644
--- a/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gl/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d segundo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Día"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mes"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Erro"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Todo correcto"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
index b01e9be..ac3c8cd 100644
--- a/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-gu/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d સેકન્ડ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"અવધિ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"દિવસ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"મહિનો"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"વર્ષ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"કન્ફર્મ કરો"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"આગળ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"નિષ્ફળ થઈ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"સફળ થઈ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ફોન પર ખોલો"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
index c861b3af..34a8cc3 100644
--- a/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hi/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि करें"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अगला"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"काम नहीं हुआ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"काम हो गया"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फ़ोन पर खोलें"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
index af212a5..e2a13eb 100644
--- a/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hr/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d sekundi</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Razdoblje"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dan"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mjesec"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Godina"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalje"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nije uspjelo"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspjeh"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Na telefonu"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
index 2cce16e..625082c 100644
--- a/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hu/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d másodperc</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Időszak"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Nap"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Hónap"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Év"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Megerősítés"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Következő"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Sikertelen"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Sikerült"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Nyissa meg mobilon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
index cda99db..b3fb75b 100644
--- a/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-hy/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d վայրկյան</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Ժամանակահատված"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Օր"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ամիս"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Տարի"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Հաստատել"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Հաջորդը"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ձախողվել է"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Պատրաստ է"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Բացեք հեռախոսում"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-in/strings.xml b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
index 66a973e..c10be64 100644
--- a/wear/compose/compose-material3/src/main/res/values-in/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-in/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d Detik</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Jangka waktu"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Hari"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Bulan"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmasi"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Berikutnya"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Gagal"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Berhasil"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buka di ponsel"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-is/strings.xml b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
index 8983c3f..022daa2 100644
--- a/wear/compose/compose-material3/src/main/res/values-is/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-is/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d sekúndur</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punktur"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dagur"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mánuður"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ár"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Staðfesta"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Áfram"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mistókst"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Tókst"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Opna í símanum"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-it/strings.xml b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
index a9566b7..f592436 100644
--- a/wear/compose/compose-material3/src/main/res/values-it/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-it/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d secondo</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periodo"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Giorno"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mese"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Anno"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Conferma"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avanti"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Non riuscita"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Riuscita"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Su smartph."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
index 91f1e27..8e05ba1 100644
--- a/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-iw/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d שניות</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"תקופת זמן"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"יום"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"חודש"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"שנה"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"אישור"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"הבא"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"הפעולה נכשלה"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"הפעולה הצליחה"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"פתיחה בטלפון"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
index 65e9cc7..0604d20 100644
--- a/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ja/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"次へ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"スマホで開く"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
index 4f4734e..2558c1d 100644
--- a/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ka/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"წელი"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"დადასტურება"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"შემდეგი"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ვერ შესრულდა"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"შესრულდა"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ტელეფონში გახსნა"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
index e0d8b3d..6c27666 100644
--- a/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kk/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Кезең"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Күн"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ай"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Растау"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Келесі"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Расталмады."</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Расталды."</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефоннан ашыңыз."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-km/strings.xml b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
index 410a6f2..c2f812d 100644
--- a/wear/compose/compose-material3/src/main/res/values-km/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-km/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d វិនាទី</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"រយៈពេល"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ថ្ងៃ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ខែ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ឆ្នាំ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"បញ្ជាក់"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"បន្ទាប់"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"មិនបានសម្រេច"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ជោគជ័យ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"បើកលើទូរសព្ទ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
index 6ae6994..eac8e18 100644
--- a/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-kn/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d ಸೆಕೆಂಡ್ಗಳು</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ಅವಧಿ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ದಿನ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ತಿಂಗಳು"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ವರ್ಷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ದೃಢೀಕರಿಸಿ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ಮುಂದಿನದು"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ವಿಫಲವಾಗಿದೆ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ಯಶಸ್ವಿಯಾಗಿದೆ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ಫೋನ್ನಲ್ಲಿ ತೆರೆಯಿರಿ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
index 45f8446..58a3c15 100644
--- a/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ko/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d초</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"기간"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"일"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"월"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"년"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"확인"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"다음"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"실패"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"성공"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"휴대전화에서 열기"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
index 692726e..0cd62d9 100644
--- a/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ky/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Чекит"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Күн"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ай"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Жыл"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Ырастоо"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Кийинки"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ишке ашпады"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Ийгилик"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Телефондо ачуу"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
index 59d2d94..5914732 100644
--- a/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lo/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d ວິນາທີ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ໄລຍະເວລາ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ມື້"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ເດືອນ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ປີ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ຢືນຢັນ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ຕໍ່ໄປ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ບໍ່ສຳເລັດ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ສຳເລັດ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ເປີດໃນໂທລະສັບ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
index 615054e..81eeef5 100644
--- a/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lt/strings.xml
@@ -39,13 +39,12 @@
<item quantity="other">%d sekundžių</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Laikotarpis"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Diena"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mėnuo"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Metai"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Patvirtinti"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Kitas"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nepavyko"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pavyko"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atidaryti telefone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
index 08f2d2c..eb34815 100644
--- a/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-lv/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d sekundes</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periods"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Diena"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mēnesis"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Gads"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Apstiprināt"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tālāk"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neizdevās"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Izdevās"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Atvērt tālrunī"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
index a093425..7364597 100644
--- a/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mk/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ден"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месец"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Следно"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Неуспешно"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успешно"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Отвори на телефонот"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
index 36d8b54..102fbe14 100644
--- a/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ml/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d സെക്കൻഡ്</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"കാലയളവ്"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ദിവസം"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"മാസം"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"വർഷം"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"സ്ഥിരീകരിക്കുക"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"അടുത്തത്"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"പരാജയപ്പെട്ടു"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"വിജയിച്ചു"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ഫോണിൽ തുറക്കൂ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
index 629e16a..4d8658f 100644
--- a/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mn/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d секунд</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Хугацаа"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Өдөр"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Сар"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Он"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Баталгаажуулах"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Дараах"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Амжилтгүй"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Амжилттай"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Утсанд нээх"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
index 904ad4f..dbcc0e28 100644
--- a/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-mr/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d सेकंद</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"कालावधी"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"दिवस"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"महिना"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"वर्ष"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"कन्फर्म करा"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"पुढील"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"अयशस्वी"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"यशस्वी झाले"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फोनवर उघडा"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
index df4b8c9..b7d0922 100644
--- a/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ms/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d Saat</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Tempoh"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Hari"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Bulan"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Tahun"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Sahkan"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seterusnya"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Gagal"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Berjaya"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buka pada telefon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-my/strings.xml b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
index aa85494..4540c64 100644
--- a/wear/compose/compose-material3/src/main/res/values-my/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-my/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d စက္ကန့်</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"အချိန်ကာလ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ရက်"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"လ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"နှစ်"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"အတည်ပြုရန်"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ရှေ့သို့"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"မအောင်မြင်ပါ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"အောင်မြင်သည်"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ဖုန်း၌ဖွင့်ရန်"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
index 0d9dbd3..5f8fa3b 100644
--- a/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nb/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Måned"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekreft"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Neste"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislyktes"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Vellykket"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Åpne på tlf."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
index 4f5d479..d216dc7 100644
--- a/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ne/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d सेकेन्ड</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"अवधि"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"दिन"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"महिना"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"साल"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"पुष्टि गर्नुहोस्"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"अर्को"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"पुष्टि गर्न सकिएन"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"पुष्टि गरियो"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"फोनमा खोल्नुहोस्"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
index 8561545..c57422b 100644
--- a/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-nl/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d seconde</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periode"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Maand"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Jaar"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bevestigen"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Volgende"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Mislukt"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Geslaagd"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Openen op telefoon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-or/strings.xml b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
index 7701b598..22d20ef 100644
--- a/wear/compose/compose-material3/src/main/res/values-or/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-or/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d ସେକେଣ୍ଡ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ଅବଧି"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ଦିନ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ମାସ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ବର୍ଷ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ପରବର୍ତ୍ତୀ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ବିଫଳ ହୋଇଛି"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ସଫଳ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ଫୋନରେ ଖୋଲନ୍ତୁ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
index 1000cde..7666dd8 100644
--- a/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pa/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">%d ਸਕਿੰਟ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"ਮਿਆਦ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"ਦਿਨ"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"ਮਹੀਨਾ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ਸਾਲ"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ਤਸਦੀਕ ਕਰੋ"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ਅੱਗੇ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ਅਸਫਲ ਰਿਹਾ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"ਸਫਲ ਰਿਹਾ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ਫ਼ੋਨ \'ਤੇ ਖੋਲ੍ਹੋ"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
index 5d56d82..d330a6f 100644
--- a/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pl/strings.xml
@@ -39,13 +39,12 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kropka"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dzień"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Miesiąc"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potwierdź"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Dalej"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Niepowodzenie"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Udało się"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otwórz na telefonie"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
index 77c3710..9552f13 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rBR/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d segundos</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mês"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
index 416cbc0..b200da8 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt-rPT/strings.xml
@@ -41,4 +41,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Seguinte"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falhou"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Concluído"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abrir no tel."</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
index 77c3710..9552f13 100644
--- a/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-pt/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d segundos</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Período"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dia"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mês"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Ano"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmar"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Avançar"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Falha"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Pronto"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Abra no smartphone"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
index f9723df..2f2593f 100644
--- a/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ro/strings.xml
@@ -36,13 +36,12 @@
<item quantity="one">%d secundă</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Perioada"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Zi"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Lună"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"An"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Confirmă"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Înainte"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Eroare"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Succes"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Pe telefon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
index e01e3c3..9d251bd 100644
--- a/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ru/strings.xml
@@ -39,13 +39,12 @@
<item quantity="other">%d секунды</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"День"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месяц"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Год"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Подтвердить"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далее"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Ошибка"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Открыть на телефоне"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-si/strings.xml b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
index 6f3d4c6..f8b70ed 100644
--- a/wear/compose/compose-material3/src/main/res/values-si/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-si/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">තත්පර %d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"කාල පරිච්ඡේදය"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"දවස"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"මාසය"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"වසර"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"තහවුරු කරන්න"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"මීළඟ"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"අසමත් විය"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"සාර්ථකයි"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"දුරකථනයෙන් විවෘත කරන්න"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
index 8840530..10b51d9 100644
--- a/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sk/strings.xml
@@ -39,13 +39,12 @@
<item quantity="one">%d sekunda</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Obdobie"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Deň"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mesiac"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Rok"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potvrdiť"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Ďalej"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neúspešné"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Podarilo sa"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Otvorte v telefóne"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
index cc79d76..8bbca36 100644
--- a/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sl/strings.xml
@@ -44,4 +44,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Leto"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Potrdi"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Naprej"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Neuspešno"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Uspešno"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Odpri v telefonu"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
index 411d53e..392a80a 100644
--- a/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sq/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekondë</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Periudha"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dita"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Muaji"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Viti"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Konfirmo"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Para"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Dështoi"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Me sukses"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Hape në telefon"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
index 661b6d2..4ecd9ec 100644
--- a/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sr/strings.xml
@@ -36,13 +36,12 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Период"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Дан"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Месец"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Година"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Потврди"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Даље"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Није успело"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Успело"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефону"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
index b395ba1d..af57d5c 100644
--- a/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sv/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d sekund</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Punkt"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Dag"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Månad"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"År"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Bekräfta"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Nästa"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Misslyckades"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Klart"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"På telefonen"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
index aad1959..1e84af2 100644
--- a/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-sw/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">Sekunde %d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Kipindi"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Siku"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Mwezi"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Mwaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Thibitisha"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Endelea"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Imeshindwa"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Imemaliza"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Fungua kwenye simu"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
index aacd3cd..2e9368a 100644
--- a/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ta/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d வினாடி</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"கால இடைவெளி"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"நாள்"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"மாதம்"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ஆண்டு"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"உறுதிசெய்யும்"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"அடுத்ததற்குச் செல்லும்"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"தோல்வி"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"முடிந்தது"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"மொபைலில் திற"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-te/strings.xml b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
index 2374063..58ac088 100644
--- a/wear/compose/compose-material3/src/main/res/values-te/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-te/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"సంవత్సరం"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"నిర్ధారించండి"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"తర్వాత"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"విఫలమైంది"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"విజయవంతమైంది"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"ఫోన్లో తెరు"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-th/strings.xml b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
index d1ad793..c8e01f1 100644
--- a/wear/compose/compose-material3/src/main/res/values-th/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-th/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"ปี"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"ยืนยัน"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"ถัดไป"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ไม่สำเร็จ"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"สำเร็จ"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"เปิดในโทรศัพท์"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
index fe20f09..e8780c2 100644
--- a/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tl/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Taon"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Kumpirmahin"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Susunod"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Nabigo"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Matagumpay"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Buksan"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
index 3ec35be..bb0315d 100644
--- a/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-tr/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d Saniye</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Aralık"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Gün"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Ay"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yıl"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Onayla"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Sonraki"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Başarısız"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Başarılı"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda aç"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
index bb037ea..855f9b8 100644
--- a/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uk/strings.xml
@@ -39,13 +39,12 @@
<item quantity="other">%d секунди</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Період"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"День"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Місяць"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Рік"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Підтвердити"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Далі"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Помилка"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Готово"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"На телефоні"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
index 6f902ab..1f94321 100644
--- a/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-ur/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d سیکنڈ</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"وقفہ"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"دن"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"مہینہ"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"سال"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"تصدیق کریں"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"اگلا"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"ناکام ہوا"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"کامیاب"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"فون پر کھولیں"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
index 4b1121e..433f419 100644
--- a/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-uz/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Yil"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Tasdiqlash"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Keyingisi"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Bajarilmadi"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Bajarildi"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Telefonda"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
index 3b40f28..7daf971 100644
--- a/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-vi/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d giây</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Khoảng thời gian"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Ngày"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Tháng"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Năm"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Xác nhận"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Tiếp theo"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Lỗi"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Thành công"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Mở trên điện thoại"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
index 14ce25a..c4f4636 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rCN/strings.xml
@@ -38,4 +38,7 @@
<string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"确认"</string>
<string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一个"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失败"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手机上打开"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
index 0d30e02..c7a2f65 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rHK/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d 秒</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"時段"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一步"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機開啟"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
index 3e569a9..52e3880 100644
--- a/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zh-rTW/strings.xml
@@ -33,13 +33,12 @@
<item quantity="one">%d 秒</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"期間"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"日"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"月"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"年"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"確認"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"下一個"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"失敗"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"成功"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"在手機上開啟"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
index 43167ed..86ca061 100644
--- a/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
+++ b/wear/compose/compose-material3/src/main/res/values-zu/strings.xml
@@ -33,13 +33,12 @@
<item quantity="other">Imizuzwana engu-%d</item>
</plurals>
<string name="wear_m3c_time_picker_period" msgid="5567285614451063120">"Isikhathi"</string>
- <!-- no translation found for wear_m3c_date_picker_day (8932770593644830235) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_month (5962969526377479136) -->
- <skip />
- <!-- no translation found for wear_m3c_date_picker_year (4697690064312147449) -->
- <skip />
+ <string name="wear_m3c_date_picker_day" msgid="8932770593644830235">"Usuku"</string>
+ <string name="wear_m3c_date_picker_month" msgid="5962969526377479136">"Inyanga"</string>
+ <string name="wear_m3c_date_picker_year" msgid="4697690064312147449">"Unyaka"</string>
<string name="wear_m3c_picker_confirm_button_content_description" msgid="8440144719909288057">"Qinisekisa"</string>
- <!-- no translation found for wear_m3c_picker_next_button_content_description (3346011303652897029) -->
- <skip />
+ <string name="wear_m3c_picker_next_button_content_description" msgid="3346011303652897029">"Okulandelayo"</string>
+ <string name="wear_m3c_confirmation_failure_message" msgid="6690105830380889949">"Yehlulekile"</string>
+ <string name="wear_m3c_confirmation_success_message" msgid="7045594078216655038">"Impumelelo"</string>
+ <string name="wear_m3c_open_on_phone" msgid="5829463187924353601">"Vula efonini"</string>
</resources>
diff --git a/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt b/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
new file mode 100644
index 0000000..5d231b0
--- /dev/null
+++ b/wear/compose/compose-material3/src/test/kotlin/androidx/wear/compose/material3/ProgressIndicatorTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ProgressIndicatorTest {
+
+ @Test
+ fun coerce_progress_fraction_overflow_enabled() {
+ assertEquals(0.2f, coerceProgress(0.2f, true))
+ }
+
+ @Test
+ fun coerce_progress_fraction_greater_than_one_overflow_enabled() {
+ assertEquals(0.2f, coerceProgress(1.2f, true))
+ }
+
+ @Test
+ fun coerce_progress_integer_greater_than_one_overflow_enabled() {
+ assertEquals(1.0f, coerceProgress(2.0f, true))
+ }
+
+ @Test
+ fun coerce_progress_zero_overflow_enabled() {
+ assertEquals(0.0f, coerceProgress(0.0f, true))
+ }
+
+ @Test
+ fun coerce_progress_negative_overflow_enabled() {
+ assertEquals(0.0f, coerceProgress(-1.0f, true))
+ }
+
+ @Test
+ fun coerce_progress_fraction_overflow_disabled() {
+ assertEquals(0.2f, coerceProgress(0.2f, false))
+ }
+
+ @Test
+ fun coerce_progress_fraction_greater_than_one_overflow_disabled() {
+ assertEquals(1.0f, coerceProgress(1.2f, false))
+ }
+
+ @Test
+ fun coerce_progress_integer_greater_than_one_overflow_disabled() {
+ assertEquals(1.0f, coerceProgress(2.0f, false))
+ }
+
+ @Test
+ fun coerce_progress_zero_overflow_disabled() {
+ assertEquals(0.0f, coerceProgress(0.0f, false))
+ }
+
+ @Test
+ fun coerce_progress_negative_overflow_disabled() {
+ assertEquals(0.0f, coerceProgress(-1.0f, false))
+ }
+}
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index bb35fa2..8d27898 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -41,11 +41,10 @@
implementation(libs.kotlinStdlib)
implementation("androidx.navigation:navigation-common:2.6.0")
implementation("androidx.navigation:navigation-compose:2.6.0")
- implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+ implementation("androidx.profileinstaller:profileinstaller:1.4.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":navigation:navigation-common"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 8886921..d0a3eb0 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 37
- versionName "1.37"
+ versionCode 39
+ versionName "1.39"
}
buildTypes {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index ed9e3703..98fd84a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -23,6 +23,7 @@
import androidx.wear.compose.foundation.samples.CurvedBackground
import androidx.wear.compose.foundation.samples.CurvedBottomLayout
import androidx.wear.compose.foundation.samples.CurvedFixedSize
+import androidx.wear.compose.foundation.samples.CurvedFontHeight
import androidx.wear.compose.foundation.samples.CurvedFontWeight
import androidx.wear.compose.foundation.samples.CurvedFonts
import androidx.wear.compose.foundation.samples.CurvedRowAndColumn
@@ -116,6 +117,7 @@
ComposableDemo("Curved layout direction") { CurvedLayoutDirection() },
ComposableDemo("Background") { CurvedBackground() },
ComposableDemo("Font Weight") { CurvedFontWeight() },
+ ComposableDemo("Font Height") { CurvedFontHeight() },
ComposableDemo("Fonts") { CurvedFonts() },
ComposableDemo("Curved Icons") { CurvedIconsDemo() },
ComposableDemo("Letter Spacing (em)") { CurvedSpacingEmDemo() },
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 26090ac6..fa16748 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -113,6 +113,16 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+
+ <activity
+ android:name=".ButtonActivity"
+ android:theme="@style/AppTheme"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.wear.compose.integration.macrobenchmark.target.BUTTON_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
<uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ButtonActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ButtonActivity.kt
new file mode 100644
index 0000000..cbdcb73
--- /dev/null
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/ButtonActivity.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.Text
+
+class ButtonActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MaterialTheme {
+ Column(
+ Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceAround,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ repeat(4) {
+ Button(
+ modifier =
+ Modifier.semantics {
+ contentDescription = numberedContentDescription(it)
+ },
+ onClick = {}
+ ) {
+ Text("Button $it")
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/Common.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/Common.kt
index af8e91f..61f7253 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/Common.kt
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/Common.kt
@@ -17,3 +17,5 @@
package androidx.wear.compose.integration.macrobenchmark.target
internal val CONTENT_DESCRIPTION = "find-me"
+
+internal fun numberedContentDescription(n: Int) = "find-me-$n"
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/AnimatedTextBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/AnimatedTextBenchmark.kt
index 925f27e..a8d434d 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/AnimatedTextBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/AnimatedTextBenchmark.kt
@@ -27,8 +27,6 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.uiautomator.By
-import androidx.wear.compose.integration.macrobenchmark.test.disableChargingExperience
-import androidx.wear.compose.integration.macrobenchmark.test.enableChargingExperience
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.After
@@ -56,7 +54,7 @@
@OptIn(ExperimentalMetricApi::class)
@Test
- fun testAnimatedText() {
+ fun start() {
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics =
@@ -68,7 +66,7 @@
iterations = 10,
setupBlock = {
val intent = Intent()
- intent.action = ANIMATED_TEXT_ACTION
+ intent.action = ANIMATED_TEXT_ACTIVITY
startActivityAndWait(intent)
}
) {
@@ -89,6 +87,6 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ANIMATED_TEXT_ACTION = "$PACKAGE_NAME.ANIMATED_TEXT_ACTIVITY"
+ private const val ANIMATED_TEXT_ACTIVITY = "$PACKAGE_NAME.ANIMATED_TEXT_ACTIVITY"
}
}
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
index f961dd9..ffb459d 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.integration.macrobenchmark.test
+package androidx.wear.compose.integration.macrobenchmark
import android.content.Intent
import androidx.benchmark.macro.MacrobenchmarkScope
@@ -81,7 +81,7 @@
packageName = PACKAGE_NAME,
profileBlock = {
val intent = Intent()
- intent.action = ACTION
+ intent.action = BASELINE_ACTIVITY
startActivityAndWait(intent)
testDestination(description = BUTTONS)
testDestination(description = CARDS)
@@ -163,8 +163,7 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ACTION =
- "androidx.wear.compose.integration.macrobenchmark.target.BASELINE_ACTIVITY"
+ private const val BASELINE_ACTIVITY = "${PACKAGE_NAME}.BASELINE_ACTIVITY"
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ButtonBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ButtonBenchmark.kt
new file mode 100644
index 0000000..ecdfb59
--- /dev/null
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ButtonBenchmark.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.uiautomator.By
+import androidx.testutils.createCompilationParams
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class ButtonBenchmark(private val compilationMode: CompilationMode) {
+ @get:Rule val benchmarkRule = MacrobenchmarkRule()
+
+ @Before
+ fun setUp() {
+ disableChargingExperience()
+ }
+
+ @After
+ fun destroy() {
+ enableChargingExperience()
+ }
+
+ @Test
+ fun start() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = compilationMode,
+ iterations = 10,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = BUTTON_ACTIVITY
+ startActivityAndWait(intent)
+ }
+ ) {
+ val buttons = buildList {
+ repeat(4) { add(device.findObject(By.desc(numberedContentDescription(it)))) }
+ }
+ repeat(3) {
+ for (button in buttons) {
+ button.click()
+ device.waitForIdle()
+ }
+ Thread.sleep(500)
+ }
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
+ private const val BUTTON_ACTIVITY = "${PACKAGE_NAME}.BUTTON_ACTIVITY"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
index 1cfa14d..e17430d 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package androidx.wear.compose.integration.macrobenchmark.test
+package androidx.wear.compose.integration.macrobenchmark
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
internal val CONTENT_DESCRIPTION = "find-me"
+internal fun numberedContentDescription(n: Int) = "find-me-$n"
+
internal fun disableChargingExperience() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val device = UiDevice.getInstance(instrumentation)
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/PositionIndicatorBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/PositionIndicatorBenchmark.kt
index ca88618..22b55a3 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/PositionIndicatorBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/PositionIndicatorBenchmark.kt
@@ -24,8 +24,6 @@
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.testutils.createCompilationParams
-import androidx.wear.compose.integration.macrobenchmark.test.disableChargingExperience
-import androidx.wear.compose.integration.macrobenchmark.test.enableChargingExperience
import java.lang.Thread.sleep
import org.junit.After
import org.junit.Before
@@ -58,7 +56,7 @@
iterations = 5,
setupBlock = {
val intent = Intent()
- intent.action = ACTION
+ intent.action = POSITION_INDICATOR_ACTIVITY
startActivityAndWait(intent)
}
) {
@@ -112,9 +110,8 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ACTION =
- "androidx.wear.compose.integration.macrobenchmark.target" +
- ".POSITION_INDICATOR_ACTIVITY"
+ private const val POSITION_INDICATOR_ACTIVITY =
+ "${PACKAGE_NAME}.POSITION_INDICATOR_ACTIVITY"
private const val INCREASE_POSITION = "PI_INCREASE_POSITION"
private const val DECREASE_POSITION = "PI_DECREASE_POSITION"
private const val CHANGE_VISIBILITY_SHOW = "PI_VISIBILITY_SHOW"
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
index bf26054..b3704cc 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.integration.macrobenchmark.test
+package androidx.wear.compose.integration.macrobenchmark
import android.content.Intent
import android.graphics.Point
@@ -55,7 +55,7 @@
iterations = 10,
setupBlock = {
val intent = Intent()
- intent.action = ACTION
+ intent.action = SCROLL_ACTIVITY
startActivityAndWait(intent)
}
) {
@@ -71,8 +71,7 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ACTION =
- "androidx.wear.compose.integration.macrobenchmark.target.SCROLL_ACTIVITY"
+ private const val SCROLL_ACTIVITY = "${PACKAGE_NAME}.SCROLL_ACTIVITY"
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
index 9a05809..a692163 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.integration.macrobenchmark.test
+package androidx.wear.compose.integration.macrobenchmark
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
@@ -48,16 +48,19 @@
}
@Test
- fun startup() =
+ fun start() =
benchmarkRule.measureStartup(
compilationMode = compilationMode,
startupMode = startupMode,
- packageName = "androidx.wear.compose.integration.macrobenchmark.target"
+ packageName = PACKAGE_NAME
) {
- action = "androidx.wear.compose.integration.macrobenchmark.target.WEAR_STARTUP_ACTIVITY"
+ action = WEAR_STARTUP_ACTIVITY
}
companion object {
+ private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
+ private const val WEAR_STARTUP_ACTIVITY = "${PACKAGE_NAME}.WEAR_STARTUP_ACTIVITY"
+
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
fun parameters() = createStartupCompilationParams()
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToDismissBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToDismissBenchmark.kt
index c8e9044..fc09578 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToDismissBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToDismissBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.compose.integration.macrobenchmark.test
+package androidx.wear.compose.integration.macrobenchmark
import android.content.Intent
import androidx.benchmark.macro.CompilationMode
@@ -55,7 +55,7 @@
iterations = 10,
setupBlock = {
val intent = Intent()
- intent.action = ACTION
+ intent.action = SWIPE_TO_DISMISS_ACTIVITY
startActivityAndWait(intent)
}
) {
@@ -76,8 +76,7 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ACTION =
- "androidx.wear.compose.integration.macrobenchmark.target.SWIPE_TO_DISMISS_ACTIVITY"
+ private const val SWIPE_TO_DISMISS_ACTIVITY = "${PACKAGE_NAME}.SWIPE_TO_DISMISS_ACTIVITY"
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToRevealBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToRevealBenchmark.kt
index 2908e234..9845b66 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToRevealBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeToRevealBenchmark.kt
@@ -24,9 +24,6 @@
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.testutils.createCompilationParams
-import androidx.wear.compose.integration.macrobenchmark.test.CONTENT_DESCRIPTION
-import androidx.wear.compose.integration.macrobenchmark.test.disableChargingExperience
-import androidx.wear.compose.integration.macrobenchmark.test.enableChargingExperience
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -58,7 +55,7 @@
iterations = 10,
setupBlock = {
val intent = Intent()
- intent.action = ACTION
+ intent.action = SWIPE_TO_REVEAL_ACTIVITY
startActivityAndWait(intent)
}
) {
@@ -78,8 +75,7 @@
companion object {
private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
- private const val ACTION =
- "androidx.wear.compose.integration.macrobenchmark.target.SWIPE_TO_REVEAL_ACTIVITY"
+ private const val SWIPE_TO_REVEAL_ACTIVITY = "${PACKAGE_NAME}.SWIPE_TO_REVEAL_ACTIVITY"
@Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index b3b2b3e..c3306bc 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -408,6 +408,16 @@
package androidx.wear.protolayout.expression.util {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public final class DynamicDateFormat {
+ ctor public DynamicDateFormat(String pattern);
+ ctor public DynamicDateFormat(String pattern, optional java.time.ZoneId timeZone);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant instant);
+ method public java.time.ZoneId getTimeZone();
+ method public void setTimeZone(java.time.ZoneId);
+ property public final java.time.ZoneId timeZone;
+ }
+
public final class DynamicFormatter {
ctor public DynamicFormatter();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(String format, java.lang.Object?... args);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index b3b2b3e..c3306bc 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -408,6 +408,16 @@
package androidx.wear.protolayout.expression.util {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public final class DynamicDateFormat {
+ ctor public DynamicDateFormat(String pattern);
+ ctor public DynamicDateFormat(String pattern, optional java.time.ZoneId timeZone);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant instant);
+ method public java.time.ZoneId getTimeZone();
+ method public void setTimeZone(java.time.ZoneId);
+ property public final java.time.ZoneId timeZone;
+ }
+
public final class DynamicFormatter {
ctor public DynamicFormatter();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(String format, java.lang.Object?... args);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt
new file mode 100644
index 0000000..653b51fb
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicDateFormat.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.util
+
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone
+import androidx.annotation.VisibleForTesting
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.util.ArrayDeque
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Equivalent to [android.icu.text.SimpleDateFormat], but generates a [DynamicString] based on a
+ * [DynamicInstant].
+ *
+ * See [android.icu.text.SimpleDateFormat] documentation for the pattern syntax.
+ *
+ * Literal patterns are fully supported, including quotes (`'`) or non-letters (e.g. `:`).
+ *
+ * Currently this implementation only supports hour (`HKhk`), minute (`m`), and AM/PM (`a`)
+ * patterns. Every other letter will throw [IllegalArgumentException].
+ *
+ * NOTE: [DynamicDateFormat] uses `Locale.getDefault(Locale.Category.FORMAT)` at the time of calling
+ * [format] for AM/PM markers. This can change on the remote side, which would cause a mismatch
+ * between the locally-formatted AM/PM and the remotely-formatted numbers, unless the provider sends
+ * a newly formatted [DynamicString] (using a new invocation of [format]).
+ *
+ * Example usage:
+ * ```
+ * // This statement:
+ * DynamicDateFormat(pattern = "HH:mm", timeZone = zone).format(dynamicInstant)
+ * // Generates an equivalent of:
+ * dynamicInstant
+ * .getHour(zone)
+ * .format(DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(2).build())
+ * .concat(DynamicString.constant(":"))
+ * .concat(
+ * dynamicInstant.getMinute(zone)
+ * .format(DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(2).build())
+ * )
+ * ```
+ *
+ * @property timeZone The time zone used when extracting time parts from the [DynamicInstant]
+ * provided to [format], defaults to [ZoneId.systemDefault].
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+public class DynamicDateFormat
+@VisibleForTesting
+internal constructor(
+ private val pattern: String,
+ // TODO: b/297323092 - Allow providing in the constructor for both local and remote evaluation.
+ // Currently only used for AM/PM.
+ private val locale: Locale?,
+ public var timeZone: ZoneId,
+) {
+ /**
+ * Constructs a [DynamicDateFormat].
+ *
+ * @param pattern The pattern to use when calling [format], see
+ * [android.icu.text.SimpleDateFormat] for general syntax and [DynamicDateFormat] for the
+ * supported subset of features.
+ * @param timeZone The time zone used when extracting time parts from the [DynamicInstant]
+ * provided to [format], defaults to [ZoneId.systemDefault].
+ */
+ @JvmOverloads
+ public constructor(
+ pattern: String,
+ timeZone: ZoneId = ZoneId.systemDefault()
+ ) : this(pattern, locale = null, timeZone)
+
+ init {
+ require(pattern.count { it == '\'' } % 2 == 0) { "Unterminated quote" }
+ }
+
+ private val _locale: Locale
+ get() = locale ?: Locale.getDefault(Locale.Category.FORMAT)
+
+ private val patternParts: List<Part> = extractPatternParts().mergeConstants().toList()
+
+ /**
+ * Formats the [DynamicInstant] into a date/time [DynamicString].
+ *
+ * @param instant The [DynamicInstant] to format, defaults to
+ * [DynamicInstant.platformTimeWithSecondsPrecision].
+ */
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ @JvmOverloads
+ public fun format(
+ instant: DynamicInstant = DynamicInstant.platformTimeWithSecondsPrecision(),
+ ): DynamicString =
+ patternParts
+ .map { it.format(instant) }
+ .ifEmpty { listOf(DynamicString.constant("")) }
+ .reduce { acc, formattedSection -> acc.concat(formattedSection) }
+
+ /** Builds a [Part] sequence from the [pattern]. */
+ private fun extractPatternParts(): Sequence<Part> = sequence {
+ val patternLeft: ArrayDeque<Token> = pattern.tokenize()
+ // Taking tokens from the left and yielding Parts until there's no more tokens.
+ while (patternLeft.isNotEmpty()) {
+ if (patternLeft.first().isUnescapedQuote) {
+ yield(ConstantPart(takeQuotedConstant(patternLeft)))
+ } else if (patternLeft.first().isConstant) {
+ yield(ConstantPart(takeNonLetterConstant(patternLeft)))
+ } else {
+ // Not constant (dynamic pattern).
+ yield(DynamicPart(takeDynamic(patternLeft)))
+ }
+ }
+ }
+
+ /**
+ * Returns everything until the closing quote, and removes it (including the closing quote) from
+ * [patternLeft].
+ *
+ * Assumes the constructor checks that the amount of quotes are even, and all are closed.
+ */
+ private fun takeQuotedConstant(patternLeft: ArrayDeque<Token>): String {
+ patternLeft.removeFirst()
+ val result = patternLeft.takeWhile { !it.isUnescapedQuote }.asString()
+ patternLeft.removeFirst(result.length + 1)
+ return result
+ }
+
+ /** Returns all upcoming non-letter constants, and removes it from [patternLeft]. */
+ private fun takeNonLetterConstant(patternLeft: ArrayDeque<Token>): String {
+ val result = patternLeft.takeWhile { it.isConstant }.asString()
+ patternLeft.removeFirst(result.length)
+ return result
+ }
+
+ /**
+ * Returns the next dynamic section in the pattern, which is basically the repetition of the
+ * first character.
+ */
+ private fun takeDynamic(patternLeft: ArrayDeque<Token>): String =
+ patternLeft
+ // Taking repetitions to determine padding length.
+ .takeWhile { it == patternLeft.first() }
+ .asString()
+ .also { patternLeft.removeFirst(it.length) }
+
+ /** Merges repeated constants to reduce the amount of concatenation nodes. */
+ private fun Sequence<Part>.mergeConstants(): Sequence<Part> = sequence {
+ val empty = ConstantPart("") // Saving an allocation every time we reset lastConstant.
+ var lastConstant = empty
+ forEach { nextSection ->
+ if (nextSection is ConstantPart) {
+ lastConstant += nextSection
+ } else {
+ lastConstant.ifNotEmpty { yield(it) }
+ lastConstant = empty
+ yield(nextSection)
+ }
+ }
+ lastConstant.ifNotEmpty { yield(it) }
+ }
+
+ /** Either a [ConstantPart] or [DynamicPart] part of the pattern. */
+ private sealed interface Part {
+ fun format(instant: DynamicInstant): DynamicString
+ }
+
+ /** A pattern section that is built with [DynamicString.constant]. */
+ private data class ConstantPart(val value: String) : Part {
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ override fun format(instant: DynamicInstant): DynamicString = DynamicString.constant(value)
+
+ operator fun plus(other: ConstantPart) = ConstantPart(value + other.value)
+
+ /** Invokes [block] with `this` if `value != ""`. */
+ inline fun ifNotEmpty(block: (ConstantPart) -> Unit) {
+ if (value.isNotEmpty()) block(this)
+ }
+ }
+
+ /** A pattern section that is formatted into a [DynamicString] based on [~]. */
+ private inner class DynamicPart(code: Char, val length: Int) : Part {
+ private val dynamicBuilder: (DynamicInstant) -> DynamicString
+
+ init {
+ dynamicBuilder =
+ when (code) {
+ 'H' -> this::buildHourInDay0To23
+ 'k' -> this::buildHourInDay1To24
+ 'K' -> this::buildHourInAmPm0To11
+ 'h' -> this::buildHourInAmPm1To12
+ 'm' -> this::buildMinuteInHour
+ 'a' -> this::buildAmPmMarker
+ else -> throw IllegalArgumentException("Illegal pattern character '$code'")
+ }
+ }
+
+ constructor(value: String) : this(value[0], value.length)
+
+ override fun format(instant: DynamicInstant): DynamicString = dynamicBuilder(instant)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInDay0To23(instant: DynamicInstant): DynamicString =
+ instant.getHour(timeZone).format(intFormatter)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInDay1To24(instant: DynamicInstant): DynamicString {
+ val hour = instant.getHour(timeZone)
+ return DynamicInt32.onCondition(hour.eq(0)).use(24).elseUse(hour).format(intFormatter)
+ }
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInAmPm0To11(instant: DynamicInstant): DynamicString =
+ instant.getHour(timeZone).rem(12).format(intFormatter)
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildHourInAmPm1To12(instant: DynamicInstant): DynamicString {
+ val hourRem12: DynamicInt32 = instant.getHour(timeZone).rem(12)
+ return DynamicInt32.onCondition(hourRem12.eq(0))
+ .use(12)
+ .elseUse(hourRem12)
+ .format(intFormatter)
+ }
+
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildMinuteInHour(instant: DynamicInstant): DynamicString =
+ instant.getMinute(timeZone).format(intFormatter)
+
+ // NOTE: This dynamic part ignores length.
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ private fun buildAmPmMarker(instant: DynamicInstant): DynamicString {
+ // Using SimpleDateFormat to determine what AM/PM formats to in the given locale.
+ val simpleDateFormat =
+ SimpleDateFormat("a", _locale).also { it.timeZone = TimeZone.getTimeZone("UTC") }
+ // Epoch is AM in UTC.
+ val am = simpleDateFormat.format(Date.from(Instant.EPOCH))
+ // Epoch + 12h is PM in UTC.
+ val pm = simpleDateFormat.format(Date.from(Instant.EPOCH.plus(Duration.ofHours(12))))
+
+ return DynamicString.onCondition(instant.getHour(timeZone).lt(12)).use(am).elseUse(pm)
+ }
+
+ /** Returns a formatter based on the desired [length]. */
+ private val intFormatter
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ get() = DynamicInt32.IntFormatter.Builder().setMinIntegerDigits(length).build()
+ }
+
+ /**
+ * Tokenizes the characters of the string, by replacing every double quotes (`''`) with
+ * [LiteralQuoteToken] and everything else with [CharToken].
+ */
+ private fun String.tokenize(): ArrayDeque<Token> =
+ split("''")
+ .asSequence()
+ .flatMap { it.map(::CharToken) + LiteralQuoteToken }
+ .toCollection(ArrayDeque())
+ .also { it.removeLast() }
+
+ /** Either a "normal" character or an escaped quote (`'`). */
+ private sealed interface Token {
+ /** The value of the token, which should be used based on [isUnescapedQuote]. */
+ val value: Char
+
+ /** Whether token is an unescaped quote (a single `'`). */
+ val isUnescapedQuote: Boolean
+
+ /** Whether the token is a constant (vs dynamic). */
+ val isConstant: Boolean
+ }
+
+ /** A non-literal token that can be a quote (`'`), a constant ([^A-Za-z]), or a pattern. */
+ private data class CharToken(override val value: Char) : Token {
+ override val isUnescapedQuote: Boolean = value == '\''
+ override val isConstant: Boolean =
+ !isUnescapedQuote && value !in 'a'..'z' && value !in 'A'..'Z'
+ }
+
+ /** An escaped quote (`''` that is formatted as a single `'`). */
+ private object LiteralQuoteToken : Token {
+ override val value = '\''
+ override val isUnescapedQuote = false // It's an escaped quote.
+ override val isConstant = true
+ }
+
+ private fun List<Token>.asString() = map { it.value }.joinToString("")
+}
+
+/** In-place equivalent of [ArrayDeque.drop], that accepts a count. */
+private fun <T : Any> ArrayDeque<T>.removeFirst(n: Int) {
+ repeat(n) { removeFirst() }
+}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
index 66e3d71..2b520e6 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/util/DynamicFormatter.kt
@@ -38,23 +38,6 @@
*
* See [Formatter] documentation for the format string syntax.
*
- * Example usage:
- * ```
- * DynamicFormatter().format(
- * "%s has walked %d steps. %1$s has also walked %.2f meters.",
- * "John", PlatformHealthSources.dailySteps(), PlatformHealthSources.dailyDistanceMeters()
- * )
- * // Generates an equivalent of:
- * DynamicString.constant("John has walked ")
- * .concat(PlatformHealthSources.dailySteps().format())
- * .concat(DynamicString.constant(" steps. John has also walked "))
- * .concat(
- * PlatformHealthSources.dailyMeters()
- * .format(FloatFormatter.Builder().setMaxFractionDigits(2).build())
- * )
- * .concat(DynamicString.constant(" meters."))
- * ```
- *
* Argument index options (`%s`, `%2$s`, and `%<s`) are fully supported.
*
* These are the supported conversions and options:
@@ -71,12 +54,34 @@
* |%f |See [Formatter] |No |Yes, width and precision|Yes, width and precision|No |No |
* |...|See [Formatter] |No |No |No |No |No |
*
- * NOTE: `%f` has a default precision of 6..6 in [Formatter], which is different from
+ * NOTE 1: `%f` has a default precision of 6..6 in [Formatter], which is different from
* [DynamicFloat.format] which defaults to 0..3. [DynamicFormatter] behaves like [Formatter] and
* defaults to 6..6.
+ *
+ * NOTE 2: [DynamicFormatter] uses `Locale.getDefault(Locale.Category.FORMAT)` at the time of
+ * calling [format] for non-[DynamicType] arguments. This can change on the remote side which, would
+ * cause a mismatch between the locally-formatted arguments and the remotely-formatted arguments,
+ * unless the provider sends a newly formatted [DynamicString] (using a new invocation of [format]).
+ *
+ * Example usage:
+ * ```
+ * DynamicFormatter().format(
+ * "%s has walked %d steps. %1$s has also walked %.2f meters.",
+ * "John", PlatformHealthSources.dailySteps(), PlatformHealthSources.dailyDistanceMeters()
+ * )
+ * // Generates an equivalent of:
+ * DynamicString.constant("John has walked ")
+ * .concat(PlatformHealthSources.dailySteps().format())
+ * .concat(DynamicString.constant(" steps. John has also walked "))
+ * .concat(
+ * PlatformHealthSources.dailyMeters()
+ * .format(FloatFormatter.Builder().setMaxFractionDigits(2).build())
+ * )
+ * .concat(DynamicString.constant(" meters."))
+ * ```
*/
public class DynamicFormatter {
- // TODO: b/297323092 - Allow providing locale for remote evaluation in the constructor.
+ // TODO: b/297323092 - Allow providing in the constructor for both local and remote evaluation.
private val locale
get() = Locale.getDefault(Locale.Category.FORMAT)
@@ -124,39 +129,38 @@
* on the result.
*/
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun extractFormatParts(
- format: String,
- args: Array<out Any?>
- ): Sequence<ConstantOrDynamicPart> = sequence {
- var lastPosition = 0
- var lastVariableIndex = -1
- var lastPositionalVariableIndex = -1
- for (match in PATTERN.findAll(format)) {
- if (match.range.first > lastPosition) {
- // Non-variable (non-match) from the end of the last dynamic part, i.e.:
- // "...<dynamic><*constant*><dynamic>...".
- yield(ConstantPart(format.substring(lastPosition until match.range.first)))
+ private fun extractFormatParts(format: String, args: Array<out Any?>): Sequence<Part> =
+ sequence {
+ var lastPosition = 0
+ var lastVariableIndex = -1
+ var lastPositionalVariableIndex = -1
+ for (match in PATTERN.findAll(format)) {
+ if (match.range.first > lastPosition) {
+ // Non-variable (non-match) from the end of the last dynamic part, i.e.:
+ // "...<dynamic><*constant*><dynamic>...".
+ yield(Part.Constant(format.substring(lastPosition until match.range.first)))
+ }
+ // Variable match - parsing the specifier, maintaining last indices, and formatting.
+ val formatAttributes =
+ match.toFormatStringVariable(
+ lastIndex = lastVariableIndex,
+ lastPositionalIndex = lastPositionalVariableIndex
+ )
+ lastVariableIndex = formatAttributes.index
+ if (formatAttributes.isPositionalIndex) {
+ lastPositionalVariableIndex = formatAttributes.index
+ }
+ yield(formatAttributes.format(args))
+ // Remembering position in order to extract non-variable parts between variable
+ // matches.
+ lastPosition = match.range.last + 1
}
- // Variable match - parsing the specifier, maintaining last indices, and formatting.
- val formatAttributes =
- match.toFormatStringVariable(
- lastIndex = lastVariableIndex,
- lastPositionalIndex = lastPositionalVariableIndex
- )
- lastVariableIndex = formatAttributes.index
- if (formatAttributes.isPositionalIndex) {
- lastPositionalVariableIndex = formatAttributes.index
+ if (lastPosition < format.length) {
+ // Non-variable (non-match) from the end of the last dynamic part at the end of the
+ // format, i.e.: "...<dynamic><*constant*>".
+ yield(Part.Constant(format.substring(lastPosition)))
}
- yield(formatAttributes.format(args))
- // Remembering position in order to extract non-variable parts between variable matches.
- lastPosition = match.range.last + 1
}
- if (lastPosition < format.length) {
- // Non-variable (non-match) from the end of the last dynamic part at the end of the
- // format, i.e.: "...<dynamic><*constant*>".
- yield(ConstantPart(format.substring(lastPosition)))
- }
- }
/**
* Converts a [PATTERN] match to an [FormatStringVariable].
@@ -228,36 +232,35 @@
* )
* ```
*/
- private fun Sequence<ConstantOrDynamicPart>.mergeConstants(): Sequence<ConstantOrDynamicPart> =
- sequence {
- val empty = ConstantPart("")
- var lastConstant = empty
- forEach { nextSection ->
- if (nextSection is ConstantPart) {
- lastConstant += nextSection
- } else {
- if (lastConstant.value.isNotEmpty()) yield(lastConstant)
- lastConstant = empty
- yield(nextSection)
- }
+ private fun Sequence<Part>.mergeConstants(): Sequence<Part> = sequence {
+ val empty = Part.Constant("")
+ var lastConstant = empty
+ forEach { nextSection ->
+ if (nextSection is Part.Constant) {
+ lastConstant += nextSection
+ } else {
+ if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ lastConstant = empty
+ yield(nextSection)
}
- if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ }
+ if (lastConstant.value.isNotEmpty()) yield(lastConstant)
+ }
+
+ /** Represents a [Constant] or a [Dynamic] value. */
+ private sealed interface Part {
+ @RequiresSchemaVersion(major = 1, minor = 200) fun toDynamicString(): DynamicString
+
+ data class Constant(val value: String) : Part {
+ operator fun plus(other: Constant) = Constant(value + other.value)
+
+ @RequiresSchemaVersion(major = 1, minor = 200)
+ override fun toDynamicString() = DynamicString.constant(value)
}
- /** Represents a [ConstantPart] or a [DynamicPart] value. */
- private sealed interface ConstantOrDynamicPart {
- @RequiresSchemaVersion(major = 1, minor = 200) fun toDynamicString(): DynamicString
- }
-
- private data class ConstantPart(val value: String) : ConstantOrDynamicPart {
- operator fun plus(other: ConstantPart) = ConstantPart(value + other.value)
-
- @RequiresSchemaVersion(major = 1, minor = 200)
- override fun toDynamicString() = DynamicString.constant(value)
- }
-
- private data class DynamicPart(val value: DynamicString) : ConstantOrDynamicPart {
- @RequiresSchemaVersion(major = 1, minor = 200) override fun toDynamicString() = value
+ data class Dynamic(val value: DynamicString) : Part {
+ @RequiresSchemaVersion(major = 1, minor = 200) override fun toDynamicString() = value
+ }
}
/**
@@ -299,17 +302,17 @@
* [DynamicFormatter.format].
*/
@RequiresSchemaVersion(major = 1, minor = 200)
- fun format(args: Array<out Any?>): ConstantOrDynamicPart {
+ fun format(args: Array<out Any?>): Part {
if (index >= args.size) {
throw MissingFormatArgumentException("Format specifier '$specifier'")
}
val arg = args[index]
// Non-DynamicType arguments use Formatter.
- if (arg !is DynamicType) return ConstantPart(arg.defaultFormat())
+ if (arg !is DynamicType) return Part.Constant(arg.defaultFormat())
throwIfIfNotAllowed(arg)
return when (conversion) {
- '%' -> ConstantPart(arg.defaultFormat()) // Argument is ignored by %%.
- 'n' -> ConstantPart(arg.defaultFormat()) // Argument is ignored by %n.
+ '%' -> Part.Constant(arg.defaultFormat()) // Argument is ignored by %%.
+ 'n' -> Part.Constant(arg.defaultFormat()) // Argument is ignored by %n.
's' -> asStringPart(arg)
'S' -> asStringUpperPart(arg)
'b' -> asBooleanPart(arg)
@@ -324,25 +327,25 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asStringPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asStringPart(arg: DynamicType): Part =
when (arg) {
is DynamicString -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg)
+ Part.Dynamic(arg)
}
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
is DynamicFloat -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
arg.format(FloatFormatter.Builder().setMinFractionDigits(1).build())
)
}
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
@@ -352,21 +355,21 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asStringUpperPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asStringUpperPart(arg: DynamicType): Part =
when (arg) {
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
is DynamicFloat -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
arg.format(FloatFormatter.Builder().setMinFractionDigits(1).build())
)
}
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
@@ -376,49 +379,49 @@
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asBooleanPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asBooleanPart(arg: DynamicType): Part =
when (arg) {
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(
+ Part.Dynamic(
DynamicString.onCondition(arg)
.use(true.defaultFormat())
.elseUse(false.defaultFormat())
)
}
// All non-null is true, including DynamicType.
- else -> ConstantPart(arg.defaultFormat())
+ else -> Part.Constant(arg.defaultFormat())
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asBooleanUpperPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asBooleanUpperPart(arg: DynamicType): Part =
when (arg) {
is DynamicBool -> {
throwUnsupportedForAnyOption()
- DynamicPart(DynamicString.onCondition(arg).use("TRUE").elseUse("FALSE"))
+ Part.Dynamic(DynamicString.onCondition(arg).use("TRUE").elseUse("FALSE"))
}
// All non-null is true, including DynamicType.
- else -> ConstantPart(arg.defaultFormat())
+ else -> Part.Constant(arg.defaultFormat())
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asDecimalPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asDecimalPart(arg: DynamicType): Part =
when (arg) {
is DynamicInt32 -> {
throwUnsupportedForAnyOption()
- DynamicPart(arg.format())
+ Part.Dynamic(arg.format())
}
else -> throwUnsupportedDynamicType(arg)
}
@RequiresSchemaVersion(major = 1, minor = 200)
- private fun asFloatPart(arg: DynamicType): ConstantOrDynamicPart =
+ private fun asFloatPart(arg: DynamicType): Part =
when (arg) {
is DynamicFloat -> {
throwUnsupportedSpecifierIf(
flags.isNotEmpty() || width != null || dateTimePrefix != null
)
- DynamicPart(
+ Part.Dynamic(
arg.format(
FloatFormatter.Builder()
.apply {
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt
new file mode 100644
index 0000000..235541c
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicDateFormatTest.kt
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.util
+
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone
+import android.icu.util.ULocale
+import android.text.format.DateFormat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
+import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver
+import com.google.common.truth.Expect
+import com.google.common.truth.Truth.assertWithMessage
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.util.Date
+import java.util.Locale
+import kotlin.test.assertFailsWith
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.toJavaDuration
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+
+@RunWith(AndroidJUnit4::class)
+class DynamicDateFormatTest {
+ // Using expectation loops instead of parameterized tests because there are too many tests (note
+ // the multiplication in all available locales), too expensive with test infra boilerplate.
+ @get:Rule val expect = Expect.create()
+
+ enum class Case(
+ val pattern: String,
+ val instant: Instant,
+ val expectedEnglish: String,
+ ) {
+ EMPTY(
+ pattern = "",
+ instant = MIDNIGHT,
+ expectedEnglish = "",
+ ),
+ // Constant patterns.
+ CONSTANT_AT_START(
+ pattern = ":::h",
+ instant = MIDNIGHT,
+ expectedEnglish = ":::12",
+ ),
+ CONSTANT_AT_MIDDLE(
+ pattern = "h:::h",
+ instant = MIDNIGHT,
+ expectedEnglish = "12:::12",
+ ),
+ CONSTANT_AT_END(
+ pattern = "h:::",
+ instant = MIDNIGHT,
+ expectedEnglish = "12:::",
+ ),
+ // Escape patterns.
+ ESCAPED_AT_START(
+ pattern = "'Time is' h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "Time is 3",
+ ),
+ ESCAPED_AT_MIDDLE(
+ pattern = "h 'h' m",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3 h 20",
+ ),
+ ESCAPED_AT_END(
+ pattern = "h 'is the time'",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3 is the time",
+ ),
+ ESCAPED_QUOTE_AT_START(
+ pattern = "''h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "'3",
+ ),
+ ESCAPED_QUOTE_AT_MIDDLE(
+ pattern = "h''m",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3'20",
+ ),
+ ESCAPED_QUOTE_AT_END(
+ pattern = "h''",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "3'",
+ ),
+ ESCAPED_QUOTE_IN_ESCAPED(
+ pattern = "'Time''s' h",
+ instant = MIDNIGHT.plus((3.hours + 20.minutes).toJavaDuration()),
+ expectedEnglish = "Time's 3",
+ ),
+ // Hours patterns.
+ HOURS_PADDED_MIDNIGHT(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT,
+ expectedEnglish = "0000:0000:0012:0024:AM",
+ ),
+ HOURS_PADDED_NOON(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT.plus(Duration.ofHours(12)),
+ expectedEnglish = "0012:0000:0012:0012:PM",
+ ),
+ HOURS_PADDED_A_M(
+ pattern = "HHHH:KKKK:hhhh:kkkk:aaaa",
+ instant = MIDNIGHT.plus(Duration.ofHours(6)),
+ expectedEnglish = "0006:0006:0006:0006:AM",
+ ),
+ HOURS_PADDED_P_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(18)),
+ expectedEnglish = "18:6:6:18:PM",
+ ),
+ HOURS_NOT_PADDED_MIDNIGHT(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT,
+ expectedEnglish = "0:0:12:24:AM",
+ ),
+ HOURS_NOT_PADDED_NOON(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(12)),
+ expectedEnglish = "12:0:12:12:PM",
+ ),
+ HOURS_NOT_PADDED_A_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(6)),
+ expectedEnglish = "6:6:6:6:AM",
+ ),
+ HOURS_NOT_PADDED_P_M(
+ pattern = "H:K:h:k:a",
+ instant = MIDNIGHT.plus(Duration.ofHours(18)),
+ expectedEnglish = "18:6:6:18:PM",
+ ),
+ // Minute patterns.
+ MINUTES_PADDED_ZERO(
+ pattern = "mmmm",
+ instant = MIDNIGHT,
+ expectedEnglish = "0000",
+ ),
+ MINUTES_PADDED_SINGLE_DIGIT(
+ pattern = "mmmm",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(5)),
+ expectedEnglish = "0005",
+ ),
+ MINUTES_PADDED_DOUBLE_DIGIT(
+ pattern = "mmmm",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(15)),
+ expectedEnglish = "0015",
+ ),
+ MINUTES_NOT_PADDED_ZERO(
+ pattern = "m",
+ instant = MIDNIGHT,
+ expectedEnglish = "0",
+ ),
+ MINUTES_NOT_PADDED_SINGLE_DIGIT(
+ pattern = "m",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(5)),
+ expectedEnglish = "5",
+ ),
+ MINUTES_NOT_PADDED_DOUBLE_DIGIT(
+ pattern = "m",
+ instant = MIDNIGHT.plus(Duration.ofMinutes(15)),
+ expectedEnglish = "15",
+ ),
+ }
+
+ @Test
+ fun matchesExpectedEnglish() {
+ for (case in Case.values()) {
+ expectFormatsTo(
+ message = case.name,
+ pattern = case.pattern,
+ instant = case.instant,
+ locale = EN,
+ expected = case.expectedEnglish,
+ )
+ }
+ }
+
+ /**
+ * Tests that [Case.pattern] formats to the same value [SimpleDateFormat] does, in all available
+ * locales.
+ */
+ @Test
+ fun matchesSimpleDateFormat() {
+ for (case in Case.values()) {
+ for (locale in Locale.getAvailableLocales()) {
+ expectMatchesSimpleDateFormat(
+ message = "${case.name}_$locale",
+ pattern = case.pattern,
+ instant = case.instant,
+ locale = locale,
+ )
+ }
+ }
+ }
+
+ /**
+ * Tests that the best date time pattern for "hmm" matches [SimpleDateFormat], in all available
+ * locales.
+ */
+ @Test
+ fun bestDateTimePattern_matchesSimpleDateFormat() {
+ for (locale in Locale.getAvailableLocales()) {
+ val pattern = DateFormat.getBestDateTimePattern(locale, "hmm")
+ expectMatchesSimpleDateFormat(
+ message = "${pattern}_$locale",
+ pattern = pattern,
+ instant = MIDNIGHT,
+ locale = locale,
+ )
+ }
+ }
+
+ /** Expects the [pattern] formats to [expected], in the given [locale] and [instant]. */
+ private fun expectFormatsTo(
+ message: String,
+ pattern: String,
+ instant: Instant,
+ locale: Locale,
+ expected: String,
+ ) {
+ val formatter = DynamicDateFormat(pattern, locale, TIME_ZONE)
+
+ val actual: String =
+ formatter.format(DynamicInstant.withSecondsPrecision(instant)).evaluate(locale)
+
+ expect.withMessage(message).that(actual).isEqualTo(expected)
+ }
+
+ /**
+ * Expects the [pattern] formats to the same value [SimpleDateFormat] does, in the given
+ * [locale] and [instant].
+ */
+ private fun expectMatchesSimpleDateFormat(
+ message: String,
+ pattern: String,
+ instant: Instant,
+ locale: Locale,
+ ) {
+ expectFormatsTo(
+ message = message,
+ pattern = pattern,
+ instant = instant,
+ locale = locale,
+ expected =
+ SimpleDateFormat(pattern, locale)
+ .also { it.timeZone = TimeZone.getTimeZone(TIME_ZONE.id) }
+ .format(Date.from(instant))
+ )
+ }
+
+ /** Synchronously evaluates the [DynamicString] using [DynamicTypeEvaluator]. */
+ private fun DynamicString.evaluate(locale: Locale): String {
+ lateinit var result: String
+ val evaluator = DynamicTypeEvaluator(DynamicTypeEvaluator.Config.Builder().build())
+ evaluator
+ .bind(
+ DynamicTypeBindingRequest.forDynamicString(
+ this@evaluate,
+ ULocale.forLocale(locale),
+ { it.run() }, // Synchronous executor
+ object : DynamicTypeValueReceiver<String> {
+ override fun onData(newData: String) {
+ result = newData
+ }
+
+ override fun onInvalidated() {
+ throw AssertionError("DynamicString invalidated: ${this@evaluate}")
+ }
+ }
+ )
+ )
+ .startEvaluation()
+ return result
+ }
+
+ companion object {
+ private val TIME_ZONE = ZoneId.of("UTC")
+ private val MIDNIGHT = Instant.EPOCH // Epoch is midnight in UTC.
+ private val EN = Locale.ENGLISH
+ }
+}
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+class DynamicDateFormatFailingTest(private val case: Case) {
+ enum class Case(val expected: Exception, val pattern: String) {
+ UNSUPPORTED_LETTER(
+ IllegalArgumentException("Illegal pattern character 'y'"),
+ "hh:mm yyyy-MM-dd"
+ ),
+ ODD_QUOTES(IllegalArgumentException("Unterminated quote"), "'''"),
+ }
+
+ @Test
+ fun fails() {
+ val exception =
+ assertFailsWith(case.expected::class, case.name) { DynamicDateFormat(case.pattern) }
+
+ assertWithMessage(case.name)
+ .that(exception)
+ .hasMessageThat()
+ .isEqualTo(case.expected.message)
+ }
+
+ companion object {
+ @Parameters @JvmStatic fun parameters() = Case.values()
+ }
+}
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
index bec01a3..0bb33ca 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/util/DynamicFormatterTest.kt
@@ -26,7 +26,7 @@
import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver
-import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.truth.Truth.assertThat
import java.time.Instant
import java.util.IllegalFormatConversionException
import java.util.MissingFormatArgumentException
@@ -39,166 +39,169 @@
@RunWith(ParameterizedRobolectricTestRunner::class)
class DynamicFormatterFormatTest(private val case: Case) {
enum class Case(
- val expected: String,
val format: String,
val args: List<Any?>? = null,
val dynamicArgs: List<DynamicType>? = null,
val equivalentArgs: List<Any?>? = null,
+ val expected: String,
) {
- CONSTANT_ONLY("hello world", "hello world", args = listOf()),
- NON_DYNAMIC_ARGS("hello world", "hello %s", args = listOf("world")),
- EXPLICIT_ARG_INDEX("12 34 56 56", "%d %3\$d %d %<d", args = listOf(12, 56, 34)),
+ CONSTANT_ONLY(format = "hello world", args = listOf(), expected = "hello world"),
+ NON_DYNAMIC_ARGS(format = "hello %s", args = listOf("world"), expected = "hello world"),
+ EXPLICIT_ARG_INDEX(
+ format = "%d %3\$d %d %<d",
+ args = listOf(12, 56, 34),
+ expected = "12 34 56 56",
+ ),
DYNAMIC_START(
- "hello world",
- "%s world",
+ format = "%s world",
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "hello world",
),
DYNAMIC_MIDDLE(
- "hello world",
- "hel%srld",
+ format = "hel%srld",
dynamicArgs = listOf(DynamicString.constant("lo wo")),
equivalentArgs = listOf("lo wo"),
+ expected = "hello world",
),
DYNAMIC_END(
- "hello world",
- "hello %s",
+ format = "hello %s",
dynamicArgs = listOf(DynamicString.constant("world")),
equivalentArgs = listOf("world"),
+ expected = "hello world",
),
SEPARATED_DYNAMIC(
- "hello world",
- "%s %s",
+ format = "%s %s",
dynamicArgs = listOf(DynamicString.constant("hello"), DynamicString.constant("world")),
equivalentArgs = listOf("hello", "world"),
+ expected = "hello world",
),
CONNECTED_DYNAMIC(
- "helloworld",
- "%s%s",
+ format = "%s%s",
dynamicArgs = listOf(DynamicString.constant("hello"), DynamicString.constant("world")),
equivalentArgs = listOf("hello", "world"),
+ expected = "helloworld",
),
// %%
FORMAT_percent_DYNAMIC_TYPE(
- "%",
- "%%",
+ format = "%%",
// args are ignored
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "%",
),
// %%
FORMAT_n_DYNAMIC_TYPE(
- "\n",
- "%n",
+ format = "%n",
// args are ignored
dynamicArgs = listOf(DynamicString.constant("hello")),
equivalentArgs = listOf("hello"),
+ expected = "\n",
),
// %s
FORMAT_s_DYNAMIC_STRING(
- "ab",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicString.constant("ab")),
equivalentArgs = listOf("ab"),
+ expected = "ab",
),
FORMAT_s_DYNAMIC_INT32(
- "12",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
FORMAT_s_DYNAMIC_FLOAT(
- "12.0",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicFloat.constant(12f)),
equivalentArgs = listOf(12f),
+ expected = "12.0",
),
FORMAT_s_DYNAMIC_BOOL_TRUE(
- "true",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "true",
),
FORMAT_s_DYNAMIC_BOOL_FALSE(
- "false",
- "%s",
+ format = "%s",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "false",
),
// %S
FORMAT_S_DYNAMIC_INT32(
- "12",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
FORMAT_S_DYNAMIC_FLOAT(
- "12.0",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicFloat.constant(12f)),
equivalentArgs = listOf(12f),
+ expected = "12.0",
),
FORMAT_S_DYNAMIC_BOOL_TRUE(
- "TRUE",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "TRUE",
),
FORMAT_S_DYNAMIC_BOOL_FALSE(
- "FALSE",
- "%S",
+ format = "%S",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "FALSE",
),
// %b
FORMAT_b_DYNAMIC_BOOL_TRUE(
- "true",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "true",
),
FORMAT_b_DYNAMIC_BOOL_FALSE(
- "false",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "false",
),
FORMAT_b_DYNAMIC_TYPE_IS_TRUE(
- "true",
- "%b",
+ format = "%b",
dynamicArgs = listOf(DYNAMIC_INSTANT),
equivalentArgs = listOf(INSTANT),
+ expected = "true",
),
// %B
FORMAT_B_DYNAMIC_BOOL_TRUE(
- "TRUE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DynamicBool.constant(true)),
equivalentArgs = listOf(true),
+ expected = "TRUE",
),
FORMAT_B_DYNAMIC_BOOL_FALSE(
- "FALSE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DynamicBool.constant(false)),
equivalentArgs = listOf(false),
+ expected = "FALSE",
),
FORMAT_B_DYNAMIC_TYPE_IS_TRUE(
- "TRUE",
- "%B",
+ format = "%B",
dynamicArgs = listOf(DYNAMIC_INSTANT),
equivalentArgs = listOf(INSTANT),
+ expected = "TRUE",
),
// %d
FORMAT_d_DYNAMIC_INT32(
- "12",
- "%d",
+ format = "%d",
dynamicArgs = listOf(DynamicInt32.constant(12)),
equivalentArgs = listOf(12),
+ expected = "12",
),
// %f
FORMAT_f_DYNAMIC_FLOAT(
- "12.345000 34.57 56.70",
- "%f %.2f %.2f",
+ format = "%f %.2f %.2f",
dynamicArgs =
listOf(
DynamicFloat.constant(12.345f), // default fraction digits
@@ -206,23 +209,21 @@
DynamicFloat.constant(56.7f), // min fraction digits
),
equivalentArgs = listOf(12.345f, 34.567f, 56.7f),
+ expected = "12.345000 34.57 56.70",
),
}
@Test
fun equalsExpected() {
val args = (case.args ?: case.dynamicArgs)!!.toTypedArray()
- assertWithMessage(case.name)
- .that(DynamicString.format(case.format, *args).evaluate())
- .isEqualTo(case.expected)
+ assertThat(DynamicString.format(case.format, *args).evaluate()).isEqualTo(case.expected)
}
@Test
fun equalsDefault() {
val dynamicArgs = (case.args ?: case.dynamicArgs)!!.toTypedArray()
val equivalentArgs = (case.args ?: case.equivalentArgs)!!.toTypedArray()
- assertWithMessage(case.name)
- .that(DynamicString.format(case.format, *dynamicArgs).evaluate())
+ assertThat(DynamicString.format(case.format, *dynamicArgs).evaluate())
.isEqualTo(case.format.format(*equivalentArgs))
}
@@ -256,115 +257,114 @@
@RunWith(ParameterizedRobolectricTestRunner::class)
class DynamicFormatterFailingTest(private val case: Case) {
- enum class Case(val expected: Exception, val format: String, val args: List<Any?> = listOf()) {
+ enum class Case(val format: String, val args: List<Any?> = listOf(), val expected: Exception) {
NOT_ENOUGH_POSITIONAL_ARGS(
- MissingFormatArgumentException("Format specifier '%s'"),
- "%s %s",
- args = listOf("hello")
+ format = "%s %s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%s'"),
),
NOT_ENOUGH_EXPLICIT_INDEX_ARGS(
- MissingFormatArgumentException("Format specifier '%3\$s'"),
- "%s %3\$s",
- args = listOf("hello")
+ format = "%s %3\$s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%3\$s'"),
),
RELATIVE_INDEX_IS_FIRST(
- MissingFormatArgumentException("Format specifier '%<s'"),
- "%<s",
- args = listOf("hello")
+ format = "%<s",
+ args = listOf("hello"),
+ expected = MissingFormatArgumentException("Format specifier '%<s'"),
),
DYNAMIC_ARG_NOT_ALLOWED(
- IllegalFormatConversionException('d', String::class.java),
- "%d",
- args = listOf(DynamicString.constant("hello"))
+ format = "%d",
+ args = listOf(DynamicString.constant("hello")),
+ expected = IllegalFormatConversionException('d', String::class.java),
),
UNSUPPORTED_DYNAMIC_CONVERSION(
- UnsupportedOperationException("Unsupported conversion for DynamicType: 'h'"),
- "%h",
- args = listOf(DynamicInt32.constant(12))
+ format = "%h",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported conversion for DynamicType: 'h'"),
),
// %s
FORMAT_s_DYNAMIC_STRING_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicString.constant("a"))
+ format = "%2s",
+ args = listOf(DynamicString.constant("a")),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2s",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2s",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2s'"),
- "%2s",
- args = listOf(DynamicBool.constant(true))
+ format = "%2s",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2s'"),
),
FORMAT_s_UNSUPPORTED_DYNAMIC_ARG(
- UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%s'"),
- "%s",
+ format = "%s",
args = listOf(DYNAMIC_INSTANT),
+ expected =
+ UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%s'"),
),
// %S
FORMAT_S_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2S",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2S",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2S'"),
- "%2S",
- args = listOf(DynamicBool.constant(true))
+ format = "%2S",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2S'"),
),
FORMAT_S_UNSUPPORTED_DYNAMIC_ARG(
- UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%S'"),
- "%S",
+ format = "%S",
args = listOf(DYNAMIC_INSTANT),
+ expected =
+ UnsupportedOperationException("$DYNAMIC_INSTANT unsupported for specifier: '%S'"),
),
// %b
FORMAT_b_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2b'"),
- "%2b",
- args = listOf(DynamicBool.constant(true))
+ format = "%2b",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2b'"),
),
// %B
FORMAT_B_DYNAMIC_BOOL_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2B'"),
- "%2B",
- args = listOf(DynamicBool.constant(true))
+ format = "%2B",
+ args = listOf(DynamicBool.constant(true)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2B'"),
),
// %d
FORMAT_d_DYNAMIC_INT32_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2d'"),
- "%2d",
- args = listOf(DynamicInt32.constant(12))
+ format = "%2d",
+ args = listOf(DynamicInt32.constant(12)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2d'"),
),
// %f
FORMAT_f_DYNAMIC_FLOAT_WITH_UNSUPPORTED_OPTIONS(
- UnsupportedOperationException("Unsupported specifier: '%2f'"),
- "%2f",
- args = listOf(DynamicFloat.constant(12f))
+ format = "%2f",
+ args = listOf(DynamicFloat.constant(12f)),
+ expected = UnsupportedOperationException("Unsupported specifier: '%2f'"),
),
}
@Test
fun fails() {
val exception =
- assertFailsWith(case.expected::class, case.name) {
+ assertFailsWith(case.expected::class) {
DynamicString.format(case.format, *case.args.toTypedArray())
}
- assertWithMessage(case.name)
- .that(exception)
- .hasMessageThat()
- .isEqualTo(case.expected.message)
+ assertThat(exception).hasMessageThat().isEqualTo(case.expected.message)
}
companion object {
diff --git a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
index 21678fe..0910d32 100644
--- a/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
+++ b/wear/protolayout/protolayout-lint/src/main/java/androidx/wear/protolayout/lint/ProtoLayoutMinSchemaDetector.kt
@@ -144,6 +144,7 @@
in 101..200 -> 33
in 201..300 -> 34
in 301..400 -> 35
+ in 401..500 -> 36
else -> Int.MAX_VALUE
}
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index a393a6b..310b255 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface-client/build.gradle b/wear/watchface/watchface-client/build.gradle
index 075f4bfe..f43e366 100644
--- a/wear/watchface/watchface-client/build.gradle
+++ b/wear/watchface/watchface-client/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.mockitoCore4)
diff --git a/wear/watchface/watchface-editor-guava/build.gradle b/wear/watchface/watchface-editor-guava/build.gradle
index 6baca89..15c9ff976 100644
--- a/wear/watchface/watchface-editor-guava/build.gradle
+++ b/wear/watchface/watchface-editor-guava/build.gradle
@@ -39,8 +39,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface-editor/build.gradle b/wear/watchface/watchface-editor/build.gradle
index 0bd2f0e..b3747af 100644
--- a/wear/watchface/watchface-editor/build.gradle
+++ b/wear/watchface/watchface-editor/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.truth)
diff --git a/wear/watchface/watchface-guava/build.gradle b/wear/watchface/watchface-guava/build.gradle
index 319eb9d..28b9236 100644
--- a/wear/watchface/watchface-guava/build.gradle
+++ b/wear/watchface/watchface-guava/build.gradle
@@ -39,14 +39,14 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.testRules)
- testImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- testImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ testImplementation(libs.mockitoCore)
+ testImplementation(libs.dexmakerMockito)
testImplementation(libs.robolectric)
testImplementation(libs.truth)
}
diff --git a/wear/watchface/watchface-style/api/current.txt b/wear/watchface/watchface-style/api/current.txt
index f980193..d5a9d8f 100644
--- a/wear/watchface/watchface-style/api/current.txt
+++ b/wear/watchface/watchface-style/api/current.txt
@@ -112,6 +112,8 @@
public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue);
ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public boolean getDefaultValue();
}
@@ -135,6 +137,9 @@
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
}
public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay {
@@ -169,6 +174,8 @@
ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
@@ -194,6 +201,8 @@
public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue);
ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public double getDefaultValue();
method public double getMaximumValue();
method public double getMinimumValue();
@@ -238,6 +247,9 @@
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -247,6 +259,9 @@
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
method public CharSequence? getScreenReaderName();
@@ -260,6 +275,8 @@
public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue);
ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public long getDefaultValue();
method public long getMaximumValue();
method public long getMinimumValue();
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index f980193..d5a9d8f 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -112,6 +112,8 @@
public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue);
ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, boolean defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public boolean getDefaultValue();
}
@@ -135,6 +137,9 @@
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
}
public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay {
@@ -169,6 +174,8 @@
ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+ ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
@@ -194,6 +201,8 @@
public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue);
ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, double defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public double getDefaultValue();
method public double getMaximumValue();
method public double getMinimumValue();
@@ -238,6 +247,9 @@
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -247,6 +259,9 @@
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
method public CharSequence? getScreenReaderName();
@@ -260,6 +275,8 @@
public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue);
ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int descriptionResourceId, kotlin.jvm.functions.Function0<android.graphics.drawable.Icon?> iconProvider, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.WatchFaceLayer> affectsWatchFaceLayers, long defaultValue, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
method public long getDefaultValue();
method public long getMaximumValue();
method public long getMinimumValue();
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index fa41a807..45a7dc9 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -17,6 +17,8 @@
package androidx.wear.watchface.style
import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -25,7 +27,7 @@
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
import androidx.wear.watchface.style.test.R
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.util.Locale
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,6 +44,9 @@
}
)
+ private val icon_10x10 =
+ Icon.createWithBitmap(Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888))
+
private val colorStyleSetting =
UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
@@ -71,17 +76,17 @@
@Test
public fun stringResources_en() {
- Truth.assertThat(colorStyleSetting.displayName).isEqualTo("Colors")
- Truth.assertThat(colorStyleSetting.description).isEqualTo("Watchface colorization")
+ assertThat(colorStyleSetting.displayName).isEqualTo("Colors")
+ assertThat(colorStyleSetting.description).isEqualTo("Watchface colorization")
- Truth.assertThat(
+ assertThat(
(colorStyleSetting.getOptionForId(UserStyleSetting.Option.Id("red_style"))
as UserStyleSetting.ListUserStyleSetting.ListOption)
.displayName
)
.isEqualTo("Red Style")
- Truth.assertThat(
+ assertThat(
(colorStyleSetting.getOptionForId(UserStyleSetting.Option.Id("green_style"))
as UserStyleSetting.ListUserStyleSetting.ListOption)
.displayName
@@ -97,17 +102,17 @@
context.resources.configuration.apply { setLocale(Locale.ITALIAN) },
context.resources.displayMetrics
)
- Truth.assertThat(colorStyleSetting.displayName).isEqualTo("Colori")
- Truth.assertThat(colorStyleSetting.description).isEqualTo("Colorazione del quadrante")
+ assertThat(colorStyleSetting.displayName).isEqualTo("Colori")
+ assertThat(colorStyleSetting.description).isEqualTo("Colorazione del quadrante")
- Truth.assertThat(
+ assertThat(
(colorStyleSetting.getOptionForId(UserStyleSetting.Option.Id("red_style"))
as UserStyleSetting.ListUserStyleSetting.ListOption)
.displayName
)
.isEqualTo("Stile rosso")
- Truth.assertThat(
+ assertThat(
(colorStyleSetting.getOptionForId(UserStyleSetting.Option.Id("green_style"))
as UserStyleSetting.ListUserStyleSetting.ListOption)
.displayName
@@ -152,16 +157,16 @@
)
val option0 = listUserStyleSetting.options[0] as ListOption
- Truth.assertThat(option0.displayName).isEqualTo("1st option")
- Truth.assertThat(option0.screenReaderName).isEqualTo("1st list option")
+ assertThat(option0.displayName).isEqualTo("1st option")
+ assertThat(option0.screenReaderName).isEqualTo("1st list option")
val option1 = listUserStyleSetting.options[1] as ListOption
- Truth.assertThat(option1.displayName).isEqualTo("2nd option")
- Truth.assertThat(option1.screenReaderName).isEqualTo("2nd list option")
+ assertThat(option1.displayName).isEqualTo("2nd option")
+ assertThat(option1.screenReaderName).isEqualTo("2nd list option")
val option2 = listUserStyleSetting.options[2] as ListOption
- Truth.assertThat(option2.displayName).isEqualTo("3rd option")
- Truth.assertThat(option2.screenReaderName).isEqualTo("3rd list option")
+ assertThat(option2.displayName).isEqualTo("3rd option")
+ assertThat(option2.screenReaderName).isEqualTo("3rd list option")
}
@Test
@@ -190,9 +195,9 @@
ListUserStyleSetting(listUserStyleSetting.toWireFormat())
val option0 = listUserStyleSettingAfterRoundTrip.options[0] as ListOption
- Truth.assertThat(option0.displayName).isEqualTo("1st option")
+ assertThat(option0.displayName).isEqualTo("1st option")
// We expect screenReaderName to be back filled by the displayName.
- Truth.assertThat(option0.screenReaderName).isEqualTo("1st option")
+ assertThat(option0.screenReaderName).isEqualTo("1st option")
}
@Test
@@ -234,16 +239,16 @@
)
val option0 = complicationSetting.options[0] as ComplicationSlotsOption
- Truth.assertThat(option0.displayName).isEqualTo("1st option")
- Truth.assertThat(option0.screenReaderName).isEqualTo("1st list option")
+ assertThat(option0.displayName).isEqualTo("1st option")
+ assertThat(option0.screenReaderName).isEqualTo("1st list option")
val option1 = complicationSetting.options[1] as ComplicationSlotsOption
- Truth.assertThat(option1.displayName).isEqualTo("2nd option")
- Truth.assertThat(option1.screenReaderName).isEqualTo("2nd list option")
+ assertThat(option1.displayName).isEqualTo("2nd option")
+ assertThat(option1.screenReaderName).isEqualTo("2nd list option")
val option2 = complicationSetting.options[2] as ComplicationSlotsOption
- Truth.assertThat(option2.displayName).isEqualTo("3rd option")
- Truth.assertThat(option2.screenReaderName).isEqualTo("3rd list option")
+ assertThat(option2.displayName).isEqualTo("3rd option")
+ assertThat(option2.screenReaderName).isEqualTo("3rd list option")
}
@Test
@@ -293,10 +298,10 @@
)
)
- Truth.assertThat(schema[one]!!.displayName).isEqualTo("1st style")
- Truth.assertThat(schema[one]!!.description).isEqualTo("1st style setting")
- Truth.assertThat(schema[two]!!.displayName).isEqualTo("2nd style")
- Truth.assertThat(schema[two]!!.description).isEqualTo("2nd style setting")
+ assertThat(schema[one]!!.displayName).isEqualTo("1st style")
+ assertThat(schema[one]!!.description).isEqualTo("1st style setting")
+ assertThat(schema[two]!!.displayName).isEqualTo("2nd style")
+ assertThat(schema[two]!!.description).isEqualTo("2nd style setting")
}
@Test
@@ -325,8 +330,140 @@
ComplicationSlotsUserStyleSetting(complicationSetting.toWireFormat())
val option0 = complicationSettingAfterRoundTrip.options[0] as ComplicationSlotsOption
- Truth.assertThat(option0.displayName).isEqualTo("1st option")
+ assertThat(option0.displayName).isEqualTo("1st option")
// We expect screenReaderName to be back filled by the displayName.
- Truth.assertThat(option0.screenReaderName).isEqualTo("1st option")
+ assertThat(option0.screenReaderName).isEqualTo("1st option")
+ }
+
+ @Test
+ public fun booleanUserStyleSetting_lazyIcon() {
+ val userStyleSetting =
+ UserStyleSetting.BooleanUserStyleSetting(
+ UserStyleSetting.Id("setting"),
+ context.resources,
+ displayNameResourceId = 10,
+ descriptionResourceId = 11,
+ iconProvider = { icon_10x10 },
+ affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS),
+ defaultValue = true
+ )
+
+ assertThat(userStyleSetting.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun complicationSlotsUserStyleSetting_lazyIcon() {
+ val userStyleSetting =
+ ComplicationSlotsUserStyleSetting(
+ UserStyleSetting.Id("complications_style_setting1"),
+ context.resources,
+ displayNameResourceId = 10,
+ descriptionResourceId = 11,
+ iconProvider = { icon_10x10 },
+ complicationConfig =
+ listOf(
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ displayNameResourceId = R.string.ith_option,
+ screenReaderNameResourceId = R.string.ith_option_screen_reader_name,
+ iconProvider = { icon_10x10 },
+ emptyList()
+ )
+ ),
+ listOf(WatchFaceLayer.COMPLICATIONS)
+ )
+
+ assertThat(userStyleSetting.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun complicationSlotsOption_lazyIcon() {
+ val userStyleOption =
+ ComplicationSlotsOption(
+ UserStyleSetting.Option.Id("one"),
+ context.resources,
+ displayNameResourceId = R.string.ith_option,
+ screenReaderNameResourceId = R.string.ith_option_screen_reader_name,
+ iconProvider = { icon_10x10 },
+ emptyList()
+ )
+
+ assertThat(userStyleOption.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun doubleRangeUserStyleSetting_lazyIcon() {
+ val userStyleSetting =
+ UserStyleSetting.DoubleRangeUserStyleSetting(
+ UserStyleSetting.Id("setting"),
+ context.resources,
+ displayNameResourceId = 10,
+ descriptionResourceId = 11,
+ iconProvider = { icon_10x10 },
+ 0.0,
+ 1.0,
+ listOf(WatchFaceLayer.BASE),
+ defaultValue = 0.75
+ )
+
+ assertThat(userStyleSetting.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun longRangeUserStyleSetting_lazyIcon() {
+ val userStyleSetting =
+ UserStyleSetting.LongRangeUserStyleSetting(
+ UserStyleSetting.Id("setting"),
+ context.resources,
+ displayNameResourceId = 10,
+ descriptionResourceId = 11,
+ iconProvider = { icon_10x10 },
+ 0,
+ 100,
+ listOf(WatchFaceLayer.BASE),
+ defaultValue = 75
+ )
+
+ assertThat(userStyleSetting.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun listUserStyleSetting_lazyIcon() {
+ val userStyleSetting =
+ UserStyleSetting.ListUserStyleSetting(
+ UserStyleSetting.Id("setting"),
+ context.resources,
+ displayNameResourceId = 10,
+ descriptionResourceId = 11,
+ iconProvider = { icon_10x10 },
+ options =
+ listOf(
+ ListOption(
+ UserStyleSetting.Option.Id("red_style"),
+ context.resources,
+ displayNameResourceId = R.string.red_style_name,
+ screenReaderNameResourceId = R.string.red_style_name,
+ iconProvider = { null }
+ )
+ ),
+ listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+ )
+
+ assertThat(userStyleSetting.icon).isEqualTo(icon_10x10)
+ }
+
+ @Test
+ public fun listOption_lazyIcon() {
+ val userStyleOption =
+ ListOption(
+ UserStyleSetting.Option.Id("red_style"),
+ context.resources,
+ displayNameResourceId = R.string.red_style_name,
+ screenReaderNameResourceId = R.string.red_style_name,
+ iconProvider = { icon_10x10 }
+ )
+
+ assertThat(userStyleOption.icon).isEqualTo(icon_10x10)
}
}
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 847a4ee..bd54388 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -62,6 +62,7 @@
import androidx.wear.watchface.style.data.OptionWireFormat
import androidx.wear.watchface.style.data.PerComplicationTypeMargins
import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
+import androidx.wear.watchface.utility.TraceEvent
import java.io.DataOutputStream
import java.io.InputStream
import java.nio.ByteBuffer
@@ -101,13 +102,23 @@
resources: Resources,
@StringRes id: Int,
) : ResourceDisplayText(resources, id) {
+ private var index: Int? = null
private var indexString: String = ""
fun setIndex(index: Int) {
- indexString = MessageFormat("{0,ordinal}", Locale.getDefault()).format(arrayOf(index))
+ this.index = index
}
- override fun toCharSequence() = resources.getString(id, indexString)
+ override fun toCharSequence(): String {
+ if (indexString.isEmpty()) {
+ index?.let {
+ indexString =
+ MessageFormat("{0,ordinal}", Locale.getDefault()).format(arrayOf(it))
+ }
+ }
+
+ return resources.getString(id, indexString)
+ }
}
}
@@ -151,12 +162,15 @@
public val id: Id,
private val displayNameInternal: DisplayText,
private val descriptionInternal: DisplayText,
- public val icon: Icon?,
+ /** To avoid upfront costs, icons are lazily evaluated. */
+ private val iconProvider: () -> Icon?,
public val watchFaceEditorData: WatchFaceEditorData?,
public val options: List<Option>,
public val defaultOptionIndex: Int,
public val affectedWatchFaceLayers: Collection<WatchFaceLayer>
) {
+ public val icon by lazy { TraceEvent("invoke iconProvider").use { iconProvider() } }
+
init {
require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
"defaultOptionIndex must be within the range of the options list"
@@ -249,32 +263,35 @@
@Px maxWidth: Int,
@Px maxHeight: Int
): Int {
- var sizeEstimate =
- id.value.length +
- displayName.length +
- description.length +
- 4 +
- /** [defaultOptionIndex] */
- affectedWatchFaceLayers.size * 4
- icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
- wireSizeAndDimensions.wireSizeBytes?.let { sizeEstimate += it }
- require(
- wireSizeAndDimensions.width <= maxWidth && wireSizeAndDimensions.height <= maxHeight
- ) {
- "UserStyleSetting id $id has a ${wireSizeAndDimensions.width} x " +
- "${wireSizeAndDimensions.height} icon. This is too big, the maximum size is " +
- "$maxWidth x $maxHeight."
+ TraceEvent("estimateWireSizeInBytesAndValidateIconDimensions").use {
+ var sizeEstimate =
+ id.value.length +
+ displayName.length +
+ description.length +
+ 4 +
+ /** [defaultOptionIndex] */
+ affectedWatchFaceLayers.size * 4
+ icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
+ wireSizeAndDimensions.wireSizeBytes?.let { sizeEstimate += it }
+ require(
+ wireSizeAndDimensions.width <= maxWidth &&
+ wireSizeAndDimensions.height <= maxHeight
+ ) {
+ "UserStyleSetting id $id has a ${wireSizeAndDimensions.width} x " +
+ "${wireSizeAndDimensions.height} icon. This is too big, the maximum size is " +
+ "$maxWidth x $maxHeight."
+ }
}
+ for (option in options) {
+ sizeEstimate +=
+ option.estimateWireSizeInBytesAndValidateIconDimensions(
+ context,
+ maxWidth,
+ maxHeight
+ )
+ }
+ return sizeEstimate
}
- for (option in options) {
- sizeEstimate +=
- option.estimateWireSizeInBytesAndValidateIconDimensions(
- context,
- maxWidth,
- maxHeight
- )
- }
- return sizeEstimate
}
/**
@@ -376,6 +393,15 @@
}
}
+ internal fun createLazyIcon(resources: Resources, parser: XmlResourceParser): () -> Icon? {
+ val iconId = parser.getAttributeResourceValue(NAMESPACE_ANDROID, "icon", -1)
+ if (iconId != -1) {
+ return { Icon.createWithResource(resources.getResourcePackageName(iconId), iconId) }
+ } else {
+ return { null }
+ }
+ }
+
/** Creates appropriate UserStyleSetting base on parent="@xml/..." resource reference. */
internal fun <T> createParent(
resources: Resources,
@@ -397,7 +423,7 @@
val id: Id,
val displayName: DisplayText,
val description: DisplayText,
- val icon: Icon?,
+ val iconProvider: () -> Icon?,
val watchFaceEditorData: WatchFaceEditorData?,
val options: List<Option>,
val defaultOptionIndex: Int?,
@@ -426,7 +452,7 @@
createDisplayText(resources, parser, "displayName", parent?.displayNameInternal)
val description =
createDisplayText(resources, parser, "description", parent?.descriptionInternal)
- val icon = createIcon(resources, parser) ?: parent?.icon
+ val iconProvider = createLazyIcon(resources, parser)
val defaultOptionIndex =
if (inflateDefault) {
@@ -468,7 +494,7 @@
Id(id),
displayName,
description,
- icon,
+ { iconProvider() ?: parent?.icon },
watchFaceEditorData ?: parent?.watchFaceEditorData,
if (parent == null || options.isNotEmpty()) options else parent.options,
defaultOptionIndex,
@@ -490,7 +516,7 @@
Id(wireFormat.mId),
DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName),
DisplayText.CharSequenceDisplayText(wireFormat.mDescription),
- wireFormat.mIcon,
+ { wireFormat.mIcon },
wireFormat.mOnWatchFaceEditorBundle?.let { WatchFaceEditorData(it) },
wireFormat.mOptions.map { Option.createFromWireFormat(it) },
wireFormat.mDefaultOptionIndex,
@@ -747,7 +773,49 @@
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
- icon,
+ { icon },
+ watchFaceEditorData,
+ listOf(BooleanOption.TRUE, BooleanOption.FALSE),
+ when (defaultValue) {
+ true -> 0
+ false -> 1
+ },
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a BooleanUserStyleSetting, with a lazily evaluated [icon].
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param displayName Localized human readable name for the element, used in the userStyle
+ * selection UI.
+ * @param description Localized description string displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultValue The default value for this BooleanUserStyleSetting.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ displayName: CharSequence,
+ description: CharSequence,
+ iconProvider: () -> Icon?,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultValue: Boolean,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.CharSequenceDisplayText(displayName),
+ DisplayText.CharSequenceDisplayText(description),
+ iconProvider,
watchFaceEditorData,
listOf(BooleanOption.TRUE, BooleanOption.FALSE),
when (defaultValue) {
@@ -791,7 +859,54 @@
id,
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
- icon,
+ { icon },
+ watchFaceEditorData,
+ listOf(BooleanOption.TRUE, BooleanOption.FALSE),
+ when (defaultValue) {
+ true -> 0
+ false -> 1
+ },
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a BooleanUserStyleSetting with a lazily evaluated [icon], where
+ * [BooleanUserStyleSetting.displayName] and [BooleanUserStyleSetting.description] are
+ * specified as resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] and
+ * [descriptionResourceId] are loaded.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI.
+ * @param descriptionResourceId String resource id for a human readable description string
+ * displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultValue The default value for this BooleanUserStyleSetting.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes descriptionResourceId: Int,
+ iconProvider: () -> Icon?,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultValue: Boolean,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
+ iconProvider,
watchFaceEditorData,
listOf(BooleanOption.TRUE, BooleanOption.FALSE),
when (defaultValue) {
@@ -805,7 +920,7 @@
id: Id,
displayName: DisplayText,
description: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
defaultValue: Boolean
@@ -813,7 +928,7 @@
id,
displayName,
description,
- icon,
+ iconProvider,
watchFaceEditorData,
listOf(BooleanOption.TRUE, BooleanOption.FALSE),
when (defaultValue) {
@@ -860,7 +975,7 @@
params.id,
params.displayName,
params.description,
- params.icon,
+ params.iconProvider,
params.watchFaceEditorData,
params.affectedWatchFaceLayers,
defaultValue
@@ -929,7 +1044,7 @@
id: Id,
displayNameInternal: DisplayText,
descriptionInternal: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
options: List<ComplicationSlotsOption>,
defaultOptionIndex: Int,
@@ -938,7 +1053,7 @@
id,
displayNameInternal,
descriptionInternal,
- icon,
+ iconProvider,
watchFaceEditorData,
options,
defaultOptionIndex,
@@ -1272,7 +1387,7 @@
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
- icon,
+ { icon },
watchFaceEditorData,
complicationConfig,
complicationConfig.indexOf(defaultOption),
@@ -1317,7 +1432,54 @@
id,
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
- icon,
+ { icon },
+ watchFaceEditorData,
+ complicationConfig,
+ complicationConfig.indexOf(defaultOption),
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a ComplicationSlotsUserStyleSetting with a lazily evaluated [icon], where
+ * [ComplicationSlotsUserStyleSetting.displayName] and
+ * [ComplicationSlotsUserStyleSetting.description] are specified as resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] and
+ * [descriptionResourceId] are loaded.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI.
+ * @param descriptionResourceId String resource id for a human readable description string
+ * displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param complicationConfig The configuration for affected complications.
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects, must include [WatchFaceLayer.COMPLICATIONS].
+ * @param defaultOption The default option, used when data isn't persisted. Optional
+ * parameter which defaults to the first element of [complicationConfig].
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes descriptionResourceId: Int,
+ iconProvider: () -> Icon?,
+ complicationConfig: List<ComplicationSlotsOption>,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultOption: ComplicationSlotsOption = complicationConfig.first(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : this(
+ id,
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
+ iconProvider,
watchFaceEditorData,
complicationConfig,
complicationConfig.indexOf(defaultOption),
@@ -1328,7 +1490,7 @@
id: Id,
displayName: DisplayText,
description: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData? = null,
options: List<ComplicationSlotsOption>,
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
@@ -1337,7 +1499,7 @@
id,
displayName,
description,
- icon,
+ iconProvider,
watchFaceEditorData,
options,
defaultOptionIndex,
@@ -1434,7 +1596,7 @@
params.id,
params.displayName,
params.description,
- params.icon,
+ params.iconProvider,
params.watchFaceEditorData,
params.options as List<ComplicationSlotsOption>,
params.affectedWatchFaceLayers,
@@ -1476,7 +1638,9 @@
get() = screenReaderNameInternal?.toCharSequence()
/** Icon for use in the companion style selection UI. */
- public val icon: Icon?
+ public val icon by lazy { iconProvider() }
+
+ private val iconProvider: () -> Icon?
/**
* Optional data for an on watch face editor, this will not be sent to the companion and
@@ -1515,7 +1679,7 @@
this.complicationSlotOverlays = complicationSlotOverlays
displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
screenReaderNameInternal = DisplayText.CharSequenceDisplayText(screenReaderName)
- this.icon = icon
+ this.iconProvider = { icon }
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1553,7 +1717,7 @@
displayNameInternal =
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
screenReaderNameInternal = null
- this.icon = icon
+ this.iconProvider = { icon }
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1598,7 +1762,54 @@
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
this.screenReaderNameInternal =
DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
- this.icon = icon
+ this.iconProvider = { icon }
+ this.watchFaceEditorData = watchFaceEditorData
+ }
+
+ /**
+ * Constructs a ComplicationSlotsUserStyleSetting with a lazily evaluated [icon], where
+ * [displayName] is constructed from Resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] is load.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$s` that will get replaced with
+ * the 1-based ordinal (1st, 2nd, 3rd etc...) of the ComplicationSlotsOption in the
+ * list of ComplicationSlotsOptions.
+ * @param screenReaderNameResourceId String resource id for a human readable name for
+ * the element, used by screen readers. This should be more descriptive than
+ * [displayNameResourceId]. Note if the resource string contains `%1$s` that will get
+ * replaced with the 1-based ordinal (1st, 2nd, 3rd etc...) of the
+ * ComplicationSlotsOption in the list of ComplicationSlotsOptions. Note prior to
+ * android T this is ignored by companion editors.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle
+ * selection UI. This gets lazily evaluated and is sent to the companion over
+ * bluetooth and should be small (ideally a few kb in size). Note this is not
+ * guaranteed to be called on the calling thread.
+ * @param complicationSlotOverlays Overlays to be applied when this
+ * ComplicationSlotsOption is selected. If this is empty then the net result is the
+ * initial complication configuration.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes screenReaderNameResourceId: Int,
+ iconProvider: () -> Icon?,
+ complicationSlotOverlays: Collection<ComplicationSlotOverlay>,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(id, emptyList()) {
+ this.complicationSlotOverlays = complicationSlotOverlays
+ this.displayNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ this.screenReaderNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
+ this.iconProvider = iconProvider
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1606,14 +1817,14 @@
id: Id,
displayName: DisplayText,
screenReaderName: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
complicationSlotOverlays: Collection<ComplicationSlotOverlay>
) : super(id, emptyList()) {
this.complicationSlotOverlays = complicationSlotOverlays
this.displayNameInternal = displayName
this.screenReaderNameInternal = screenReaderName
- this.icon = icon
+ this.iconProvider = iconProvider
this.watchFaceEditorData = watchFaceEditorData
}
@@ -1635,7 +1846,7 @@
}
displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
screenReaderNameInternal = null // This will get overwritten.
- icon = wireFormat.mIcon
+ iconProvider = { wireFormat.mIcon }
watchFaceEditorData = null // This will get overwritten.
}
@@ -1720,7 +1931,7 @@
defaultValue = displayName,
indexedResourceNamesSupported = true
)
- val icon = createIcon(resources, parser)
+ val iconProvider = createLazyIcon(resources, parser)
var watchFaceEditorData: WatchFaceEditorData? = null
val complicationSlotOverlays = ArrayList<ComplicationSlotOverlay>()
@@ -1751,7 +1962,7 @@
Id(id),
displayName,
screenReaderName,
- icon,
+ iconProvider,
watchFaceEditorData,
complicationSlotOverlays
)
@@ -1823,7 +2034,7 @@
params.id,
params.displayName,
params.description,
- params.icon,
+ params.iconProvider,
params.watchFaceEditorData,
minDouble,
maxDouble,
@@ -1867,7 +2078,7 @@
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
- icon,
+ { icon },
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -1916,7 +2127,59 @@
id,
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
- icon,
+ { icon },
+ watchFaceEditorData,
+ createOptionsList(minimumValue, maximumValue, defaultValue),
+ // The index of defaultValue can only ever be 0 or 1.
+ when (defaultValue) {
+ minimumValue -> 0
+ else -> 1
+ },
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a DoubleRangeUserStyleSetting with a lazily evaluated [Icon], where
+ * [DoubleRangeUserStyleSetting.displayName] and [DoubleRangeUserStyleSetting.description]
+ * are specified as resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] and
+ * [descriptionResourceId] are loaded.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI.
+ * @param descriptionResourceId String resource id for a human readable description string
+ * displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param minimumValue Minimum value (inclusive).
+ * @param maximumValue Maximum value (inclusive).
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultValue The default value for this DoubleRangeUserStyleSetting.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes descriptionResourceId: Int,
+ iconProvider: () -> Icon?,
+ minimumValue: Double,
+ maximumValue: Double,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultValue: Double,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
+ iconProvider,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -1931,7 +2194,7 @@
id: Id,
displayName: DisplayText,
description: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
minimumValue: Double,
maximumValue: Double,
@@ -1941,7 +2204,7 @@
id,
displayName,
description,
- icon,
+ iconProvider,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -2074,7 +2337,48 @@
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
- icon,
+ { icon },
+ watchFaceEditorData,
+ options,
+ options.indexOf(defaultOption),
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a ListUserStyleSetting with a lazily evaluated [Icon].
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param displayName Localized human readable name for the element, used in the userStyle
+ * selection UI.
+ * @param description Localized description string displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param options List of all options for this ListUserStyleSetting.
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultOption The default option, used when data isn't persisted.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public constructor(
+ id: Id,
+ displayName: CharSequence,
+ description: CharSequence,
+ iconProvider: () -> Icon?,
+ options: List<ListOption>,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultOption: ListOption = options.first(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.CharSequenceDisplayText(displayName),
+ DisplayText.CharSequenceDisplayText(description),
+ iconProvider,
watchFaceEditorData,
options,
options.indexOf(defaultOption),
@@ -2117,7 +2421,53 @@
id,
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
- icon,
+ { icon },
+ watchFaceEditorData,
+ options,
+ options.indexOf(defaultOption),
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a ListUserStyleSetting with a lazily evaluated [Icon], where
+ * [ListUserStyleSetting.displayName] and [ListUserStyleSetting.description] are specified
+ * as resources.
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] and
+ * [descriptionResourceId] are loaded.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI.
+ * @param descriptionResourceId String resource id for a human readable description string
+ * displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param options List of all options for this ListUserStyleSetting.
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultOption The default option, used when data isn't persisted.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes descriptionResourceId: Int,
+ iconProvider: () -> Icon?,
+ options: List<ListOption>,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultOption: ListOption = options.first(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
+ iconProvider,
watchFaceEditorData,
options,
options.indexOf(defaultOption),
@@ -2128,7 +2478,7 @@
id: Id,
displayName: DisplayText,
description: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
options: List<ListOption>,
affectsWatchFaceLayers: Collection<WatchFaceLayer>,
@@ -2137,7 +2487,7 @@
id,
displayName,
description,
- icon,
+ iconProvider,
watchFaceEditorData,
options,
defaultOptionIndex,
@@ -2221,7 +2571,7 @@
params.id,
params.displayName,
params.description,
- params.icon,
+ params.iconProvider,
params.watchFaceEditorData,
params.options as List<ListOption>,
params.affectedWatchFaceLayers,
@@ -2259,7 +2609,10 @@
get() = screenReaderNameInternal?.toCharSequence()
/** Icon for use in the companion style selection UI. */
- public val icon: Icon?
+ public val icon by lazy { TraceEvent("invoke iconProvider").use { iconProvider() } }
+
+ /** To avoid upfront costs, icons are lazily evaluated. */
+ private val iconProvider: () -> Icon?
/**
* Optional data for an on watch face editor, this will not be sent to the companion and
@@ -2297,7 +2650,23 @@
) : super(id, childSettings) {
displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
screenReaderNameInternal = DisplayText.CharSequenceDisplayText(screenReaderName)
- this.icon = icon
+ this.iconProvider = { icon }
+ this.watchFaceEditorData = watchFaceEditorData
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmOverloads
+ constructor(
+ id: Id,
+ displayName: CharSequence,
+ screenReaderName: CharSequence,
+ iconProvider: () -> Icon?,
+ childSettings: Collection<UserStyleSetting> = emptyList(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(id, childSettings) {
+ displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+ screenReaderNameInternal = DisplayText.CharSequenceDisplayText(screenReaderName)
+ this.iconProvider = iconProvider
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2330,7 +2699,7 @@
displayNameInternal =
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
screenReaderNameInternal = null
- this.icon = icon
+ this.iconProvider = { icon }
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2363,7 +2732,7 @@
displayNameInternal =
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
screenReaderNameInternal = null
- this.icon = icon
+ this.iconProvider = { icon }
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2405,7 +2774,51 @@
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
screenReaderNameInternal =
DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
- this.icon = icon
+ this.iconProvider = { icon }
+ this.watchFaceEditorData = watchFaceEditorData
+ }
+
+ /**
+ * Constructs a ListOption with a lazily evaluated [icon].
+ *
+ * @param id The [Id] of this ListOption, must be unique within the
+ * [ListUserStyleSetting].
+ * @param resources The [Resources] used to load [displayNameResourceId].
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI. This should be short, ideally < 20
+ * characters. Note if the resource string contains `%1$s` that will get replaced with
+ * the 1-based ordinal (1st, 2nd, 3rd etc...) of the ListOption in the list of
+ * ListOptions.
+ * @param screenReaderNameResourceId String resource id for a human readable name for
+ * the element, used by screen readers. This should be more descriptive than
+ * [displayNameResourceId]. Note if the resource string contains `%1$s` that will get
+ * replaced with the 1-based ordinal (1st, 2nd, 3rd etc...) of the ListOption in the
+ * list of ListOptions. Note prior to android T this is ignored by companion editors.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle
+ * selection UI. This gets lazily evaluated and is sent to the companion over
+ * bluetooth and should be small (ideally a few kb in size). Note this is not
+ * guaranteed to be called on the calling thread.
+ * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
+ * child settings must be listed in [UserStyleSchema.userStyleSettings].
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+ * be sent to the companion and its contents may be used in preference to other fields
+ * by an on watch face editor.
+ */
+ @JvmOverloads
+ constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes screenReaderNameResourceId: Int,
+ iconProvider: () -> Icon?,
+ childSettings: Collection<UserStyleSetting> = emptyList(),
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(id, childSettings) {
+ displayNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+ screenReaderNameInternal =
+ DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
+ this.iconProvider = iconProvider
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2413,13 +2826,13 @@
id: Id,
displayName: DisplayText,
screenReaderName: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
childSettings: Collection<UserStyleSetting> = emptyList()
) : super(id, childSettings) {
displayNameInternal = displayName
screenReaderNameInternal = screenReaderName
- this.icon = icon
+ this.iconProvider = iconProvider
this.watchFaceEditorData = watchFaceEditorData
}
@@ -2428,7 +2841,7 @@
) : super(Id(wireFormat.mId), ArrayList()) {
displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
screenReaderNameInternal = null // This will get overwritten.
- icon = wireFormat.mIcon
+ iconProvider = { wireFormat.mIcon }
watchFaceEditorData = null // This gets overwritten.
}
@@ -2492,7 +2905,7 @@
defaultValue = displayName,
indexedResourceNamesSupported = true
)
- val icon = createIcon(resources, parser)
+ val iconProvider = createLazyIcon(resources, parser)
var watchFaceEditorData: WatchFaceEditorData? = null
val childSettings = ArrayList<UserStyleSetting>()
@@ -2524,7 +2937,7 @@
Id(id),
displayName,
screenReaderName,
- icon,
+ iconProvider,
watchFaceEditorData,
childSettings
)
@@ -2596,7 +3009,7 @@
params.id,
params.displayName,
params.description,
- params.icon,
+ params.iconProvider,
params.watchFaceEditorData,
minInteger,
maxInteger,
@@ -2640,7 +3053,7 @@
id,
DisplayText.CharSequenceDisplayText(displayName),
DisplayText.CharSequenceDisplayText(description),
- icon,
+ { icon },
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -2689,7 +3102,59 @@
id,
DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
- icon,
+ { icon },
+ watchFaceEditorData,
+ createOptionsList(minimumValue, maximumValue, defaultValue),
+ // The index of defaultValue can only ever be 0 or 1.
+ when (defaultValue) {
+ minimumValue -> 0
+ else -> 1
+ },
+ affectsWatchFaceLayers
+ )
+
+ /**
+ * Constructs a LongRangeUserStyleSetting where [LongRangeUserStyleSetting.displayName] and
+ * [LongRangeUserStyleSetting.description] are specified as resources, with a lazily
+ * constructed [icon].
+ *
+ * @param id [Id] for the element, must be unique.
+ * @param resources The [Resources] from which [displayNameResourceId] and
+ * [descriptionResourceId] are loaded.
+ * @param displayNameResourceId String resource id for a human readable name for the
+ * element, used in the userStyle selection UI.
+ * @param descriptionResourceId String resource id for a human readable description string
+ * displayed under the displayName.
+ * @param iconProvider A provider of an [Icon] for use in the companion userStyle selection
+ * UI. This gets lazily evaluated and is sent to the companion over bluetooth and should
+ * be small (ideally a few kb in size). Note this is not guaranteed to be called on the
+ * calling thread.
+ * @param minimumValue Minimum value (inclusive).
+ * @param maximumValue Maximum value (inclusive).
+ * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
+ * face rendering layers this style affects.
+ * @param defaultValue The default value for this LongRangeUserStyleSetting.
+ * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+ * sent to the companion and its contents may be used in preference to other fields by an
+ * on watch face editor.
+ */
+ @JvmOverloads
+ public constructor(
+ id: Id,
+ resources: Resources,
+ @StringRes displayNameResourceId: Int,
+ @StringRes descriptionResourceId: Int,
+ iconProvider: () -> Icon?,
+ minimumValue: Long,
+ maximumValue: Long,
+ affectsWatchFaceLayers: Collection<WatchFaceLayer>,
+ defaultValue: Long,
+ watchFaceEditorData: WatchFaceEditorData? = null
+ ) : super(
+ id,
+ DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+ DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
+ iconProvider,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -2704,7 +3169,7 @@
id: Id,
displayName: DisplayText,
description: DisplayText,
- icon: Icon?,
+ iconProvider: () -> Icon?,
watchFaceEditorData: WatchFaceEditorData?,
minimumValue: Long,
maximumValue: Long,
@@ -2714,7 +3179,7 @@
id,
displayName,
description,
- icon,
+ iconProvider,
watchFaceEditorData,
createOptionsList(minimumValue, maximumValue, defaultValue),
// The index of defaultValue can only ever be 0 or 1.
@@ -2840,7 +3305,7 @@
Id(CUSTOM_VALUE_USER_STYLE_SETTING_ID),
DisplayText.CharSequenceDisplayText(""),
DisplayText.CharSequenceDisplayText(""),
- null,
+ { null },
null,
listOf(CustomValueOption(defaultValue)),
0,
@@ -2934,7 +3399,7 @@
Id(CUSTOM_VALUE_USER_STYLE_SETTING_ID),
DisplayText.CharSequenceDisplayText(""),
DisplayText.CharSequenceDisplayText(""),
- null,
+ { null },
null,
listOf(CustomValueOption(defaultValue)),
0,
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index 8b5f470..2204343 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -48,9 +48,9 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.mockitoKotlin)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(project(":wear:watchface:watchface-complications-rendering"))
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 138bcd1..6331e3b 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -92,21 +92,21 @@
resources,
R.string.colors_style_red,
R.string.colors_style_red_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
),
ListUserStyleSetting.ListOption(
Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
R.string.colors_style_green_screen_reader,
- Icon.createWithResource(this, R.drawable.green_style)
+ { Icon.createWithResource(this, R.drawable.green_style) }
),
ListUserStyleSetting.ListOption(
Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
R.string.colors_style_blue_screen_reader,
- Icon.createWithResource(this, R.drawable.blue_style)
+ { Icon.createWithResource(this, R.drawable.blue_style) }
)
),
listOf(
@@ -161,8 +161,7 @@
R.string.watchface_complications_setting_both,
null,
// NB this list is empty because each [ComplicationSlotOverlay] is applied
- // on
- // top of the initial config.
+ // on top of the initial config.
listOf()
),
ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index 96757db..eab0525 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -90,21 +90,21 @@
resources,
R.string.colors_style_red,
R.string.colors_style_red_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
),
UserStyleSetting.ListUserStyleSetting.ListOption(
Option.Id(GREEN_STYLE),
resources,
R.string.colors_style_green,
R.string.colors_style_green_screen_reader,
- Icon.createWithResource(this, R.drawable.green_style)
+ { Icon.createWithResource(this, R.drawable.green_style) }
),
UserStyleSetting.ListUserStyleSetting.ListOption(
Option.Id(BLUE_STYLE),
resources,
R.string.colors_style_blue,
R.string.colors_style_blue_screen_reader,
- Icon.createWithResource(this, R.drawable.blue_style)
+ { Icon.createWithResource(this, R.drawable.blue_style) }
)
),
listOf(
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 205e03f..2db1132 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -63,7 +63,7 @@
resources,
R.string.digital_clock_style_12,
R.string.digital_clock_style_12_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
)
}
@@ -73,7 +73,7 @@
resources,
R.string.digital_clock_style_24,
R.string.digital_clock_style_24_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
)
}
@@ -91,7 +91,8 @@
UserStyleSetting.Option.Id("On"),
resources,
R.string.digital_complication_on_screen_name,
- Icon.createWithResource(this, R.drawable.on),
+ R.string.digital_complication_on_screen_name,
+ { Icon.createWithResource(this, R.drawable.on) },
listOf(
ComplicationSlotOverlay(
COMPLICATION1_ID,
@@ -107,7 +108,8 @@
UserStyleSetting.Option.Id("Off"),
resources,
R.string.digital_complication_off_screen_name,
- Icon.createWithResource(this, R.drawable.off),
+ R.string.digital_complication_on_screen_name,
+ { Icon.createWithResource(this, R.drawable.off) },
listOf(
ComplicationSlotOverlay(COMPLICATION1_ID, enabled = false),
ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
@@ -137,7 +139,7 @@
resources,
R.string.colors_style_red,
R.string.colors_style_red_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
)
}
@@ -147,7 +149,7 @@
resources,
R.string.colors_style_green,
R.string.colors_style_green_screen_reader,
- Icon.createWithResource(this, R.drawable.green_style)
+ { Icon.createWithResource(this, R.drawable.green_style) }
)
}
@@ -157,7 +159,7 @@
resources,
R.string.colors_style_blue,
R.string.colors_style_blue_screen_reader,
- Icon.createWithResource(this, R.drawable.blue_style)
+ { Icon.createWithResource(this, R.drawable.blue_style) }
)
}
@@ -204,7 +206,8 @@
UserStyleSetting.Option.Id("One"),
resources,
R.string.analog_complication_one_screen_name,
- Icon.createWithResource(this, R.drawable.one),
+ R.string.analog_complication_one_screen_name,
+ { Icon.createWithResource(this, R.drawable.one) },
listOf(
ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
@@ -215,7 +218,8 @@
UserStyleSetting.Option.Id("Two"),
resources,
R.string.analog_complication_two_screen_name,
- Icon.createWithResource(this, R.drawable.two),
+ R.string.analog_complication_two_screen_name,
+ { Icon.createWithResource(this, R.drawable.two) },
listOf(
ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
@@ -226,7 +230,8 @@
UserStyleSetting.Option.Id("Three"),
resources,
R.string.analog_complication_three_screen_name,
- Icon.createWithResource(this, R.drawable.three),
+ R.string.analog_complication_three_screen_name,
+ { Icon.createWithResource(this, R.drawable.three) },
listOf(
ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
@@ -244,7 +249,7 @@
resources,
R.string.style_digital_watch,
R.string.style_digital_watch_screen_reader,
- icon = Icon.createWithResource(this, R.drawable.d),
+ iconProvider = { Icon.createWithResource(this, R.drawable.d) },
childSettings =
listOf(digitalClockStyleSetting, colorStyleSetting, digitalComplicationSettings)
)
@@ -256,7 +261,7 @@
resources,
R.string.style_analog_watch,
R.string.style_analog_watch_screen_reader,
- icon = Icon.createWithResource(this, R.drawable.a),
+ iconProvider = { Icon.createWithResource(this, R.drawable.a) },
childSettings = listOf(colorStyleSetting, drawHoursSetting, analogComplicationSettings)
)
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index f61f2b5..7c13751 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -57,14 +57,14 @@
resources,
R.string.colors_style_yellow,
R.string.colors_style_yellow_screen_reader,
- Icon.createWithResource(this, R.drawable.yellow_style)
+ { Icon.createWithResource(this, R.drawable.yellow_style) }
),
UserStyleSetting.ListUserStyleSetting.ListOption(
UserStyleSetting.Option.Id("blue_style"),
resources,
R.string.colors_style_blue,
R.string.colors_style_blue_screen_reader,
- Icon.createWithResource(this, R.drawable.blue_style)
+ { Icon.createWithResource(this, R.drawable.blue_style) }
)
),
listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 3831a29..623ade9 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -81,14 +81,14 @@
resources,
R.string.colors_style_red,
R.string.colors_style_red_screen_reader,
- Icon.createWithResource(this, R.drawable.red_style)
+ { Icon.createWithResource(this, R.drawable.red_style) }
),
ListUserStyleSetting.ListOption(
Option.Id("green_style"),
resources,
R.string.colors_style_green,
R.string.colors_style_green_screen_reader,
- Icon.createWithResource(this, R.drawable.green_style)
+ { Icon.createWithResource(this, R.drawable.green_style) }
)
),
listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index c065779..421dc62 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -999,7 +999,6 @@
}
if (watchState.isHeadless) {
headlessWatchFaceImpl!!.release()
- [email protected]()
}
}
}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 2c52bc1..0888ae7 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1267,6 +1267,8 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public val deferredWatchFaceImpl = CompletableDeferred<WatchFaceImpl>()
+ public val deferredFirstFrame = CompletableDeferred<Unit>()
+
@VisibleForTesting public var deferredValidation = CompletableDeferred<Unit>()
/**
@@ -1728,6 +1730,9 @@
synchronized(lock) { editedComplicationPreviewData.clear() }
}
+ internal fun hasOverriddenComplications(): Boolean =
+ synchronized(lock) { overriddenComplications?.isNotEmpty() ?: false }
+
/**
* Undoes any complication overrides by [overrideComplicationsForEditing], restoring the
* original data. In addition any complications marked as being cleared after editing by
@@ -1791,7 +1796,12 @@
pair.key,
pair.value,
now,
- forceLoad = mutableWatchState.isAmbient.value ?: false,
+ // Force synchronous complication image update if there's overridden
+ // complications or if we're rendering ambient frames where the next
+ // frame might be up to a minute away.
+ forceLoad =
+ hasOverriddenComplications() ||
+ (mutableWatchState.isAmbient.value ?: false),
)
}
complicationSlotsManager.onComplicationsUpdated()
@@ -1927,6 +1937,7 @@
if (this::choreographer.isInitialized) {
frameCallback?.let { choreographer.removeFrameCallback(it) }
}
+ frameCallback = null
if (this::interactiveInstanceId.isInitialized) {
InteractiveInstanceManager.deleteInstance(interactiveInstanceId)
}
@@ -2438,9 +2449,14 @@
// Now init has completed, it's OK to complete deferredWatchFaceImpl.
initComplicationsDone.complete(Unit)
- // validateSchemaWireSize is fairly expensive so only perform it for
- // interactive watch faces.
+ // validateSchemaWireSize is fairly expensive so only perform it for interactive
+ // watch faces.
if (!watchState.isHeadless) {
+ // Wait until the first frame has been rendered since
+ // validateSchemaWireSize is computationally expensive and it may trigger
+ // lazy Icon construction and we want to avoid CPU contention with user
+ // visible tasks.
+ deferredFirstFrame.await()
validateSchemaWireSize(currentUserStyleRepository.schema)
}
} catch (e: CancellationException) {
@@ -2521,6 +2537,7 @@
}
}
}
+ deferredFirstFrame.complete(Unit)
Log.d(TAG, "init complete ${watchState.watchFaceInstanceId.value}")
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 28b26e9..62c809f 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -3164,6 +3164,62 @@
}
@Test
+ public fun overrideComplicationData() {
+ initWallpaperInteractiveWatchFaceInstance(
+ complicationSlots = listOf(mockComplication, mockComplication2)
+ )
+ // Set initial complications.
+ val liveComplication1 =
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("Live complication1"))
+ .setDataSource(ComponentName("one.com", "one"))
+ .build()
+ val liveComplication2 =
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("Live complication2"))
+ .setDataSource(ComponentName("two.com", "one"))
+ .build()
+ interactiveWatchFaceInstance.updateComplicationData(
+ listOf(
+ IdAndComplicationDataWireFormat(MOCK_COMPLICATION_ID, liveComplication1),
+ IdAndComplicationDataWireFormat(MOCK_COMPLICATION_ID2, liveComplication2)
+ )
+ )
+ reset(mockCanvasComplication)
+ reset(mockCanvasComplication2)
+
+ // Preview complications set by the editor.
+ val previewComplication1 =
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("Preview complication1"))
+ .setDataSource(ComponentName("one.com", "one"))
+ .build()
+ val previewComplication2 =
+ WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+ .setLongText(WireComplicationText.plainText("Preview complication2"))
+ .setDataSource(ComponentName("two.com", "one"))
+ .build()
+ interactiveWatchFaceInstance.overrideComplicationData(
+ listOf(
+ IdAndComplicationDataWireFormat(MOCK_COMPLICATION_ID, previewComplication1),
+ IdAndComplicationDataWireFormat(MOCK_COMPLICATION_ID2, previewComplication2)
+ )
+ )
+
+ // The updates should be synchronous.
+ verify(mockCanvasComplication)
+ .loadData(
+ previewComplication1.toApiComplicationData(),
+ loadDrawablesAsynchronous = false
+ )
+ verify(mockCanvasComplication2)
+ .loadData(
+ previewComplication2.toApiComplicationData(),
+ loadDrawablesAsynchronous = false
+ )
+ }
+
+ @Test
public fun overrideComplicationData_onEditSessionFinished() {
initWallpaperInteractiveWatchFaceInstance(
complicationSlots = listOf(mockComplication, mockComplication2)
@@ -3224,11 +3280,11 @@
// MOCK_COMPLICATION_ID was unchanged so we should load the origional.
verify(mockCanvasComplication)
- .loadData(liveComplication1.toApiComplicationData(), loadDrawablesAsynchronous = true)
+ .loadData(liveComplication1.toApiComplicationData(), loadDrawablesAsynchronous = false)
// MOCK_COMPLICATION_ID was changed so we should load empty to prevent the user from seeing
// a glimpse of the old complication.
verify(mockCanvasComplication2)
- .loadData(EmptyComplicationData(), loadDrawablesAsynchronous = true)
+ .loadData(EmptyComplicationData(), loadDrawablesAsynchronous = false)
}
@Test
@@ -7756,6 +7812,14 @@
) {
// Intentionally empty.
}
+
+ var destroyed: Boolean = false
+
+ override fun onDestroy() {
+ super.onDestroy()
+ assert(!destroyed) { "onDestroy already called!!" }
+ destroyed = true
+ }
}
)
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 41e2757..bc59963 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -30,8 +30,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.espressoCore, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.kotlinStdlib)
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 26122f9..224156b 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -5,5 +5,5 @@
"preinstalled on WearOS devices."
gerrit source: "vendor/google_clockwork/sdk/lib"
API version: 35.1
-Build ID: 12239970
-Last updated: Fri Aug 16 06:57:41 PM UTC 2024
+Build ID: 12295646
+Last updated: Thu Aug 29 08:37:30 PM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 36f61d4..43b434d 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ
diff --git a/webkit/integration-tests/instrumentation/build.gradle b/webkit/integration-tests/instrumentation/build.gradle
index 289f6b7..377fea1 100644
--- a/webkit/integration-tests/instrumentation/build.gradle
+++ b/webkit/integration-tests/instrumentation/build.gradle
@@ -45,8 +45,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation("androidx.concurrent:concurrent-futures:1.0.0")
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
// Hamcrest matchers:
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 6d26540..09d8d9a 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -46,9 +46,8 @@
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
androidTestImplementation(libs.espressoIdlingResource)
androidTestImplementation(libs.espressoWeb, excludes.espresso)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
}
// We want to publish a release apk of this project for webkit team's use
diff --git a/webkit/webkit/build.gradle b/webkit/webkit/build.gradle
index 193e4b7..8be9b86 100644
--- a/webkit/webkit/build.gradle
+++ b/webkit/webkit/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testRules)
androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
androidTestImplementation("androidx.concurrent:concurrent-futures:1.0.0")
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
// Hamcrest matchers:
androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 399cc93..2643332 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -44,8 +44,8 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
}
androidx {
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index 2c8a4d2..7b79461 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -28,13 +28,13 @@
ctor public WindowSizeClass(float widthDp, float heightDp);
ctor public WindowSizeClass(int minWidthDp, int minHeightDp);
method @Deprecated public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
+ method public boolean containsHeightDp(int heightDp);
+ method public boolean containsWidthDp(int widthDp);
+ method public boolean containsWindowSizeDp(int widthDp, int heightDp);
method public int getMinHeightDp();
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
@@ -52,6 +52,7 @@
}
public final class WindowSizeClassSelectors {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, float widthDp, float heightDp);
method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClassPreferHeight(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
}
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index 2c8a4d2..7b79461 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -28,13 +28,13 @@
ctor public WindowSizeClass(float widthDp, float heightDp);
ctor public WindowSizeClass(int minWidthDp, int minHeightDp);
method @Deprecated public static androidx.window.core.layout.WindowSizeClass compute(float dpWidth, float dpHeight);
+ method public boolean containsHeightDp(int heightDp);
+ method public boolean containsWidthDp(int widthDp);
+ method public boolean containsWindowSizeDp(int widthDp, int heightDp);
method public int getMinHeightDp();
method public int getMinWidthDp();
method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
- method public boolean isAtLeast(int widthDp, int heightDp);
- method public boolean isHeightAtLeast(int heightDp);
- method public boolean isWidthAtLeast(int widthDp);
property public final int minHeightDp;
property public final int minWidthDp;
property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
@@ -52,6 +52,7 @@
}
public final class WindowSizeClassSelectors {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, float widthDp, float heightDp);
method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClassPreferHeight(java.util.Set<androidx.window.core.layout.WindowSizeClass>, int widthDp, int heightDp);
}
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
index 43bef20..964360a 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
@@ -45,6 +45,11 @@
* these cases developers may wish to specify their own custom break points and match using a `when`
* statement.
*
+ * To process a [WindowSizeClass] use the methods [containsWindowSizeDp], [containsWidthDp],
+ * [containsHeightDp] methods. Note these methods are order dependent as the smaller [minWidthDp]
+ * and [minHeightDp] would match all the breakpoints that are larger. Therefore when processing the
+ * selection should normally be ordered from larger to smaller breakpoints.
+ *
* @see WindowWidthSizeClass
* @see WindowHeightSizeClass
*/
@@ -80,25 +85,34 @@
get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
/**
- * Returns `true` when [widthDp] is greater than or equal to [minWidthDp], `false` otherwise.
+ * Returns `true` when [minWidthDp] is greater than or equal to [widthDp], `false` otherwise.
+ * When processing a [WindowSizeClass] note that this method is order dependent. A
+ * [WindowSizeClass] with [minWidthDp] = 0 and [minHeightDp] = 0 will match any breakpoint, so
+ * the selection should normally go from largest to smallest breakpoints.
*/
- fun isWidthAtLeast(widthDp: Int): Boolean {
- return widthDp >= minWidthDp
+ fun containsWidthDp(widthDp: Int): Boolean {
+ return minWidthDp >= widthDp
}
/**
- * Returns `true` when [heightDp] is greater than or equal to [minHeightDp], `false` otherwise.
+ * Returns `true` when [minHeightDp] is greater than or equal to [heightDp], `false` otherwise.
+ * When processing a [WindowSizeClass] note that this method is order dependent. A
+ * [WindowSizeClass] with [minWidthDp] = 0 and [minHeightDp] = 0 will match any breakpoint, so
+ * the selection should normally go from largest to smallest breakpoints.
*/
- fun isHeightAtLeast(heightDp: Int): Boolean {
- return heightDp >= minHeightDp
+ fun containsHeightDp(heightDp: Int): Boolean {
+ return minHeightDp >= heightDp
}
/**
* Returns `true` when [widthDp] is greater than or equal to [minWidthDp] and [heightDp] is
- * greater than or equal to [minHeightDp], `false` otherwise.
+ * greater than or equal to [minHeightDp], `false` otherwise. When processing a
+ * [WindowSizeClass] note that this method is order dependent. A [WindowSizeClass] with
+ * [minWidthDp] = 0 and [minHeightDp] = 0 will match any breakpoint, so * the selection should
+ * normally go from largest to smallest breakpoints.
*/
- fun isAtLeast(widthDp: Int, heightDp: Int): Boolean {
- return isWidthAtLeast(widthDp) && isHeightAtLeast(heightDp)
+ fun containsWindowSizeDp(widthDp: Int, heightDp: Int): Boolean {
+ return containsWidthDp(widthDp) && containsHeightDp(heightDp)
}
override fun equals(other: Any?): Boolean {
@@ -137,10 +151,10 @@
const val HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
private val WIDTH_DP_BREAKPOINTS_V1 =
- listOf(WIDTH_DP_MEDIUM_LOWER_BOUND, WIDTH_DP_EXPANDED_LOWER_BOUND)
+ listOf(0, WIDTH_DP_MEDIUM_LOWER_BOUND, WIDTH_DP_EXPANDED_LOWER_BOUND)
private val HEIGHT_DP_BREAKPOINTS_V1 =
- listOf(HEIGHT_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ listOf(0, HEIGHT_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_EXPANDED_LOWER_BOUND)
@JvmField
val BREAKPOINTS_V1 =
@@ -160,7 +174,13 @@
* @throws IllegalArgumentException if [dpWidth] or [dpHeight] is negative.
*/
@JvmStatic
- @Deprecated("Use the constructor instead.")
+ @Deprecated(
+ "Use computeWindowSizeClass instead.",
+ ReplaceWith(
+ "BREAKPOINTS_V1.computeWindowSizeClass(widthDp = dpWidth, heightDp = dpHeight)",
+ "androidx.window.core.layout.computeWindowSizeClass"
+ )
+ )
fun compute(dpWidth: Float, dpHeight: Float): WindowSizeClass {
val widthDp =
when {
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt
index 25ef7c0..5215cdb 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClassSelectors.kt
@@ -24,6 +24,23 @@
* Returns the largest [WindowSizeClass] that is within the bounds of ([widthDp], [heightDp]). This
* method prefers width and uses max height to break ties. If there is no match a default of
* `WindowSizeClass(0,0)` is returned. Examples: Input: Set: `setOf(WindowSizeClass(300, 300),
+ * WindowSizeClass(300, 600)` widthDp: `300.5f` heightDp: `800.5f` Output: `WindowSizeClass(300,
+ * 600)` Input: Set: `setOf(WindowSizeClass(300, 300), WindowSizeClass(300, 600)` widthDp: `300`
+ * heightDp: `400` Output: `WindowSizeClass(300, 300)`. This is an overload that truncates the
+ * floats to integers.
+ *
+ * @param widthDp the width of the window to match a [WindowSizeClass] to.
+ * @param heightDp the height of the window to match a [WindowSizeClass] to.
+ * @see computeWindowSizeClass
+ */
+fun Set<WindowSizeClass>.computeWindowSizeClass(widthDp: Float, heightDp: Float): WindowSizeClass {
+ return computeWindowSizeClass(widthDp.toInt(), heightDp.toInt())
+}
+
+/**
+ * Returns the largest [WindowSizeClass] that is within the bounds of ([widthDp], [heightDp]). This
+ * method prefers width and uses max height to break ties. If there is no match a default of
+ * `WindowSizeClass(0,0)` is returned. Examples: Input: Set: `setOf(WindowSizeClass(300, 300),
* WindowSizeClass(300, 600)` widthDp: `300` heightDp: `800` Output: `WindowSizeClass(300, 600)`
* Input: Set: `setOf(WindowSizeClass(300, 300), WindowSizeClass(300, 600)` widthDp: `300` heightDp:
* `400` Output: `WindowSizeClass(300, 300)`
@@ -43,7 +60,7 @@
if (
bucket.minWidthDp == maxWidth &&
bucket.minHeightDp <= heightDp &&
- match.minHeightDp < bucket.minHeightDp
+ match.minHeightDp <= bucket.minHeightDp
) {
match = bucket
}
@@ -77,7 +94,7 @@
if (
bucket.minHeightDp == maxHeight &&
bucket.minWidthDp <= widthDp &&
- match.minWidthDp < bucket.minWidthDp
+ match.minWidthDp <= bucket.minWidthDp
) {
match = bucket
}
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt
index f72e378..c928fc1 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassSelectorsTest.kt
@@ -16,14 +16,34 @@
package androidx.window.core.layout
+import androidx.window.core.layout.WindowSizeClass.Companion.BREAKPOINTS_V1
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_EXPANDED_LOWER_BOUND
import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import kotlin.test.Test
import kotlin.test.assertEquals
class WindowSizeClassSelectorsTest {
- val coreSet = WindowSizeClass.BREAKPOINTS_V1
+ val coreSet = BREAKPOINTS_V1
+
+ @Test
+ fun compute_window_size_class_with_floats_truncates() {
+ // coreSet does not contain 10, 10
+ val intResult =
+ coreSet.computeWindowSizeClass(
+ WIDTH_DP_MEDIUM_LOWER_BOUND,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND
+ )
+ val floatResult =
+ coreSet.computeWindowSizeClass(
+ WIDTH_DP_MEDIUM_LOWER_BOUND + .9f,
+ HEIGHT_DP_MEDIUM_LOWER_BOUND + .9f
+ )
+
+ assertEquals(intResult, floatResult)
+ }
@Test
fun compute_window_size_class_returns_zero_for_default() {
@@ -152,4 +172,20 @@
assertEquals(expected, actual)
}
+
+ @Test
+ fun edge_case_matching_bucket_has_min_height_0() {
+ val expected = WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ val actual = BREAKPOINTS_V1.computeWindowSizeClass(1290, 400)
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun edge_case_matching_bucket_has_min_width_0() {
+ val expected = WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ val actual = BREAKPOINTS_V1.computeWindowSizeClassPreferHeight(400, 1290)
+
+ assertEquals(expected, actual)
+ }
}
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
index 816e40c..88d9f0f 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
@@ -16,6 +16,11 @@
package androidx.window.core.layout
+import androidx.window.core.layout.WindowSizeClass.Companion.BREAKPOINTS_V1
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -43,6 +48,34 @@
assertEquals(expected, actual)
}
+ @Test
+ @Suppress("DEPRECATION")
+ fun test_breakpoint_matches_original_set() {
+ val breakpoints = BREAKPOINTS_V1
+
+ val expectedWidthBreakpoints =
+ setOf(
+ WindowWidthSizeClass.COMPACT,
+ WindowWidthSizeClass.MEDIUM,
+ WindowWidthSizeClass.EXPANDED
+ )
+
+ val expectedHeightBreakpoints =
+ setOf(
+ WindowHeightSizeClass.COMPACT,
+ WindowHeightSizeClass.MEDIUM,
+ WindowHeightSizeClass.EXPANDED
+ )
+
+ val actualWidthBreakpoints =
+ breakpoints.map { sizeClass -> sizeClass.windowWidthSizeClass }.toSet()
+ val actualHeightBreakpoints =
+ breakpoints.map { sizeClass -> sizeClass.windowHeightSizeClass }.toSet()
+
+ assertEquals(expectedWidthBreakpoints, actualWidthBreakpoints)
+ assertEquals(expectedHeightBreakpoints, actualHeightBreakpoints)
+ }
+
@Suppress("DEPRECATION")
@Test
fun testWindowSizeClass_computeRounds() {
@@ -115,85 +148,252 @@
}
@Test
- fun is_width_at_least_returns_true_when_input_is_greater() {
+ fun is_width_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width + 1))
+ assertFalse(sizeClass.containsWidthDp(width + 1))
}
@Test
- fun is_width_at_least_returns_true_when_input_is_equal() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isWidthAtLeast(width))
+ assertTrue(sizeClass.containsWidthDp(width))
}
@Test
- fun is_width_at_least_returns_false_when_input_is_smaller() {
+ fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isWidthAtLeast(width - 1))
+ assertTrue(sizeClass.containsWidthDp(width - 1))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * WIDTH_DP_MEDIUM_LOWER_BOUND = 600 WIDTH_DP_EXPANDED_LOWER_BOUND = 840
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 300, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 0) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 840, minHeightDp = 0)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_width_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .containsWidthDp(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+ .containsWidthDp(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .containsWidthDp(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+ .containsWidthDp(WIDTH_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).containsWidthDp(WIDTH_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).containsWidthDp(WIDTH_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * HEIGHT_DP_MEDIUM_LOWER_BOUND = 480 HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when {
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+ * sizeClass.isHeightAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 480) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_height_at_least_bounds_checks() {
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .containsHeightDp(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ .containsHeightDp(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .containsHeightDp(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+ )
+ assertTrue(
+ WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ .containsHeightDp(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).containsHeightDp(HEIGHT_DP_EXPANDED_LOWER_BOUND))
+ assertFalse(WindowSizeClass(0, 0).containsHeightDp(HEIGHT_DP_MEDIUM_LOWER_BOUND))
+ }
+
+ /**
+ * Tests that the width breakpoint logic works as expected. The following sample shows what the
+ * dev use site should be
+ *
+ * DIAGONAL_BOUND_MEDIUM = 600, 600 DIAGONAL_BOUND_EXPANDED = 900, 900
+ *
+ * fun process(sizeClass: WindowSizeClass) { when { sizeClass.isAtLeast(DIAGONAL_BOUND_EXPANDED,
+ * DIAGONAL_BOUND_EXPANDED) -> doExpanded() sizeClass.isAtLeast(DIAGONAL_BOUND_MEDIUM,
+ * DIAGONAL_BOUND_MEDIUM) -> doMedium() else -> doCompact() } }
+ *
+ * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+ * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 600) val
+ * expandedBreakpoint = WindowSizeClass(minWidthDp = 900, minHeightDp = 900)
+ *
+ * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+ * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+ *
+ * So the following must be true
+ *
+ * expandedBreakpoint WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+ * true WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * equalMediumBreakpoint WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+ * == false WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+ *
+ * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+ * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+ */
+ @Test
+ fun is_area_at_least_bounds_checks() {
+ val diagonalMedium = 600
+ val diagonalExpanded = 900
+ // expandedBreakpoint
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .containsWindowSizeDp(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalExpanded, diagonalExpanded)
+ .containsWindowSizeDp(diagonalMedium, diagonalMedium)
+ )
+
+ // equalMediumBreakpoint
+ assertFalse(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .containsWindowSizeDp(diagonalExpanded, diagonalExpanded)
+ )
+ assertTrue(
+ WindowSizeClass(diagonalMedium, diagonalMedium)
+ .containsWindowSizeDp(diagonalMedium, diagonalMedium)
+ )
+
+ // belowBreakpoint
+ assertFalse(WindowSizeClass(0, 0).containsWindowSizeDp(diagonalExpanded, diagonalExpanded))
+ assertFalse(WindowSizeClass(0, 0).containsWindowSizeDp(diagonalMedium, diagonalMedium))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_greater() {
+ fun is_height_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height + 1))
+ assertFalse(sizeClass.containsHeightDp(height + 1))
}
@Test
- fun is_height_at_least_returns_true_when_input_is_equal() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isHeightAtLeast(height))
+ assertTrue(sizeClass.containsHeightDp(height))
}
@Test
- fun is_height_at_least_returns_false_when_input_is_smaller() {
+ fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isHeightAtLeast(height - 1))
+ assertTrue(sizeClass.containsHeightDp(height - 1))
}
@Test
- fun is_at_least_returns_true_when_input_is_greater() {
+ fun is_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height + 1))
- assertTrue(sizeClass.isAtLeast(width + 1, height))
+ assertFalse(sizeClass.containsWindowSizeDp(width, height + 1))
+ assertFalse(sizeClass.containsWindowSizeDp(width + 1, height))
}
@Test
- fun is_at_least_returns_true_when_input_is_equal() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertTrue(sizeClass.isAtLeast(width, height))
+ assertTrue(sizeClass.containsWindowSizeDp(width, height))
}
@Test
- fun is_at_least_returns_false_when_input_is_smaller() {
+ fun is_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
val width = 200
val height = 100
val sizeClass = WindowSizeClass(width, height)
- assertFalse(sizeClass.isAtLeast(width, height - 1))
- assertFalse(sizeClass.isAtLeast(width - 1, height))
+ assertTrue(sizeClass.containsWindowSizeDp(width, height - 1))
+ assertTrue(sizeClass.containsWindowSizeDp(width - 1, height))
}
}
diff --git a/window/window-demos/demo/build.gradle b/window/window-demos/demo/build.gradle
index fc2d898..6c7d54e 100644
--- a/window/window-demos/demo/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -85,7 +85,7 @@
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:ui:ui-tooling-preview"))
- debugImplementation(project(":compose:ui:ui-tooling"))
+ implementation(project(":compose:ui:ui-tooling"))
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.compose.material3:material3:1.2.1")
implementation("androidx.compose.material:material-icons-extended:1.6.8")
diff --git a/window/window-demos/demo/src/main/res/xml/main_split_config.xml b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
index 3a898ce..c3eb2e4 100644
--- a/window/window-demos/demo/src/main/res/xml/main_split_config.xml
+++ b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
@@ -25,13 +25,26 @@
<SplitPairFilter
window:primaryActivityName="androidx.window.demo.embedding.SplitActivityList"
window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>
+ <DividerAttributes
+ window:embeddingDividerType="draggable"
+ window:embeddingDividerColor="@color/colorAccent"
+ window:embeddingDividerWidthDp="1"
+ window:dragRangeMinRatio="0.2"
+ window:dragRangeMaxRatio="0.8" />
</SplitPairRule>
+
<SplitPlaceholderRule
window:placeholderActivityName="androidx.window.demo.embedding.SplitActivityListPlaceholder"
window:stickyPlaceholder="true"
window:finishPrimaryWithSecondary="adjacent">
<ActivityFilter
window:activityName="androidx.window.demo.embedding.SplitActivityList"/>
+ <DividerAttributes
+ window:embeddingDividerType="draggable"
+ window:embeddingDividerColor="@color/colorAccent"
+ window:embeddingDividerWidthDp="1"
+ window:dragRangeMinRatio="0.2"
+ window:dragRangeMaxRatio="0.8" />
</SplitPlaceholderRule>
<!-- Rules for SplitImeActivityMain -->
diff --git a/window/window-java/build.gradle b/window/window-java/build.gradle
index a64e31c..372d095 100644
--- a/window/window-java/build.gradle
+++ b/window/window-java/build.gradle
@@ -47,9 +47,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
}
androidx {
diff --git a/window/window-rxjava2/build.gradle b/window/window-rxjava2/build.gradle
index 0a84a10..01a72dd 100644
--- a/window/window-rxjava2/build.gradle
+++ b/window/window-rxjava2/build.gradle
@@ -45,9 +45,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/window/window-rxjava3/build.gradle b/window/window-rxjava3/build.gradle
index 27f2064..eece733 100644
--- a/window/window-rxjava3/build.gradle
+++ b/window/window-rxjava3/build.gradle
@@ -45,9 +45,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 9931de5..b4bf8af 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -623,3 +623,11 @@
}
+package androidx.window.layout.adapter {
+
+ public final class WindowSizeClassFactory {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, androidx.window.layout.WindowMetrics windowMetrics);
+ }
+
+}
+
diff --git a/window/window/api/res-current.txt b/window/window/api/res-current.txt
index 185352b..7dced15 100644
--- a/window/window/api/res-current.txt
+++ b/window/window/api/res-current.txt
@@ -3,6 +3,11 @@
attr alwaysExpand
attr animationBackgroundColor
attr clearTop
+attr dragRangeMaxRatio
+attr dragRangeMinRatio
+attr embeddingDividerColor
+attr embeddingDividerType
+attr embeddingDividerWidthDp
attr finishPrimaryWithPlaceholder
attr finishPrimaryWithSecondary
attr finishSecondaryWithPrimary
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 6f55013..2708556 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -739,3 +739,11 @@
}
+package androidx.window.layout.adapter {
+
+ public final class WindowSizeClassFactory {
+ method public static androidx.window.core.layout.WindowSizeClass computeWindowSizeClass(java.util.Set<androidx.window.core.layout.WindowSizeClass>, androidx.window.layout.WindowMetrics windowMetrics);
+ }
+
+}
+
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 004b04a..1bf8a23 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -52,6 +52,7 @@
implementation("androidx.annotation:annotation:1.8.1")
implementation("androidx.collection:collection:1.4.2")
implementation("androidx.core:core:1.8.0")
+ implementation(project(":window:window-core"))
def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
def extensions_version = "androidx.window.extensions:extensions:1.4.0-beta01"
@@ -84,9 +85,9 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
- androidTestImplementation(libs.mockitoKotlin, excludes.bytebuddy)
+ androidTestImplementation(libs.dexmakerMockito)
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
index e6c1d66..e1932bb 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
@@ -152,6 +152,83 @@
}
/**
+ * Verifies that params are set correctly when reading {@link SplitPairRule} from XML with
+ * divider attributes.
+ *
+ * @see R.xml.test_split_config_custom_split_pair_rule_with_divider for customized value.
+ */
+ @Test
+ fun testCustom_SplitPairRule_withDivider() {
+ val rules =
+ RuleController.parseRules(
+ application,
+ R.xml.test_split_config_custom_split_pair_rule_with_divider
+ )
+ assertEquals(4, rules.size)
+ val expectedDividerColor = 0xff112233
+
+ val expectedDividerAttributes1 = DividerAttributes.FixedDividerAttributes.Builder().build()
+
+ val expectedDividerAttributes2 =
+ DividerAttributes.DraggableDividerAttributes.Builder().build()
+
+ val expectedDividerAttributes3 =
+ DividerAttributes.FixedDividerAttributes.Builder()
+ .setWidthDp(1)
+ .setColor(expectedDividerColor.toInt())
+ .build()
+
+ val expectedDividerAttributes4 =
+ DividerAttributes.DraggableDividerAttributes.Builder()
+ .setWidthDp(1)
+ .setColor(expectedDividerColor.toInt())
+ .setDragRange(DividerAttributes.DragRange.SplitRatioDragRange(0.2f, 0.8f))
+ .build()
+
+ rules.forEach {
+ val rule = it as SplitPairRule
+ when (rule.tag) {
+ "rule1" ->
+ assertEquals(
+ expectedDividerAttributes1,
+ rule.defaultSplitAttributes.dividerAttributes
+ )
+ "rule2" ->
+ assertEquals(
+ expectedDividerAttributes2,
+ rule.defaultSplitAttributes.dividerAttributes
+ )
+ "rule3" ->
+ assertEquals(
+ expectedDividerAttributes3,
+ rule.defaultSplitAttributes.dividerAttributes
+ )
+ "rule4" ->
+ assertEquals(
+ expectedDividerAttributes4,
+ rule.defaultSplitAttributes.dividerAttributes
+ )
+ else -> throw IllegalStateException("Unexpected rule tag ${rule.tag}")
+ }
+ }
+ }
+
+ /**
+ * Verifies that a `IllegalArgumentException` thrown for invalid divider attributes.
+ *
+ * @see R.xml.test_split_config_custom_split_pair_rule_with_divider_error for customized value.
+ */
+ @Test
+ fun testCustom_SplitPairRule_withDividerError() {
+ assertThrows(IllegalArgumentException::class.java) {
+ RuleController.parseRules(
+ application,
+ R.xml.test_split_config_custom_split_pair_rule_with_divider_error
+ )
+ }
+ }
+
+ /**
* Verifies that default params are set correctly when reading {@link SplitPlaceholderRule} from
* XML.
*/
diff --git a/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider.xml b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider.xml
new file mode 100644
index 0000000..cf01e39
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources
+ xmlns:window="http://schemas.android.com/apk/res-auto">
+ <SplitPairRule window:tag="rule1">
+ <SplitPairFilter
+ window:primaryActivityName="A"
+ window:secondaryActivityName="B"/>
+ <DividerAttributes window:embeddingDividerType="fixed" />
+ </SplitPairRule>
+
+ <SplitPairRule window:tag="rule2">
+ <SplitPairFilter
+ window:primaryActivityName="A"
+ window:secondaryActivityName="B"/>
+ <DividerAttributes window:embeddingDividerType="draggable" />
+ </SplitPairRule>
+
+ <SplitPairRule window:tag="rule3">
+ <SplitPairFilter
+ window:primaryActivityName="C"
+ window:secondaryActivityName="D"/>
+ <DividerAttributes
+ window:embeddingDividerType="fixed"
+ window:embeddingDividerColor="#112233"
+ window:embeddingDividerWidthDp="1" />
+ </SplitPairRule>
+
+ <SplitPairRule window:tag="rule4">
+ <SplitPairFilter
+ window:primaryActivityName="C"
+ window:secondaryActivityName="D"/>
+ <DividerAttributes
+ window:embeddingDividerType="draggable"
+ window:embeddingDividerColor="#112233"
+ window:embeddingDividerWidthDp="1"
+ window:dragRangeMinRatio="0.2"
+ window:dragRangeMaxRatio="0.8" />
+ </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider_error.xml b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider_error.xml
new file mode 100644
index 0000000..468cd2a
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule_with_divider_error.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources
+ xmlns:window="http://schemas.android.com/apk/res-auto">
+ <SplitPairRule window:tag="rule1">
+ <SplitPairFilter
+ window:primaryActivityName="A"
+ window:secondaryActivityName="B"/>
+ <!-- Fixed divider does not allow the dragging attributes -->
+ <DividerAttributes
+ window:embeddingDividerType="fixed"
+ window:dragRangeMinRatio="0.2"
+ window:dragRangeMaxRatio="0.8" />
+ </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
index 48cb686..e7df30c 100644
--- a/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
@@ -35,7 +35,7 @@
abstract class DividerAttributes
private constructor(
@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) val widthDp: Int = WIDTH_SYSTEM_DEFAULT,
- @ColorInt val color: Int = Color.BLACK,
+ @ColorInt val color: Int = COLOR_SYSTEM_DEFAULT,
) {
override fun toString(): String =
DividerAttributes::class.java.simpleName + "{" + "width=$widthDp, " + "color=$color" + "}"
@@ -52,7 +52,7 @@
@RequiresWindowSdkExtension(6)
private constructor(
@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
- @ColorInt color: Int = Color.BLACK
+ @ColorInt color: Int = COLOR_SYSTEM_DEFAULT
) : DividerAttributes(widthDp, color) {
override fun equals(other: Any?): Boolean {
@@ -73,7 +73,7 @@
@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
private var widthDp = WIDTH_SYSTEM_DEFAULT
- @ColorInt private var color = Color.BLACK
+ @ColorInt private var color = COLOR_SYSTEM_DEFAULT
/**
* The [FixedDividerAttributes] builder constructor initialized by an existing
@@ -139,7 +139,7 @@
@RequiresWindowSdkExtension(6)
private constructor(
@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
- @ColorInt color: Int = Color.BLACK,
+ @ColorInt color: Int = COLOR_SYSTEM_DEFAULT,
val dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT,
) : DividerAttributes(widthDp, color) {
@@ -169,7 +169,7 @@
@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
private var widthDp = WIDTH_SYSTEM_DEFAULT
- @ColorInt private var color = Color.BLACK
+ @ColorInt private var color = COLOR_SYSTEM_DEFAULT
private var dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT
@@ -317,6 +317,70 @@
override fun toString(): String = "NO_DIVIDER"
}
+ /** Specifies a fixed divider. Used by the XML rule parser and must match attrs.xml. */
+ internal const val TYPE_VALUE_FIXED: Int = 0
+
+ /** Specifies a draggable divider. Used by the XML rule parser and must match attrs.xml. */
+ internal const val TYPE_VALUE_DRAGGABLE: Int = 1
+
+ /** Indicates that the drag range value is unspecified. Used by the XML rule parser. */
+ internal const val DRAG_RANGE_VALUE_UNSPECIFIED: Float = -1.0f
+
+ /** The default color of a divider. */
+ internal const val COLOR_SYSTEM_DEFAULT: Int = Color.BLACK
+
+ /** Creates a [DividerAttributes] from values. Used by the XML rule parser. */
+ internal fun createDividerAttributes(
+ type: Int,
+ widthDp: Int,
+ color: Int,
+ dragRangeMinRatio: Float,
+ dragRangeMaxRatio: Float,
+ ): DividerAttributes {
+ return when (type) {
+ TYPE_VALUE_FIXED ->
+ FixedDividerAttributes.Builder().setWidthDp(widthDp).setColor(color).build()
+ TYPE_VALUE_DRAGGABLE -> {
+ val builder =
+ DraggableDividerAttributes.Builder().setWidthDp(widthDp).setColor(color)
+ if (
+ dragRangeMinRatio == DRAG_RANGE_VALUE_UNSPECIFIED ||
+ dragRangeMaxRatio == DRAG_RANGE_VALUE_UNSPECIFIED
+ ) {
+ builder.setDragRange(DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
+ } else {
+ // Validation happens in SplitRatioDragRange constructor
+ builder.setDragRange(
+ DragRange.SplitRatioDragRange(dragRangeMinRatio, dragRangeMaxRatio)
+ )
+ }
+ builder.build()
+ }
+ else -> throw IllegalArgumentException("Got unknown divider type $type!")
+ }
+ }
+
+ /** Validates divider XML attributes. */
+ internal fun validateXmlDividerAttributes(
+ type: Int,
+ hasDragRangeMinRatio: Boolean,
+ hasDragRangeMaxRatio: Boolean,
+ ) {
+ if (type == TYPE_VALUE_DRAGGABLE) {
+ return
+ }
+ if (hasDragRangeMinRatio) {
+ throw IllegalArgumentException(
+ "Fixed divider does not allow attribute dragRangeMinRatio!"
+ )
+ }
+ if (hasDragRangeMaxRatio) {
+ throw IllegalArgumentException(
+ "Fixed divider does not allow attribute dragRangeMaxRatio!"
+ )
+ }
+ }
+
private fun validateWidth(widthDp: Int) = run {
require(widthDp == WIDTH_SYSTEM_DEFAULT || widthDp >= 0) {
"widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT. Got: $widthDp"
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
index 666196a..5cf4b521 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -23,6 +23,11 @@
import android.content.res.XmlResourceParser
import androidx.annotation.XmlRes
import androidx.window.R
+import androidx.window.embedding.DividerAttributes.Companion.COLOR_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.Companion.DRAG_RANGE_VALUE_UNSPECIFIED
+import androidx.window.embedding.DividerAttributes.Companion.TYPE_VALUE_FIXED
+import androidx.window.embedding.DividerAttributes.Companion.WIDTH_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.Companion.validateXmlDividerAttributes
import androidx.window.embedding.EmbeddingAspectRatio.Companion.buildAspectRatioFromValue
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
@@ -109,6 +114,35 @@
rules.addRuleWithDuplicatedTagCheck(lastSplitPlaceholderRule)
}
}
+ "DividerAttributes" -> {
+ if (lastSplitPairRule == null && lastSplitPlaceholderRule == null) {
+ throw IllegalArgumentException("Found orphaned DividerAttributes")
+ }
+ val dividerAttributes = parseDividerAttributes(context, parser)
+ if (lastSplitPairRule != null) {
+ rules.remove(lastSplitPairRule)
+ val splitAttributes =
+ SplitAttributes.Builder(lastSplitPairRule.defaultSplitAttributes)
+ .setDividerAttributes(dividerAttributes)
+ .build()
+ lastSplitPairRule =
+ SplitPairRule.Builder(lastSplitPairRule)
+ .setDefaultSplitAttributes(splitAttributes)
+ .build()
+ rules.addRuleWithDuplicatedTagCheck(lastSplitPairRule)
+ } else if (lastSplitPlaceholderRule != null) {
+ rules.remove(lastSplitPlaceholderRule)
+ val splitAttributes =
+ SplitAttributes.Builder(lastSplitPlaceholderRule.defaultSplitAttributes)
+ .setDividerAttributes(dividerAttributes)
+ .build()
+ lastSplitPlaceholderRule =
+ SplitPlaceholderRule.Builder(lastSplitPlaceholderRule)
+ .setDefaultSplitAttributes(splitAttributes)
+ .build()
+ rules.addRuleWithDuplicatedTagCheck(lastSplitPlaceholderRule)
+ }
+ }
}
type = parser.next()
}
@@ -336,6 +370,42 @@
return ActivityFilter(buildClassName(packageName, activityName), activityIntentAction)
}
+ private fun parseDividerAttributes(
+ context: Context,
+ parser: XmlResourceParser
+ ): DividerAttributes {
+ context.theme.obtainStyledAttributes(parser, R.styleable.DividerAttributes, 0, 0).apply {
+ val type = getInt(R.styleable.DividerAttributes_embeddingDividerType, TYPE_VALUE_FIXED)
+ validateXmlDividerAttributes(
+ type,
+ hasValue(R.styleable.DividerAttributes_dragRangeMinRatio),
+ hasValue(R.styleable.DividerAttributes_dragRangeMaxRatio),
+ )
+
+ val widthDp =
+ getInt(R.styleable.DividerAttributes_embeddingDividerWidthDp, WIDTH_SYSTEM_DEFAULT)
+ val color =
+ getColor(R.styleable.DividerAttributes_embeddingDividerColor, COLOR_SYSTEM_DEFAULT)
+ val dragRangeMinRatio =
+ getFloat(
+ R.styleable.DividerAttributes_dragRangeMinRatio,
+ DRAG_RANGE_VALUE_UNSPECIFIED
+ )
+ val dragRangeMaxRatio =
+ getFloat(
+ R.styleable.DividerAttributes_dragRangeMaxRatio,
+ DRAG_RANGE_VALUE_UNSPECIFIED
+ )
+ return@parseDividerAttributes DividerAttributes.createDividerAttributes(
+ type,
+ widthDp,
+ color,
+ dragRangeMinRatio,
+ dragRangeMaxRatio,
+ )
+ }
+ }
+
private fun buildClassName(pkg: String, clsSeq: CharSequence?): ComponentName {
if (clsSeq.isNullOrEmpty()) {
throw IllegalArgumentException("Activity name must not be null")
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index 565f729..a615d9e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -378,12 +378,20 @@
* - The default animation background color is to use the current theme window background color.
* - The default divider attributes is not to use divider.
*/
- class Builder {
+ class Builder() {
private var splitType = SPLIT_TYPE_EQUAL
private var layoutDirection = LOCALE
private var animationBackground = EmbeddingAnimationBackground.DEFAULT
private var dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER
+ /** Creates a Builder with values initialized from the original [SplitAttributes] */
+ internal constructor(original: SplitAttributes) : this() {
+ this.setSplitType(original.splitType)
+ .setLayoutDirection(original.layoutDirection)
+ .setAnimationBackground(animationBackground)
+ .setDividerAttributes(original.dividerAttributes)
+ }
+
/**
* Sets the split type attribute.
*
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 fa646a7..cea9286 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -98,6 +98,20 @@
private var clearTop = false
private var defaultSplitAttributes = SplitAttributes.Builder().build()
+ /** Creates a Builder with values initialized from the original [SplitPairRule] */
+ internal constructor(original: SplitPairRule) : this(original.filters) {
+ this.setTag(original.tag)
+ .setMinWidthDp(original.minWidthDp)
+ .setMinHeightDp(original.minHeightDp)
+ .setMinSmallestWidthDp(original.minSmallestWidthDp)
+ .setMaxAspectRatioInPortrait(original.maxAspectRatioInPortrait)
+ .setMaxAspectRatioInLandscape(original.maxAspectRatioInLandscape)
+ .setFinishPrimaryWithSecondary(original.finishPrimaryWithSecondary)
+ .setFinishSecondaryWithPrimary(original.finishSecondaryWithPrimary)
+ .setClearTop(original.clearTop)
+ .setDefaultSplitAttributes(original.defaultSplitAttributes)
+ }
+
/**
* Sets the smallest value of width of the parent window when the split should be used, in
* DP. When the window size is smaller than requested here, activities in the secondary
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 b7396b3..30c5661 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -120,6 +120,21 @@
private var isSticky = false
private var defaultSplitAttributes = SplitAttributes.Builder().build()
+ /** Creates a Builder with values initialized from the original [SplitPlaceholderRule] */
+ internal constructor(
+ original: SplitPlaceholderRule
+ ) : this(original.filters, original.placeholderIntent) {
+ this.setTag(original.tag)
+ .setMinWidthDp(original.minWidthDp)
+ .setMinHeightDp(original.minHeightDp)
+ .setMinSmallestWidthDp(original.minSmallestWidthDp)
+ .setMaxAspectRatioInPortrait(original.maxAspectRatioInPortrait)
+ .setMaxAspectRatioInLandscape(original.maxAspectRatioInLandscape)
+ .setFinishPrimaryWithPlaceholder(original.finishPrimaryWithPlaceholder)
+ .setSticky(original.isSticky)
+ .setDefaultSplitAttributes(original.defaultSplitAttributes)
+ }
+
/**
* Sets the smallest value of width of the parent window when the split should be used, in
* DP. When the window size is smaller than requested here, activities in the secondary
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
index 6707353..c1bf62a 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
@@ -36,7 +36,6 @@
internal constructor(
private val _bounds: Bounds,
private val _windowInsetsCompat: WindowInsetsCompat,
-
/**
* Returns the logical density of the display this window is in.
*
@@ -65,8 +64,14 @@
val bounds: Rect
get() = _bounds.toRect()
- override fun toString(): String {
- return "WindowMetrics( bounds=$_bounds, windowInsetsCompat=$_windowInsetsCompat)"
+ /**
+ * Returns the [WindowInsetsCompat] of the area associated with this window or visual context.
+ */
+ @ExperimentalWindowApi
+ @RequiresApi(VERSION_CODES.R)
+ // TODO (b/238354685): Match interface style of Bounds after the API is fully backported
+ fun getWindowInsets(): WindowInsetsCompat {
+ return _windowInsetsCompat
}
override fun equals(other: Any?): Boolean {
@@ -85,16 +90,11 @@
override fun hashCode(): Int {
var result = _bounds.hashCode()
result = 31 * result + _windowInsetsCompat.hashCode()
+ result = 31 * result + density.hashCode()
return result
}
- /**
- * Returns the [WindowInsetsCompat] of the area associated with this window or visual context.
- */
- @ExperimentalWindowApi
- @RequiresApi(VERSION_CODES.R)
- // TODO (b/238354685): Match interface style of Bounds after the API is fully backported
- fun getWindowInsets(): WindowInsetsCompat {
- return _windowInsetsCompat
+ override fun toString(): String {
+ return "WindowMetrics(_bounds=$_bounds, _windowInsetsCompat=$_windowInsetsCompat, density=$density)"
}
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/WindowSizeClassFactory.kt b/window/window/src/main/java/androidx/window/layout/adapter/WindowSizeClassFactory.kt
new file mode 100644
index 0000000..686173e
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/adapter/WindowSizeClassFactory.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("WindowSizeClassFactory")
+
+package androidx.window.layout.adapter
+
+import androidx.window.core.layout.WindowSizeClass
+import androidx.window.core.layout.computeWindowSizeClass
+import androidx.window.layout.WindowMetrics
+
+/** A convenience function for computing the [WindowSizeClass] from the [WindowMetrics] */
+fun Set<WindowSizeClass>.computeWindowSizeClass(windowMetrics: WindowMetrics): WindowSizeClass {
+ val density = windowMetrics.density
+ val widthDp = (windowMetrics.bounds.width() * 160) / density
+ val heightDp = (windowMetrics.bounds.height() * 160) / density
+ return computeWindowSizeClass(widthDp, heightDp)
+}
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index a2880ca..7f6b59e 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -232,4 +232,38 @@
the given action. -->
<attr name="activityAction" format="string" />
</declare-styleable>
+
+ <!-- Attributes that are read when parsing an <DividerAttributes> tag, which defines the divider
+ attributes for `SplitPairRule` and `SplitPlaceholderRule`. The <DividerAttributes> tag must
+ be a child of the <SplitPairRule> or <SplitPlaceholderRule> tag. If not set, no divider
+ is shown. -->
+ <declare-styleable name="DividerAttributes">
+ <!-- The divider type. The default type is `fixed`. -->
+ <attr name="embeddingDividerType" format="enum">
+ <!-- Fixed divider type. -->
+ <enum name="fixed" value="0" />
+ <!-- Draggable divider type. -->
+ <enum name="draggable" value="1" />
+ </attr>
+ <!-- The divider width in dp. If unspecified, the system default value will be used. The
+ width may be set to zero for a draggable divider, so that the drag handle is displayed
+ without the divider line. -->
+ <attr name="embeddingDividerWidthDp" format="integer" />
+ <!-- The divider color. The color must be opaque. The default color is black. -->
+ <attr name="embeddingDividerColor" format="color|reference" />
+ <!-- The minimum split ratio of the primary container that the user is allowed to drag to.
+ The value must be in the range (0.0, 1.0). System default value will be used if
+ dragRangeMinRatio or dragRangeMaxRatio is unspecified. dragRangeMaxRatio must be
+ greater than or equal to dragRangeMinRatio.
+ This attribute is only allowed for a draggable divider and an exception is thrown
+ otherwise. See DividerAttributes.DragRange for more details. -->
+ <attr name="dragRangeMinRatio" format="float" />
+ <!-- The maximum split ratio of the primary container that the user is allowed to drag to.
+ The value must be in the range (0.0, 1.0). System default value will be used if
+ dragRangeMinRatio or dragRangeMaxRatio is unspecified. dragRangeMaxRatio must be
+ greater than or equal to dragRangeMinRatio.
+ This attribute is only allowed for a draggable divider and an exception is thrown
+ otherwise. See DividerAttributes.DragRange for more details. -->
+ <attr name="dragRangeMaxRatio" format="float" />
+ </declare-styleable>
</resources>
\ No newline at end of file
diff --git a/window/window/src/main/res/values/public.xml b/window/window/src/main/res/values/public.xml
index 70d9306..d05d4dd 100644
--- a/window/window/src/main/res/values/public.xml
+++ b/window/window/src/main/res/values/public.xml
@@ -38,4 +38,10 @@
<public name="secondaryActivityAction" type="attr" />
<public name="activityName" type="attr" />
<public name="activityAction" type="attr" />
+
+ <public name="embeddingDividerType" type="attr" />
+ <public name="embeddingDividerWidthDp" type="attr" />
+ <public name="embeddingDividerColor" type="attr" />
+ <public name="dragRangeMinRatio" type="attr" />
+ <public name="dragRangeMaxRatio" type="attr" />
</resources>
\ No newline at end of file
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index e9e5895..65f9532 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -44,9 +44,9 @@
}
dependencies {
- annotationProcessor(projectOrArtifact(":room:room-compiler"))
- implementation(projectOrArtifact(":room:room-runtime"))
- implementation(projectOrArtifact(":room:room-ktx"))
+ annotationProcessor(project(":room:room-compiler"))
+ implementation(project(":room:room-runtime"))
+ implementation(project(":room:room-ktx"))
implementation(libs.constraintLayout)
implementation("androidx.core:core:1.12.0")
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index 3ac1da2..c5e9353 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -34,8 +34,8 @@
androidTestImplementation(project(":work:work-runtime-ktx"))
androidTestImplementation(project(":work:work-multiprocess"))
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
- androidTestImplementation(projectOrArtifact(":room:room-runtime"))
- androidTestImplementation(projectOrArtifact(":room:room-ktx"))
+ androidTestImplementation(project(":room:room-runtime"))
+ androidTestImplementation(project(":room:room-ktx"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index b316707..e06ae25 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -49,8 +49,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
testImplementation(libs.junit)
}
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 692e72f..364b730 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -48,8 +48,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
}
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 5cd8014..4bdf4b9 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -432,6 +432,7 @@
field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9
field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4
field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf
+ field public static final int STOP_REASON_FOREGROUND_SERVICE_TIMEOUT = -128; // 0xffffff80
field public static final int STOP_REASON_NOT_STOPPED = -256; // 0xffffff00
field public static final int STOP_REASON_PREEMPT = 2; // 0x2
field public static final int STOP_REASON_QUOTA = 10; // 0xa
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 5cd8014..4bdf4b9 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -432,6 +432,7 @@
field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9
field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4
field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf
+ field public static final int STOP_REASON_FOREGROUND_SERVICE_TIMEOUT = -128; // 0xffffff80
field public static final int STOP_REASON_NOT_STOPPED = -256; // 0xffffff00
field public static final int STOP_REASON_PREEMPT = 2; // 0x2
field public static final int STOP_REASON_QUOTA = 10; // 0xa
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 8a5ec39..94aa053 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -80,12 +80,12 @@
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation("androidx.room:room-testing:2.6.1")
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(project(":internal-testutils-runtime"))
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
index 05f8602..4f3c8d1 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/foreground/SystemForegroundDispatcherTest.kt
@@ -57,6 +57,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.reset
@@ -382,7 +383,7 @@
assertThat(fakeChargingTracker.isTracking, `is`(true))
fakeChargingTracker.state = false
verify(workManager, times(1))
- .stopForegroundWork(eq(WorkGenerationalId(request.workSpec.id, 0)))
+ .stopForegroundWork(eq(WorkGenerationalId(request.workSpec.id, 0)), anyInt())
}
@Test
@@ -455,7 +456,33 @@
@Test
fun testTimeoutForeground() {
+ val request =
+ OneTimeWorkRequest.Builder(NeverResolvedWorker::class.java)
+ .setConstraints(Constraints.Builder().setRequiresCharging(true).build())
+ .build()
+ workDatabase.workSpecDao().insertWorkSpec(request.workSpec)
+ processor.startWork(StartStopToken(WorkGenerationalId(request.stringId, 0)))
+ val notificationId = 1
+ val notification = mock(Notification::class.java)
+ val metadata =
+ ForegroundInfo(notificationId, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)
+ val intent =
+ createStartForegroundIntent(
+ context,
+ WorkGenerationalId(request.workSpec.id, 0),
+ metadata
+ )
+ dispatcher.onStartCommand(intent)
+ assertThat(fakeChargingTracker.isTracking, `is`(true))
+
dispatcher.onTimeout(0, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)
+ verify(workManager, times(1))
+ .stopForegroundWork(
+ WorkGenerationalId(request.workSpec.id, 0),
+ WorkInfo.STOP_REASON_FOREGROUND_SERVICE_TIMEOUT
+ )
verify(dispatcherCallback, times(1)).stop()
+
+ assertThat(processor.hasWork(), `is`(false))
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
index 0a1d834..ce574b5 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
+++ b/work/work-runtime/src/main/java/androidx/work/WorkInfo.kt
@@ -20,7 +20,6 @@
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import androidx.annotation.RequiresApi
-import androidx.work.WorkInfo.Companion.STOP_REASON_NOT_STOPPED
import androidx.work.WorkInfo.State
import java.util.UUID
@@ -231,6 +230,15 @@
}
companion object {
+
+ /**
+ * The foreground worker used up its maximum execution time and timed out.
+ *
+ * Foreground workers have a maximum execution time limit depending on the [ForegroundInfo]
+ * type. See the notes on [android.content.pm.ServiceInfo] types.
+ */
+ const val STOP_REASON_FOREGROUND_SERVICE_TIMEOUT = -128
+
/**
* Additional stop reason, that is returned from [WorkInfo.stopReason] in cases when a
* worker in question wasn't stopped. E.g. when a worker was just enqueued, but didn't run
@@ -337,10 +345,20 @@
}
}
+/**
+ * Stops reason integers are divided in ranges since some corresponds to platform equivalents, while
+ * other are WorkManager specific.
+ * * `-512` - Special STOP_REASON_UNKNOWN
+ * * `-256` - Special STOP_REASON_NOT_STOPPED
+ * * `[-255, -128]` - Reserved for WM specific reasons (i.e. not reflected by JobScheduler).
+ * * `[-127, -1]` - Unused on purpose.
+ * * `[0, MAX_VALUE]` - Reserved for JobScheduler mirror reasons (i.e. JobParameters.STOP_REASON_X).
+ */
@Retention(AnnotationRetention.SOURCE)
@IntDef(
- STOP_REASON_NOT_STOPPED,
WorkInfo.STOP_REASON_UNKNOWN,
+ WorkInfo.STOP_REASON_NOT_STOPPED,
+ WorkInfo.STOP_REASON_FOREGROUND_SERVICE_TIMEOUT,
WorkInfo.STOP_REASON_CANCELLED_BY_APP,
WorkInfo.STOP_REASON_PREEMPT,
WorkInfo.STOP_REASON_TIMEOUT,
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 2e8c1b7..e8153ad 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -50,6 +50,7 @@
import androidx.work.OneTimeWorkRequest;
import androidx.work.Operation;
import androidx.work.PeriodicWorkRequest;
+import androidx.work.StopReason;
import androidx.work.TracerKt;
import androidx.work.WorkContinuation;
import androidx.work.WorkInfo;
@@ -621,9 +622,9 @@
* foreground service.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void stopForegroundWork(@NonNull WorkGenerationalId id) {
+ public void stopForegroundWork(@NonNull WorkGenerationalId id, @StopReason int reason) {
mWorkTaskExecutor.executeOnTaskThread(new StopWorkRunnable(mProcessor,
- new StartStopToken(id), true));
+ new StartStopToken(id), true, reason));
}
/**
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index 5c3bca6..a327b14 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -34,6 +34,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.work.ForegroundInfo;
import androidx.work.Logger;
+import androidx.work.WorkInfo;
import androidx.work.impl.ExecutionListener;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.ConstraintsState;
@@ -241,6 +242,14 @@
@MainThread
void onTimeout(int startId, int fgsType) {
Logger.get().info(TAG, "Foreground service timed out, FGS type: " + fgsType);
+ for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry : mForegroundInfoById.entrySet()) {
+ ForegroundInfo info = entry.getValue();
+ if (info.getForegroundServiceType() == fgsType) {
+ WorkGenerationalId id = entry.getKey();
+ mWorkManagerImpl.stopForegroundWork(id,
+ WorkInfo.STOP_REASON_FOREGROUND_SERVICE_TIMEOUT);
+ }
+ }
if (mCallback != null) {
mCallback.stop();
}
@@ -347,7 +356,9 @@
if (state instanceof ConstraintsState.ConstraintsNotMet) {
String workSpecId = workSpec.id;
Logger.get().debug(TAG, "Constraints unmet for WorkSpec " + workSpecId);
- mWorkManagerImpl.stopForegroundWork(generationalId(workSpec));
+ mWorkManagerImpl.stopForegroundWork(
+ generationalId(workSpec),
+ ((ConstraintsState.ConstraintsNotMet) state).getReason());
}
}
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index a9caf52..a972b75 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -41,8 +41,8 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
- androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(libs.mockitoCore)
+ androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.truth)
testImplementation(libs.truth)