Merge "Revert "passing common-source-path to metalava"" into androidx-main
diff --git a/annotation/annotation/api/1.8.0-beta01.txt b/annotation/annotation/api/1.8.0-beta01.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/1.8.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.8.0-beta01.txt b/annotation/annotation/api/restricted_1.8.0-beta01.txt
new file mode 100644
index 0000000..dababc5
--- /dev/null
+++ b/annotation/annotation/api/restricted_1.8.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/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
index ecfdba3..f386a35 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/AtraceTag.kt
@@ -54,6 +54,7 @@
return rooted || api >= 24
}
},
+ Power("power"),
Resources("res"),
Scheduling("sched"),
Synchronization("sync") {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 533583b..561d71c 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -151,6 +151,7 @@
AtraceTag.Idle,
AtraceTag.Input,
AtraceTag.MemReclaim,
+ AtraceTag.Power,
AtraceTag.Resources,
AtraceTag.Scheduling,
AtraceTag.Synchronization,
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index a0095f8..63a175a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -51,11 +51,9 @@
var perfettoPid: Int? = null
/**
- * For now, we use --background-wait only when unbundled.
- *
- * Eventually, we should also use it when using bundled platform version that support it (T+?)
+ * --background-wait requires unbundled or API 33 bundled version of perfetto
*/
- private val useBackgroundWait = unbundled
+ private val useBackgroundWait = unbundled || Build.VERSION.SDK_INT >= 33
private fun perfettoStartupException(label: String, cause: Exception?): IllegalStateException {
return IllegalStateException(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index dcdd156..dbbc781 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -17,12 +17,10 @@
package androidx.build
import androidx.build.dependencies.KOTLIN_NATIVE_VERSION
+import com.android.build.api.dsl.CommonExtension
import com.android.build.api.variant.AndroidComponentsExtension
-import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
-import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.LibraryPlugin
-import com.android.build.gradle.TestedExtension
import java.io.File
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -47,19 +45,12 @@
project.extensions.create<AndroidXComposeExtension>("androidxCompose", project)
project.plugins.all { plugin ->
when (plugin) {
- is LibraryPlugin -> {
- val library =
- project.extensions.findByType(LibraryExtension::class.java)
+ is AppPlugin, is LibraryPlugin -> {
+ val commonExtension =
+ project.extensions.findByType(CommonExtension::class.java)
?: throw Exception("Failed to find Android extension")
-
- project.configureAndroidCommonOptions(library)
- }
- is AppPlugin -> {
- val app =
- project.extensions.findByType(AppExtension::class.java)
- ?: throw Exception("Failed to find Android extension")
-
- project.configureAndroidCommonOptions(app)
+ commonExtension.defaultConfig.minSdk = 21
+ project.configureAndroidCommonOptions()
}
is KotlinBasePluginWrapper -> {
configureComposeCompilerPlugin(project, extension)
@@ -73,9 +64,7 @@
}
companion object {
- private fun Project.configureAndroidCommonOptions(testedExtension: TestedExtension) {
- testedExtension.defaultConfig.minSdk = 21
-
+ private fun Project.configureAndroidCommonOptions() {
extensions.findByType(AndroidComponentsExtension::class.java)!!.finalizeDsl {
val isPublished = androidXExtension.shouldPublish()
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 19e56cd..20ea0f3 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -23,12 +23,11 @@
-Pandroidx.enableComposeCompilerMetrics=true \
-Pandroidx.enableComposeCompilerReports=true \
-Pandroidx.constraints=true \
- --no-daemon \
- --profile "$@"; then
+ --no-daemon "$@"; then
EXIT_VALUE=1
fi
- # Parse performance profile reports (generated with the --profile option above) and re-export
+ # Parse performance profile reports (generated with the --profile option) and re-export
# the metrics in an easily machine-readable format for tracking
impl/parse_profile_data.sh
fi
diff --git a/busytown/androidx_incremental.sh b/busytown/androidx_incremental.sh
index 775d422..949b04b 100755
--- a/busytown/androidx_incremental.sh
+++ b/busytown/androidx_incremental.sh
@@ -64,7 +64,6 @@
else
# Run Gradle
if impl/build.sh $DIAGNOSE_ARG buildOnServer checkExternalLicenses listTaskOutputs exportSboms \
- --profile \
"$@"; then
echo build succeeded
EXIT_VALUE=0
@@ -73,7 +72,7 @@
EXIT_VALUE=1
fi
- # Parse performance profile reports (generated with the --profile option above) and re-export the metrics in an easily machine-readable format for tracking
+ # Parse performance profile reports (generated with the --profile option) and re-export the metrics in an easily machine-readable format for tracking
impl/parse_profile_data.sh
fi
diff --git a/busytown/impl/build-studio-and-androidx.sh b/busytown/impl/build-studio-and-androidx.sh
index d37dd88..1511b7c 100755
--- a/busytown/impl/build-studio-and-androidx.sh
+++ b/busytown/impl/build-studio-and-androidx.sh
@@ -99,5 +99,5 @@
export USE_ANDROIDX_REMOTE_BUILD_CACHE=gcp
fi
-$SCRIPTS_DIR/impl/build.sh $androidxArguments --profile --dependency-verification=off -Pandroidx.validateNoUnrecognizedMessages=false
+$SCRIPTS_DIR/impl/build.sh $androidxArguments --dependency-verification=off -Pandroidx.validateNoUnrecognizedMessages=false
echo "Completing $0 at $(date)"
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index e1ffc07..050e210 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -56,11 +56,9 @@
if eval "$*"; then
return 0
else
- echo "Gradle command failed:" >&2
# Echo the Gradle command formatted for ease of reading.
- # Put each argument on its own line because some arguments may be long.
- # Also put "\" at the end of non-final lines so the command can be copy-pasted
- echo "$*" | sed 's/ / \\\n/g' | sed 's/^/ /' >&2
+ echo "Gradle command failed:" >&2
+ echo " $*" >&2
return 1
fi
}
diff --git a/camera/camera-camera2/api/1.4.0-beta01.txt b/camera/camera-camera2/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..1f2bf8d
--- /dev/null
+++ b/camera/camera-camera2/api/1.4.0-beta01.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.camera.camera2 {
+
+ @RequiresApi(21) public final class Camera2Config {
+ method public static androidx.camera.core.CameraXConfig defaultConfig();
+ }
+
+}
+
+package androidx.camera.camera2.interop {
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
+ method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
+ method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraInfo {
+ method public static androidx.camera.camera2.interop.Camera2CameraInfo from(androidx.camera.core.CameraInfo);
+ method public <T> T? getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key<T!>);
+ method public String getCameraId();
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2Interop {
+ }
+
+ @RequiresApi(21) public static final class Camera2Interop.Extender<T> {
+ ctor public Camera2Interop.Extender(androidx.camera.core.ExtendableBuilder<T!>);
+ method public <ValueT> androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setDeviceStateCallback(android.hardware.camera2.CameraDevice.StateCallback);
+ method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+ method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
+ method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ }
+
+ @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
+ ctor public CaptureRequestOptions.Builder();
+ method public androidx.camera.camera2.interop.CaptureRequestOptions build();
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+ }
+
+}
+
diff --git a/camera/camera-camera2/api/res-1.4.0-beta01.txt b/camera/camera-camera2/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-camera2/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-camera2/api/restricted_1.4.0-beta01.txt b/camera/camera-camera2/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..1f2bf8d
--- /dev/null
+++ b/camera/camera-camera2/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,54 @@
+// Signature format: 4.0
+package androidx.camera.camera2 {
+
+ @RequiresApi(21) public final class Camera2Config {
+ method public static androidx.camera.core.CameraXConfig defaultConfig();
+ }
+
+}
+
+package androidx.camera.camera2.interop {
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> addCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> clearCaptureRequestOptions();
+ method public static androidx.camera.camera2.interop.Camera2CameraControl from(androidx.camera.core.CameraControl);
+ method public androidx.camera.camera2.interop.CaptureRequestOptions getCaptureRequestOptions();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setCaptureRequestOptions(androidx.camera.camera2.interop.CaptureRequestOptions);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2CameraInfo {
+ method public static androidx.camera.camera2.interop.Camera2CameraInfo from(androidx.camera.core.CameraInfo);
+ method public <T> T? getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key<T!>);
+ method public String getCameraId();
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public final class Camera2Interop {
+ }
+
+ @RequiresApi(21) public static final class Camera2Interop.Extender<T> {
+ ctor public Camera2Interop.Extender(androidx.camera.core.ExtendableBuilder<T!>);
+ method public <ValueT> androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setDeviceStateCallback(android.hardware.camera2.CameraDevice.StateCallback);
+ method @RequiresApi(28) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setPhysicalCameraId(String);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionCaptureCallback(android.hardware.camera2.CameraCaptureSession.CaptureCallback);
+ method public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setSessionStateCallback(android.hardware.camera2.CameraCaptureSession.StateCallback);
+ method @RequiresApi(33) public androidx.camera.camera2.interop.Camera2Interop.Extender<T!> setStreamUseCase(long);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.camera2.interop.ExperimentalCamera2Interop public class CaptureRequestOptions {
+ method public <ValueT> ValueT? getCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ }
+
+ @RequiresApi(21) public static final class CaptureRequestOptions.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.camera2.interop.CaptureRequestOptions!> {
+ ctor public CaptureRequestOptions.Builder();
+ method public androidx.camera.camera2.interop.CaptureRequestOptions build();
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder clearCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>);
+ method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+ }
+
+}
+
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 6d83e9f..d86a586 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -19,6 +19,7 @@
import static android.hardware.camera2.CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES;
import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION;
+import static android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA;
import static android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING;
import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME;
import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN;
@@ -56,6 +57,7 @@
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.Logger;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -125,6 +127,9 @@
@NonNull
private final CameraManagerCompat mCameraManager;
+ @Nullable
+ private Set<PhysicalCameraInfo> mPhysicalCameraInfos;
+
/**
* Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
* called, camera control related API (torch/exposure/zoom) will return default values.
@@ -405,6 +410,12 @@
REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING);
}
+ @Override
+ public boolean isLogicalMultiCameraSupported() {
+ return isCapabilitySupported(mCameraCharacteristicsCompat,
+ REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA);
+ }
+
/** {@inheritDoc} */
@NonNull
@Override
@@ -627,6 +638,32 @@
return map;
}
+ @NonNull
+ @Override
+ public Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ if (mPhysicalCameraInfos == null) {
+ mPhysicalCameraInfos = new HashSet<>();
+ for (String physicalCameraId : mCameraCharacteristicsCompat.getPhysicalCameraIds()) {
+ CameraCharacteristicsCompat characteristicsCompat;
+ try {
+ characteristicsCompat =
+ mCameraManager.getCameraCharacteristicsCompat(physicalCameraId);
+ } catch (CameraAccessExceptionCompat e) {
+ Logger.e(TAG,
+ "Failed to get CameraCharacteristics for cameraId " + physicalCameraId,
+ e);
+ return Collections.emptySet();
+ }
+
+ PhysicalCameraInfo physicalCameraInfo = Camera2PhysicalCameraInfo.of(
+ physicalCameraId, characteristicsCompat);
+ mPhysicalCameraInfos.add(physicalCameraInfo);
+ }
+ }
+
+ return mPhysicalCameraInfos;
+ }
+
/**
* A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
* is set, initial value will be used.
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java
new file mode 100644
index 0000000..2bf301e
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.PhysicalCameraInfo;
+import androidx.core.util.Preconditions;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Camera2 implementation of {@link PhysicalCameraInfo} which wraps physical camera id and
+ * camera characteristics.
+ */
+@RequiresApi(21)
+@AutoValue
+abstract class Camera2PhysicalCameraInfo implements PhysicalCameraInfo {
+
+ @NonNull
+ @Override
+ public abstract String getPhysicalCameraId();
+
+ @NonNull
+ public abstract CameraCharacteristicsCompat getCameraCharacteristicsCompat();
+
+ @RequiresApi(28)
+ @NonNull
+ @Override
+ public Integer getLensPoseReference() {
+ Integer lensPoseRef =
+ getCameraCharacteristicsCompat().get(CameraCharacteristics.LENS_POSE_REFERENCE);
+ Preconditions.checkNotNull(lensPoseRef);
+ return lensPoseRef;
+ }
+
+ /**
+ * Creates {@link Camera2PhysicalCameraInfo} instance.
+ *
+ * @param physicalCameraId physical camera id.
+ * @param cameraCharacteristicsCompat {@link CameraCharacteristicsCompat}.
+ * @return
+ */
+ @NonNull
+ public static Camera2PhysicalCameraInfo of(
+ @NonNull String physicalCameraId,
+ @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
+ return new AutoValue_Camera2PhysicalCameraInfo(
+ physicalCameraId, cameraCharacteristicsCompat);
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 1932f6a..a47d41c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -62,6 +62,7 @@
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.core.TorchState;
import androidx.camera.core.ZoomState;
@@ -86,6 +87,7 @@
import org.robolectric.shadows.StreamConfigurationMapBuilder;
import org.robolectric.util.ReflectionHelpers;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -515,6 +517,34 @@
assertThat(map.get("3")).isSameInstanceAs(characteristicsPhysical3);
}
+ @Config(minSdk = 28)
+ @RequiresApi(28)
+ @Test
+ public void canReturnPhysicalCameraInfos()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ CameraCharacteristics characteristics0 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical2 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical3 = mock(CameraCharacteristics.class);
+ when(characteristics0.getPhysicalCameraIds())
+ .thenReturn(new HashSet<>(Arrays.asList("0", "2", "3")));
+ CameraManagerCompat cameraManagerCompat = initCameraManagerWithPhysicalIds(
+ Arrays.asList(
+ new Pair<>("0", characteristics0),
+ new Pair<>("2", characteristicsPhysical2),
+ new Pair<>("3", characteristicsPhysical3)));
+ Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", cameraManagerCompat);
+
+ List<PhysicalCameraInfo> physicalCameraInfos = new ArrayList<>(
+ impl.getPhysicalCameraInfos());
+ assertThat(physicalCameraInfos.size()).isEqualTo(3);
+ assertThat(characteristics0.getPhysicalCameraIds()).containsExactly(
+ physicalCameraInfos.get(0).getPhysicalCameraId(),
+ physicalCameraInfos.get(1).getPhysicalCameraId(),
+ physicalCameraInfos.get(2).getPhysicalCameraId());
+ }
+
@Config(maxSdk = 27)
@Test
public void canReturnCameraCharacteristicsMapWithMainCamera()
diff --git a/camera/camera-core/api/1.4.0-beta01.txt b/camera/camera-core/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..abd42142
--- /dev/null
+++ b/camera/camera-core/api/1.4.0-beta01.txt
@@ -0,0 +1,716 @@
+// Signature format: 4.0
+package androidx.camera.core {
+
+ @RequiresApi(21) public class AspectRatio {
+ field public static final int RATIO_16_9 = 1; // 0x1
+ field public static final int RATIO_4_3 = 0; // 0x0
+ field public static final int RATIO_DEFAULT = -1; // 0xffffffff
+ }
+
+ @RequiresApi(21) public interface Camera {
+ method public androidx.camera.core.CameraControl getCameraControl();
+ method public androidx.camera.core.CameraInfo getCameraInfo();
+ }
+
+ @RequiresApi(21) public interface CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ }
+
+ public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
+ }
+
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.ImageProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public androidx.core.util.Consumer<java.lang.Throwable!> getErrorListener();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 2; // 0x2
+ }
+
+ @RequiresApi(21) public interface CameraFilter {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ }
+
+ @RequiresApi(21) public interface CameraInfo {
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.CameraState!> getCameraState();
+ method public androidx.camera.core.ExposureState getExposureState();
+ method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
+ method public default int getLensFacing();
+ method public int getSensorRotationDegrees();
+ method public int getSensorRotationDegrees(int);
+ method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
+ method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method public boolean hasFlashUnit();
+ method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
+ method public static boolean mustPlayShutterSound();
+ method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ }
+
+ @RequiresApi(21) public final class CameraInfoUnavailableException extends java.lang.Exception {
+ }
+
+ @RequiresApi(21) public interface CameraProvider {
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ }
+
+ @RequiresApi(21) public final class CameraSelector {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
+ field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
+ field public static final int LENS_FACING_BACK = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalLensFacing public static final int LENS_FACING_EXTERNAL = 2; // 0x2
+ field public static final int LENS_FACING_FRONT = 0; // 0x0
+ field public static final int LENS_FACING_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static final class CameraSelector.Builder {
+ ctor public CameraSelector.Builder();
+ method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
+ method public androidx.camera.core.CameraSelector build();
+ method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class CameraState {
+ ctor public CameraState();
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type);
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type, androidx.camera.core.CameraState.StateError?);
+ method public abstract androidx.camera.core.CameraState.StateError? getError();
+ method public abstract androidx.camera.core.CameraState.Type getType();
+ field public static final int ERROR_CAMERA_DISABLED = 5; // 0x5
+ field public static final int ERROR_CAMERA_FATAL_ERROR = 6; // 0x6
+ field public static final int ERROR_CAMERA_IN_USE = 2; // 0x2
+ field public static final int ERROR_DO_NOT_DISTURB_MODE_ENABLED = 7; // 0x7
+ field public static final int ERROR_MAX_CAMERAS_IN_USE = 1; // 0x1
+ field public static final int ERROR_OTHER_RECOVERABLE_ERROR = 3; // 0x3
+ field public static final int ERROR_STREAM_CONFIG = 4; // 0x4
+ }
+
+ public enum CameraState.ErrorType {
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType CRITICAL;
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType RECOVERABLE;
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class CameraState.StateError {
+ ctor public CameraState.StateError();
+ method public static androidx.camera.core.CameraState.StateError create(int);
+ method public static androidx.camera.core.CameraState.StateError create(int, Throwable?);
+ method public abstract Throwable? getCause();
+ method public abstract int getCode();
+ method public androidx.camera.core.CameraState.ErrorType getType();
+ }
+
+ public enum CameraState.Type {
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSED;
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSING;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPEN;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPENING;
+ enum_constant public static final androidx.camera.core.CameraState.Type PENDING_OPEN;
+ }
+
+ @RequiresApi(21) public class CameraUnavailableException extends java.lang.Exception {
+ ctor public CameraUnavailableException(int);
+ ctor public CameraUnavailableException(int, String?);
+ ctor public CameraUnavailableException(int, String?, Throwable?);
+ ctor public CameraUnavailableException(int, Throwable?);
+ method public int getReason();
+ field public static final int CAMERA_DISABLED = 1; // 0x1
+ field public static final int CAMERA_DISCONNECTED = 2; // 0x2
+ field public static final int CAMERA_ERROR = 3; // 0x3
+ field public static final int CAMERA_IN_USE = 4; // 0x4
+ field public static final int CAMERA_MAX_IN_USE = 5; // 0x5
+ field public static final int CAMERA_UNAVAILABLE_DO_NOT_DISTURB = 6; // 0x6
+ field public static final int CAMERA_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @RequiresApi(21) public final class CameraXConfig {
+ method public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
+ method public java.util.concurrent.Executor? getCameraExecutor(java.util.concurrent.Executor?);
+ method public long getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.RetryPolicy getCameraProviderInitRetryPolicy();
+ method public int getMinimumLoggingLevel();
+ method public android.os.Handler? getSchedulerHandler(android.os.Handler?);
+ }
+
+ public static final class CameraXConfig.Builder {
+ method public androidx.camera.core.CameraXConfig build();
+ method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraOpenRetryMaxTimeoutInMillisWhileResuming(long);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.CameraXConfig.Builder setCameraProviderInitRetryPolicy(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
+ method public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
+ }
+
+ public static interface CameraXConfig.Provider {
+ method public androidx.camera.core.CameraXConfig getCameraXConfig();
+ }
+
+ @RequiresApi(21) public class ConcurrentCamera {
+ ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
+ method public java.util.List<androidx.camera.core.Camera!> getCameras();
+ }
+
+ public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
+ method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
+ }
+
+ @RequiresApi(21) public final class DisplayOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
+ }
+
+ @RequiresApi(21) public final class DynamicRange {
+ ctor public DynamicRange(int, int);
+ method public int getBitDepth();
+ method public int getEncoding();
+ field public static final int BIT_DEPTH_10_BIT = 10; // 0xa
+ field public static final int BIT_DEPTH_8_BIT = 8; // 0x8
+ field public static final int BIT_DEPTH_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_10_BIT;
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_8_BIT;
+ field public static final int ENCODING_DOLBY_VISION = 6; // 0x6
+ field public static final int ENCODING_HDR10 = 4; // 0x4
+ field public static final int ENCODING_HDR10_PLUS = 5; // 0x5
+ field public static final int ENCODING_HDR_UNSPECIFIED = 2; // 0x2
+ field public static final int ENCODING_HLG = 3; // 0x3
+ field public static final int ENCODING_SDR = 1; // 0x1
+ field public static final int ENCODING_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange HDR10_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR10_PLUS_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR_UNSPECIFIED_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HLG_10_BIT;
+ field public static final androidx.camera.core.DynamicRange SDR;
+ field public static final androidx.camera.core.DynamicRange UNSPECIFIED;
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalZeroShutterLag {
+ }
+
+ @RequiresApi(21) public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
+ @RequiresApi(21) public interface ExtendableBuilder<T> {
+ method public T build();
+ }
+
+ @RequiresApi(21) public final class FocusMeteringAction {
+ method public long getAutoCancelDurationInMillis();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAe();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAf();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAwb();
+ method public boolean isAutoCancelEnabled();
+ field public static final int FLAG_AE = 2; // 0x2
+ field public static final int FLAG_AF = 1; // 0x1
+ field public static final int FLAG_AWB = 4; // 0x4
+ }
+
+ public static class FocusMeteringAction.Builder {
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint);
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction build();
+ method public androidx.camera.core.FocusMeteringAction.Builder disableAutoCancel();
+ method public androidx.camera.core.FocusMeteringAction.Builder setAutoCancelDuration(@IntRange(from=1) long, java.util.concurrent.TimeUnit);
+ }
+
+ @RequiresApi(21) public final class FocusMeteringResult {
+ method public boolean isFocusSuccessful();
+ }
+
+ @RequiresApi(21) public final class ImageAnalysis extends androidx.camera.core.UseCase {
+ method public void clearAnalyzer();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalUseCaseApi public java.util.concurrent.Executor? getBackgroundExecutor();
+ method public int getBackpressureStrategy();
+ method public int getImageQueueDepth();
+ method public int getOutputImageFormat();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public int getTargetRotation();
+ method public boolean isOutputImageRotationEnabled();
+ method public void setAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method public void setTargetRotation(int);
+ field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
+ field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
+ field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
+ field public static final int STRATEGY_KEEP_ONLY_LATEST = 0; // 0x0
+ }
+
+ public static interface ImageAnalysis.Analyzer {
+ method public void analyze(androidx.camera.core.ImageProxy);
+ method public default android.util.Size? getDefaultTargetResolution();
+ method public default int getTargetCoordinateSystem();
+ method public default void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
+ ctor public ImageAnalysis.Builder();
+ method public androidx.camera.core.ImageAnalysis build();
+ method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageAnalysis.Builder setBackpressureStrategy(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setImageQueueDepth(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setOutputImageFormat(int);
+ method @RequiresApi(23) public androidx.camera.core.ImageAnalysis.Builder setOutputImageRotationEnabled(boolean);
+ method public androidx.camera.core.ImageAnalysis.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetRotation(int);
+ }
+
+ @RequiresApi(21) public final class ImageCapture extends androidx.camera.core.UseCase {
+ method public int getCaptureMode();
+ method public int getFlashMode();
+ method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
+ method @IntRange(from=1, to=100) public int getJpegQuality();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
+ method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method public int getTargetRotation();
+ method public boolean isPostviewEnabled();
+ method public void setCropAspectRatio(android.util.Rational);
+ method public void setFlashMode(int);
+ method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
+ method public void setTargetRotation(int);
+ method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
+ field public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public static final int CAPTURE_MODE_ZERO_SHUTTER_LAG = 2; // 0x2
+ field public static final int ERROR_CAMERA_CLOSED = 3; // 0x3
+ field public static final int ERROR_CAPTURE_FAILED = 2; // 0x2
+ field public static final int ERROR_FILE_IO = 1; // 0x1
+ field public static final int ERROR_INVALID_CAMERA = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int FLASH_MODE_AUTO = 0; // 0x0
+ field public static final int FLASH_MODE_OFF = 2; // 0x2
+ field public static final int FLASH_MODE_ON = 1; // 0x1
+ field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+ }
+
+ public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
+ ctor public ImageCapture.Builder();
+ method public androidx.camera.core.ImageCapture build();
+ method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageCapture.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageCapture.Builder setTargetRotation(int);
+ }
+
+ public static final class ImageCapture.Metadata {
+ ctor public ImageCapture.Metadata();
+ method public android.location.Location? getLocation();
+ method public boolean isReversedHorizontal();
+ method public boolean isReversedVertical();
+ method public void setLocation(android.location.Location?);
+ method public void setReversedHorizontal(boolean);
+ method public void setReversedVertical(boolean);
+ }
+
+ public abstract static class ImageCapture.OnImageCapturedCallback {
+ ctor public ImageCapture.OnImageCapturedCallback();
+ method public void onCaptureProcessProgressed(int);
+ method public void onCaptureStarted();
+ method public void onCaptureSuccess(androidx.camera.core.ImageProxy);
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static interface ImageCapture.OnImageSavedCallback {
+ method public default void onCaptureProcessProgressed(int);
+ method public default void onCaptureStarted();
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onImageSaved(androidx.camera.core.ImageCapture.OutputFileResults);
+ method public default void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static final class ImageCapture.OutputFileOptions {
+ }
+
+ public static final class ImageCapture.OutputFileOptions.Builder {
+ ctor public ImageCapture.OutputFileOptions.Builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.File);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.OutputStream);
+ method public androidx.camera.core.ImageCapture.OutputFileOptions build();
+ method public androidx.camera.core.ImageCapture.OutputFileOptions.Builder setMetadata(androidx.camera.core.ImageCapture.Metadata);
+ }
+
+ public static class ImageCapture.OutputFileResults {
+ method public android.net.Uri? getSavedUri();
+ }
+
+ public static interface ImageCapture.ScreenFlash {
+ method @UiThread public void apply(long, androidx.camera.core.ImageCapture.ScreenFlashListener);
+ method @UiThread public void clear();
+ }
+
+ public static interface ImageCapture.ScreenFlashListener {
+ method public void onCompleted();
+ }
+
+ public interface ImageCaptureCapabilities {
+ method public boolean isCaptureProcessProgressSupported();
+ method public boolean isPostviewSupported();
+ }
+
+ @RequiresApi(21) public class ImageCaptureException extends java.lang.Exception {
+ ctor public ImageCaptureException(int, String, Throwable?);
+ method public int getImageCaptureError();
+ }
+
+ @RequiresApi(21) public class ImageCaptureLatencyEstimate {
+ ctor public ImageCaptureLatencyEstimate(long, long);
+ method public long getCaptureLatencyMillis();
+ method public long getProcessingLatencyMillis();
+ method public long getTotalCaptureLatencyMillis();
+ field public static final long UNDEFINED_CAPTURE_LATENCY = -1L; // 0xffffffffffffffffL
+ field public static final androidx.camera.core.ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ field public static final long UNDEFINED_PROCESSING_LATENCY = -1L; // 0xffffffffffffffffL
+ }
+
+ @RequiresApi(21) public interface ImageInfo {
+ method public int getRotationDegrees();
+ method public default android.graphics.Matrix getSensorToBufferTransformMatrix();
+ method public long getTimestamp();
+ }
+
+ public interface ImageProcessor {
+ method public androidx.camera.core.ImageProcessor.Response process(androidx.camera.core.ImageProcessor.Request) throws androidx.camera.core.ProcessingException;
+ }
+
+ public static interface ImageProcessor.Request {
+ method public androidx.camera.core.ImageProxy getInputImage();
+ method public int getOutputFormat();
+ }
+
+ public static interface ImageProcessor.Response {
+ method public androidx.camera.core.ImageProxy getOutputImage();
+ }
+
+ @RequiresApi(21) public interface ImageProxy extends java.lang.AutoCloseable {
+ method public void close();
+ method public android.graphics.Rect getCropRect();
+ method public int getFormat();
+ method public int getHeight();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalGetImage public android.media.Image? getImage();
+ method public androidx.camera.core.ImageInfo getImageInfo();
+ method public androidx.camera.core.ImageProxy.PlaneProxy![] getPlanes();
+ method public int getWidth();
+ method public void setCropRect(android.graphics.Rect?);
+ method public default android.graphics.Bitmap toBitmap();
+ }
+
+ public static interface ImageProxy.PlaneProxy {
+ method public java.nio.ByteBuffer getBuffer();
+ method public int getPixelStride();
+ method public int getRowStride();
+ }
+
+ @RequiresApi(21) public class InitializationException extends java.lang.Exception {
+ ctor public InitializationException(String?);
+ ctor public InitializationException(String?, Throwable?);
+ ctor public InitializationException(Throwable?);
+ }
+
+ @RequiresApi(21) public class MeteringPoint {
+ method public float getSize();
+ }
+
+ @RequiresApi(21) public abstract class MeteringPointFactory {
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float);
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float, float);
+ method public static float getDefaultPointSize();
+ }
+
+ @RequiresApi(21) public class MirrorMode {
+ field public static final int MIRROR_MODE_OFF = 0; // 0x0
+ field public static final int MIRROR_MODE_ON = 1; // 0x1
+ field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+ }
+
+ @RequiresApi(21) public final class Preview extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public static androidx.camera.core.PreviewCapabilities getPreviewCapabilities(androidx.camera.core.CameraInfo);
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isPreviewStabilizationEnabled();
+ method @UiThread public void setSurfaceProvider(androidx.camera.core.Preview.SurfaceProvider?);
+ method @UiThread public void setSurfaceProvider(java.util.concurrent.Executor, androidx.camera.core.Preview.SurfaceProvider?);
+ method public void setTargetRotation(int);
+ }
+
+ public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
+ ctor public Preview.Builder();
+ method public androidx.camera.core.Preview build();
+ method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
+ method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.Preview.Builder setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.core.Preview.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.Preview.Builder setTargetRotation(int);
+ }
+
+ public static interface Preview.SurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ @RequiresApi(21) public interface PreviewCapabilities {
+ method public boolean isStabilizationSupported();
+ }
+
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
+ @RequiresApi(21) public class ResolutionInfo {
+ ctor public ResolutionInfo(android.util.Size, android.graphics.Rect, int);
+ method public android.graphics.Rect getCropRect();
+ method public android.util.Size getResolution();
+ method public int getRotationDegrees();
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
+ method public static long getDefaultRetryTimeoutInMillis();
+ method public default long getTimeoutInMillis();
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
+ field public static final androidx.camera.core.RetryPolicy DEFAULT;
+ field public static final androidx.camera.core.RetryPolicy NEVER;
+ field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.Builder {
+ ctor public RetryPolicy.Builder(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.RetryPolicy build();
+ method public androidx.camera.core.RetryPolicy.Builder setTimeoutInMillis(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static interface RetryPolicy.ExecutionState {
+ method public Throwable? getCause();
+ method public long getExecutedTimeInMillis();
+ method public int getNumOfAttempts();
+ method public int getStatus();
+ field public static final int STATUS_CAMERA_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_CONFIGURATION_FAIL = 1; // 0x1
+ field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
+ method public static long getDefaultRetryDelayInMillis();
+ method public long getRetryDelayInMillis();
+ method public boolean shouldRetry();
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
+ }
+
+ @RequiresApi(21) public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public SurfaceOrientedMeteringPointFactory(float, float);
+ ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
+ }
+
+ public interface SurfaceOutput extends java.io.Closeable {
+ method public void close();
+ method public default android.graphics.Matrix getSensorToBufferTransform();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
+ @RequiresApi(21) public final class SurfaceRequest {
+ method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
+ method public void clearTransformationInfoListener();
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public android.util.Size getResolution();
+ method public boolean invalidate();
+ method public void provideSurface(android.view.Surface, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceRequest.Result!>);
+ method public void setTransformationInfoListener(java.util.concurrent.Executor, androidx.camera.core.SurfaceRequest.TransformationInfoListener);
+ method public boolean willNotProvideSurface();
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.Result {
+ method public abstract int getResultCode();
+ method public abstract android.view.Surface getSurface();
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.TransformationInfo {
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract boolean hasCameraTransform();
+ method public abstract boolean isMirroring();
+ }
+
+ public static interface SurfaceRequest.TransformationInfoListener {
+ method public void onTransformationInfoUpdate(androidx.camera.core.SurfaceRequest.TransformationInfo);
+ }
+
+ @RequiresApi(21) public class TorchState {
+ field public static final int OFF = 0; // 0x0
+ field public static final int ON = 1; // 0x1
+ }
+
+ @RequiresApi(21) public abstract class UseCase {
+ method public static int snapToSurfaceRotation(@IntRange(from=0, to=359) int);
+ }
+
+ @RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
+ method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
+ method public androidx.camera.core.ViewPort? getViewPort();
+ }
+
+ public static final class UseCaseGroup.Builder {
+ ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
+ method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
+ method public androidx.camera.core.UseCaseGroup build();
+ method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
+ }
+
+ @RequiresApi(21) public final class ViewPort {
+ method public android.util.Rational getAspectRatio();
+ method public int getLayoutDirection();
+ method public int getRotation();
+ method public int getScaleType();
+ field public static final int FILL_CENTER = 1; // 0x1
+ field public static final int FILL_END = 2; // 0x2
+ field public static final int FILL_START = 0; // 0x0
+ field public static final int FIT = 3; // 0x3
+ }
+
+ public static final class ViewPort.Builder {
+ ctor public ViewPort.Builder(android.util.Rational, int);
+ method public androidx.camera.core.ViewPort build();
+ method public androidx.camera.core.ViewPort.Builder setLayoutDirection(int);
+ method public androidx.camera.core.ViewPort.Builder setScaleType(int);
+ }
+
+ @RequiresApi(21) public interface ZoomState {
+ method public float getLinearZoom();
+ method public float getMaxZoomRatio();
+ method public float getMinZoomRatio();
+ method public float getZoomRatio();
+ }
+
+}
+
+package androidx.camera.core.resolutionselector {
+
+ @RequiresApi(21) public final class AspectRatioStrategy {
+ ctor public AspectRatioStrategy(int, int);
+ method public int getFallbackRule();
+ method public int getPreferredAspectRatio();
+ field public static final int FALLBACK_RULE_AUTO = 1; // 0x1
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_16_9_FALLBACK_AUTO_STRATEGY;
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+ }
+
+ @RequiresApi(21) public interface ResolutionFilter {
+ method public java.util.List<android.util.Size!> filter(java.util.List<android.util.Size!>, int);
+ }
+
+ @RequiresApi(21) public final class ResolutionSelector {
+ method public int getAllowedResolutionMode();
+ method public androidx.camera.core.resolutionselector.AspectRatioStrategy getAspectRatioStrategy();
+ method public androidx.camera.core.resolutionselector.ResolutionFilter? getResolutionFilter();
+ method public androidx.camera.core.resolutionselector.ResolutionStrategy? getResolutionStrategy();
+ field public static final int PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION = 0; // 0x0
+ field public static final int PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE = 1; // 0x1
+ }
+
+ public static final class ResolutionSelector.Builder {
+ ctor public ResolutionSelector.Builder();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector build();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAllowedResolutionMode(int);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAspectRatioStrategy(androidx.camera.core.resolutionselector.AspectRatioStrategy);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionStrategy(androidx.camera.core.resolutionselector.ResolutionStrategy);
+ }
+
+ @RequiresApi(21) public final class ResolutionStrategy {
+ ctor public ResolutionStrategy(android.util.Size, int);
+ method public android.util.Size? getBoundSize();
+ method public int getFallbackRule();
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2; // 0x2
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1; // 0x1
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER = 4; // 0x4
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3; // 0x3
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY;
+ }
+
+}
+
diff --git a/camera/camera-core/api/res-1.4.0-beta01.txt b/camera/camera-core/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-core/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-core/api/restricted_1.4.0-beta01.txt b/camera/camera-core/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..abd42142
--- /dev/null
+++ b/camera/camera-core/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,716 @@
+// Signature format: 4.0
+package androidx.camera.core {
+
+ @RequiresApi(21) public class AspectRatio {
+ field public static final int RATIO_16_9 = 1; // 0x1
+ field public static final int RATIO_4_3 = 0; // 0x0
+ field public static final int RATIO_DEFAULT = -1; // 0xffffffff
+ }
+
+ @RequiresApi(21) public interface Camera {
+ method public androidx.camera.core.CameraControl getCameraControl();
+ method public androidx.camera.core.CameraInfo getCameraInfo();
+ }
+
+ @RequiresApi(21) public interface CameraControl {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
+ }
+
+ public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
+ }
+
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.ImageProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public androidx.core.util.Consumer<java.lang.Throwable!> getErrorListener();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 2; // 0x2
+ }
+
+ @RequiresApi(21) public interface CameraFilter {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ }
+
+ @RequiresApi(21) public interface CameraInfo {
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.CameraState!> getCameraState();
+ method public androidx.camera.core.ExposureState getExposureState();
+ method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
+ method public default int getLensFacing();
+ method public int getSensorRotationDegrees();
+ method public int getSensorRotationDegrees(int);
+ method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
+ method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method public boolean hasFlashUnit();
+ method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
+ method public static boolean mustPlayShutterSound();
+ method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+ }
+
+ @RequiresApi(21) public final class CameraInfoUnavailableException extends java.lang.Exception {
+ }
+
+ @RequiresApi(21) public interface CameraProvider {
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ }
+
+ @RequiresApi(21) public final class CameraSelector {
+ method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
+ field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
+ field public static final int LENS_FACING_BACK = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalLensFacing public static final int LENS_FACING_EXTERNAL = 2; // 0x2
+ field public static final int LENS_FACING_FRONT = 0; // 0x0
+ field public static final int LENS_FACING_UNKNOWN = -1; // 0xffffffff
+ }
+
+ public static final class CameraSelector.Builder {
+ ctor public CameraSelector.Builder();
+ method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
+ method public androidx.camera.core.CameraSelector build();
+ method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class CameraState {
+ ctor public CameraState();
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type);
+ method public static androidx.camera.core.CameraState create(androidx.camera.core.CameraState.Type, androidx.camera.core.CameraState.StateError?);
+ method public abstract androidx.camera.core.CameraState.StateError? getError();
+ method public abstract androidx.camera.core.CameraState.Type getType();
+ field public static final int ERROR_CAMERA_DISABLED = 5; // 0x5
+ field public static final int ERROR_CAMERA_FATAL_ERROR = 6; // 0x6
+ field public static final int ERROR_CAMERA_IN_USE = 2; // 0x2
+ field public static final int ERROR_DO_NOT_DISTURB_MODE_ENABLED = 7; // 0x7
+ field public static final int ERROR_MAX_CAMERAS_IN_USE = 1; // 0x1
+ field public static final int ERROR_OTHER_RECOVERABLE_ERROR = 3; // 0x3
+ field public static final int ERROR_STREAM_CONFIG = 4; // 0x4
+ }
+
+ public enum CameraState.ErrorType {
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType CRITICAL;
+ enum_constant public static final androidx.camera.core.CameraState.ErrorType RECOVERABLE;
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class CameraState.StateError {
+ ctor public CameraState.StateError();
+ method public static androidx.camera.core.CameraState.StateError create(int);
+ method public static androidx.camera.core.CameraState.StateError create(int, Throwable?);
+ method public abstract Throwable? getCause();
+ method public abstract int getCode();
+ method public androidx.camera.core.CameraState.ErrorType getType();
+ }
+
+ public enum CameraState.Type {
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSED;
+ enum_constant public static final androidx.camera.core.CameraState.Type CLOSING;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPEN;
+ enum_constant public static final androidx.camera.core.CameraState.Type OPENING;
+ enum_constant public static final androidx.camera.core.CameraState.Type PENDING_OPEN;
+ }
+
+ @RequiresApi(21) public class CameraUnavailableException extends java.lang.Exception {
+ ctor public CameraUnavailableException(int);
+ ctor public CameraUnavailableException(int, String?);
+ ctor public CameraUnavailableException(int, String?, Throwable?);
+ ctor public CameraUnavailableException(int, Throwable?);
+ method public int getReason();
+ field public static final int CAMERA_DISABLED = 1; // 0x1
+ field public static final int CAMERA_DISCONNECTED = 2; // 0x2
+ field public static final int CAMERA_ERROR = 3; // 0x3
+ field public static final int CAMERA_IN_USE = 4; // 0x4
+ field public static final int CAMERA_MAX_IN_USE = 5; // 0x5
+ field public static final int CAMERA_UNAVAILABLE_DO_NOT_DISTURB = 6; // 0x6
+ field public static final int CAMERA_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @RequiresApi(21) public final class CameraXConfig {
+ method public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
+ method public java.util.concurrent.Executor? getCameraExecutor(java.util.concurrent.Executor?);
+ method public long getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.RetryPolicy getCameraProviderInitRetryPolicy();
+ method public int getMinimumLoggingLevel();
+ method public android.os.Handler? getSchedulerHandler(android.os.Handler?);
+ }
+
+ public static final class CameraXConfig.Builder {
+ method public androidx.camera.core.CameraXConfig build();
+ method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.CameraXConfig.Builder setCameraOpenRetryMaxTimeoutInMillisWhileResuming(long);
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public androidx.camera.core.CameraXConfig.Builder setCameraProviderInitRetryPolicy(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
+ method public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
+ }
+
+ public static interface CameraXConfig.Provider {
+ method public androidx.camera.core.CameraXConfig getCameraXConfig();
+ }
+
+ @RequiresApi(21) public class ConcurrentCamera {
+ ctor public ConcurrentCamera(java.util.List<androidx.camera.core.Camera!>);
+ method public java.util.List<androidx.camera.core.Camera!> getCameras();
+ }
+
+ public static final class ConcurrentCamera.SingleCameraConfig {
+ ctor public ConcurrentCamera.SingleCameraConfig(androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup, androidx.lifecycle.LifecycleOwner);
+ method public androidx.camera.core.CameraSelector getCameraSelector();
+ method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
+ method public androidx.camera.core.UseCaseGroup getUseCaseGroup();
+ }
+
+ @RequiresApi(21) public final class DisplayOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
+ }
+
+ @RequiresApi(21) public final class DynamicRange {
+ ctor public DynamicRange(int, int);
+ method public int getBitDepth();
+ method public int getEncoding();
+ field public static final int BIT_DEPTH_10_BIT = 10; // 0xa
+ field public static final int BIT_DEPTH_8_BIT = 8; // 0x8
+ field public static final int BIT_DEPTH_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_10_BIT;
+ field public static final androidx.camera.core.DynamicRange DOLBY_VISION_8_BIT;
+ field public static final int ENCODING_DOLBY_VISION = 6; // 0x6
+ field public static final int ENCODING_HDR10 = 4; // 0x4
+ field public static final int ENCODING_HDR10_PLUS = 5; // 0x5
+ field public static final int ENCODING_HDR_UNSPECIFIED = 2; // 0x2
+ field public static final int ENCODING_HLG = 3; // 0x3
+ field public static final int ENCODING_SDR = 1; // 0x1
+ field public static final int ENCODING_UNSPECIFIED = 0; // 0x0
+ field public static final androidx.camera.core.DynamicRange HDR10_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR10_PLUS_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HDR_UNSPECIFIED_10_BIT;
+ field public static final androidx.camera.core.DynamicRange HLG_10_BIT;
+ field public static final androidx.camera.core.DynamicRange SDR;
+ field public static final androidx.camera.core.DynamicRange UNSPECIFIED;
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLensFacing {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalRetryPolicy {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalZeroShutterLag {
+ }
+
+ @RequiresApi(21) public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
+ @RequiresApi(21) public interface ExtendableBuilder<T> {
+ method public T build();
+ }
+
+ @RequiresApi(21) public final class FocusMeteringAction {
+ method public long getAutoCancelDurationInMillis();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAe();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAf();
+ method public java.util.List<androidx.camera.core.MeteringPoint!> getMeteringPointsAwb();
+ method public boolean isAutoCancelEnabled();
+ field public static final int FLAG_AE = 2; // 0x2
+ field public static final int FLAG_AF = 1; // 0x1
+ field public static final int FLAG_AWB = 4; // 0x4
+ }
+
+ public static class FocusMeteringAction.Builder {
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint);
+ ctor public FocusMeteringAction.Builder(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint);
+ method public androidx.camera.core.FocusMeteringAction.Builder addPoint(androidx.camera.core.MeteringPoint, int);
+ method public androidx.camera.core.FocusMeteringAction build();
+ method public androidx.camera.core.FocusMeteringAction.Builder disableAutoCancel();
+ method public androidx.camera.core.FocusMeteringAction.Builder setAutoCancelDuration(@IntRange(from=1) long, java.util.concurrent.TimeUnit);
+ }
+
+ @RequiresApi(21) public final class FocusMeteringResult {
+ method public boolean isFocusSuccessful();
+ }
+
+ @RequiresApi(21) public final class ImageAnalysis extends androidx.camera.core.UseCase {
+ method public void clearAnalyzer();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalUseCaseApi public java.util.concurrent.Executor? getBackgroundExecutor();
+ method public int getBackpressureStrategy();
+ method public int getImageQueueDepth();
+ method public int getOutputImageFormat();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public int getTargetRotation();
+ method public boolean isOutputImageRotationEnabled();
+ method public void setAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method public void setTargetRotation(int);
+ field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
+ field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
+ field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
+ field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
+ field public static final int STRATEGY_KEEP_ONLY_LATEST = 0; // 0x0
+ }
+
+ public static interface ImageAnalysis.Analyzer {
+ method public void analyze(androidx.camera.core.ImageProxy);
+ method public default android.util.Size? getDefaultTargetResolution();
+ method public default int getTargetCoordinateSystem();
+ method public default void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class ImageAnalysis.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageAnalysis!> {
+ ctor public ImageAnalysis.Builder();
+ method public androidx.camera.core.ImageAnalysis build();
+ method public androidx.camera.core.ImageAnalysis.Builder setBackgroundExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageAnalysis.Builder setBackpressureStrategy(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setImageQueueDepth(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setOutputImageFormat(int);
+ method @RequiresApi(23) public androidx.camera.core.ImageAnalysis.Builder setOutputImageRotationEnabled(boolean);
+ method public androidx.camera.core.ImageAnalysis.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageAnalysis.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageAnalysis.Builder setTargetRotation(int);
+ }
+
+ @RequiresApi(21) public final class ImageCapture extends androidx.camera.core.UseCase {
+ method public int getCaptureMode();
+ method public int getFlashMode();
+ method public static androidx.camera.core.ImageCaptureCapabilities getImageCaptureCapabilities(androidx.camera.core.CameraInfo);
+ method @IntRange(from=1, to=100) public int getJpegQuality();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getPostviewResolutionSelector();
+ method public androidx.camera.core.ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate();
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method public int getTargetRotation();
+ method public boolean isPostviewEnabled();
+ method public void setCropAspectRatio(android.util.Rational);
+ method public void setFlashMode(int);
+ method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
+ method public void setTargetRotation(int);
+ method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
+ field public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1; // 0x1
+ field @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public static final int CAPTURE_MODE_ZERO_SHUTTER_LAG = 2; // 0x2
+ field public static final int ERROR_CAMERA_CLOSED = 3; // 0x3
+ field public static final int ERROR_CAPTURE_FAILED = 2; // 0x2
+ field public static final int ERROR_FILE_IO = 1; // 0x1
+ field public static final int ERROR_INVALID_CAMERA = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int FLASH_MODE_AUTO = 0; // 0x0
+ field public static final int FLASH_MODE_OFF = 2; // 0x2
+ field public static final int FLASH_MODE_ON = 1; // 0x1
+ field public static final int FLASH_MODE_SCREEN = 3; // 0x3
+ }
+
+ public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
+ ctor public ImageCapture.Builder();
+ method public androidx.camera.core.ImageCapture build();
+ method public androidx.camera.core.ImageCapture.Builder setCaptureMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setFlashMode(int);
+ method public androidx.camera.core.ImageCapture.Builder setIoExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.core.ImageCapture.Builder setJpegQuality(@IntRange(from=1, to=100) int);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewEnabled(boolean);
+ method public androidx.camera.core.ImageCapture.Builder setPostviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method public androidx.camera.core.ImageCapture.Builder setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.ImageCapture.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.ImageCapture.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.ImageCapture.Builder setTargetRotation(int);
+ }
+
+ public static final class ImageCapture.Metadata {
+ ctor public ImageCapture.Metadata();
+ method public android.location.Location? getLocation();
+ method public boolean isReversedHorizontal();
+ method public boolean isReversedVertical();
+ method public void setLocation(android.location.Location?);
+ method public void setReversedHorizontal(boolean);
+ method public void setReversedVertical(boolean);
+ }
+
+ public abstract static class ImageCapture.OnImageCapturedCallback {
+ ctor public ImageCapture.OnImageCapturedCallback();
+ method public void onCaptureProcessProgressed(int);
+ method public void onCaptureStarted();
+ method public void onCaptureSuccess(androidx.camera.core.ImageProxy);
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static interface ImageCapture.OnImageSavedCallback {
+ method public default void onCaptureProcessProgressed(int);
+ method public default void onCaptureStarted();
+ method public void onError(androidx.camera.core.ImageCaptureException);
+ method public void onImageSaved(androidx.camera.core.ImageCapture.OutputFileResults);
+ method public default void onPostviewBitmapAvailable(android.graphics.Bitmap);
+ }
+
+ public static final class ImageCapture.OutputFileOptions {
+ }
+
+ public static final class ImageCapture.OutputFileOptions.Builder {
+ ctor public ImageCapture.OutputFileOptions.Builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.File);
+ ctor public ImageCapture.OutputFileOptions.Builder(java.io.OutputStream);
+ method public androidx.camera.core.ImageCapture.OutputFileOptions build();
+ method public androidx.camera.core.ImageCapture.OutputFileOptions.Builder setMetadata(androidx.camera.core.ImageCapture.Metadata);
+ }
+
+ public static class ImageCapture.OutputFileResults {
+ method public android.net.Uri? getSavedUri();
+ }
+
+ public static interface ImageCapture.ScreenFlash {
+ method @UiThread public void apply(long, androidx.camera.core.ImageCapture.ScreenFlashListener);
+ method @UiThread public void clear();
+ }
+
+ public static interface ImageCapture.ScreenFlashListener {
+ method public void onCompleted();
+ }
+
+ public interface ImageCaptureCapabilities {
+ method public boolean isCaptureProcessProgressSupported();
+ method public boolean isPostviewSupported();
+ }
+
+ @RequiresApi(21) public class ImageCaptureException extends java.lang.Exception {
+ ctor public ImageCaptureException(int, String, Throwable?);
+ method public int getImageCaptureError();
+ }
+
+ @RequiresApi(21) public class ImageCaptureLatencyEstimate {
+ ctor public ImageCaptureLatencyEstimate(long, long);
+ method public long getCaptureLatencyMillis();
+ method public long getProcessingLatencyMillis();
+ method public long getTotalCaptureLatencyMillis();
+ field public static final long UNDEFINED_CAPTURE_LATENCY = -1L; // 0xffffffffffffffffL
+ field public static final androidx.camera.core.ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY;
+ field public static final long UNDEFINED_PROCESSING_LATENCY = -1L; // 0xffffffffffffffffL
+ }
+
+ @RequiresApi(21) public interface ImageInfo {
+ method public int getRotationDegrees();
+ method public default android.graphics.Matrix getSensorToBufferTransformMatrix();
+ method public long getTimestamp();
+ }
+
+ public interface ImageProcessor {
+ method public androidx.camera.core.ImageProcessor.Response process(androidx.camera.core.ImageProcessor.Request) throws androidx.camera.core.ProcessingException;
+ }
+
+ public static interface ImageProcessor.Request {
+ method public androidx.camera.core.ImageProxy getInputImage();
+ method public int getOutputFormat();
+ }
+
+ public static interface ImageProcessor.Response {
+ method public androidx.camera.core.ImageProxy getOutputImage();
+ }
+
+ @RequiresApi(21) public interface ImageProxy extends java.lang.AutoCloseable {
+ method public void close();
+ method public android.graphics.Rect getCropRect();
+ method public int getFormat();
+ method public int getHeight();
+ method @SuppressCompatibility @androidx.camera.core.ExperimentalGetImage public android.media.Image? getImage();
+ method public androidx.camera.core.ImageInfo getImageInfo();
+ method public androidx.camera.core.ImageProxy.PlaneProxy![] getPlanes();
+ method public int getWidth();
+ method public void setCropRect(android.graphics.Rect?);
+ method public default android.graphics.Bitmap toBitmap();
+ }
+
+ public static interface ImageProxy.PlaneProxy {
+ method public java.nio.ByteBuffer getBuffer();
+ method public int getPixelStride();
+ method public int getRowStride();
+ }
+
+ @RequiresApi(21) public class InitializationException extends java.lang.Exception {
+ ctor public InitializationException(String?);
+ ctor public InitializationException(String?, Throwable?);
+ ctor public InitializationException(Throwable?);
+ }
+
+ @RequiresApi(21) public class MeteringPoint {
+ method public float getSize();
+ }
+
+ @RequiresApi(21) public abstract class MeteringPointFactory {
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float);
+ method public final androidx.camera.core.MeteringPoint createPoint(float, float, float);
+ method public static float getDefaultPointSize();
+ }
+
+ @RequiresApi(21) public class MirrorMode {
+ field public static final int MIRROR_MODE_OFF = 0; // 0x0
+ field public static final int MIRROR_MODE_ON = 1; // 0x1
+ field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+ }
+
+ @RequiresApi(21) public final class Preview extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public static androidx.camera.core.PreviewCapabilities getPreviewCapabilities(androidx.camera.core.CameraInfo);
+ method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector? getResolutionSelector();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isPreviewStabilizationEnabled();
+ method @UiThread public void setSurfaceProvider(androidx.camera.core.Preview.SurfaceProvider?);
+ method @UiThread public void setSurfaceProvider(java.util.concurrent.Executor, androidx.camera.core.Preview.SurfaceProvider?);
+ method public void setTargetRotation(int);
+ }
+
+ public static final class Preview.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.Preview!> {
+ ctor public Preview.Builder();
+ method public androidx.camera.core.Preview build();
+ method public androidx.camera.core.Preview.Builder setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.core.Preview.Builder setPreviewStabilizationEnabled(boolean);
+ method public androidx.camera.core.Preview.Builder setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetAspectRatio(int);
+ method public androidx.camera.core.Preview.Builder setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.core.Preview.Builder setTargetName(String);
+ method @Deprecated public androidx.camera.core.Preview.Builder setTargetResolution(android.util.Size);
+ method public androidx.camera.core.Preview.Builder setTargetRotation(int);
+ }
+
+ public static interface Preview.SurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ @RequiresApi(21) public interface PreviewCapabilities {
+ method public boolean isStabilizationSupported();
+ }
+
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
+ @RequiresApi(21) public class ResolutionInfo {
+ ctor public ResolutionInfo(android.util.Size, android.graphics.Rect, int);
+ method public android.graphics.Rect getCropRect();
+ method public android.util.Size getResolution();
+ method public int getRotationDegrees();
+ }
+
+ @SuppressCompatibility @RequiresApi(21) @androidx.camera.core.ExperimentalRetryPolicy public interface RetryPolicy {
+ method public static long getDefaultRetryTimeoutInMillis();
+ method public default long getTimeoutInMillis();
+ method public androidx.camera.core.RetryPolicy.RetryConfig onRetryDecisionRequested(androidx.camera.core.RetryPolicy.ExecutionState);
+ field public static final androidx.camera.core.RetryPolicy DEFAULT;
+ field public static final androidx.camera.core.RetryPolicy NEVER;
+ field public static final androidx.camera.core.RetryPolicy RETRY_UNAVAILABLE_CAMERA;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.Builder {
+ ctor public RetryPolicy.Builder(androidx.camera.core.RetryPolicy);
+ method public androidx.camera.core.RetryPolicy build();
+ method public androidx.camera.core.RetryPolicy.Builder setTimeoutInMillis(long);
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static interface RetryPolicy.ExecutionState {
+ method public Throwable? getCause();
+ method public long getExecutedTimeInMillis();
+ method public int getNumOfAttempts();
+ method public int getStatus();
+ field public static final int STATUS_CAMERA_UNAVAILABLE = 2; // 0x2
+ field public static final int STATUS_CONFIGURATION_FAIL = 1; // 0x1
+ field public static final int STATUS_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig {
+ method public static long getDefaultRetryDelayInMillis();
+ method public long getRetryDelayInMillis();
+ method public boolean shouldRetry();
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig DEFAULT_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig MINI_DELAY_RETRY;
+ field public static final androidx.camera.core.RetryPolicy.RetryConfig NOT_RETRY;
+ }
+
+ @SuppressCompatibility @androidx.camera.core.ExperimentalRetryPolicy public static final class RetryPolicy.RetryConfig.Builder {
+ ctor public RetryPolicy.RetryConfig.Builder();
+ method public androidx.camera.core.RetryPolicy.RetryConfig build();
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setRetryDelayInMillis(@IntRange(from=100, to=2000) long);
+ method public androidx.camera.core.RetryPolicy.RetryConfig.Builder setShouldRetry(boolean);
+ }
+
+ @RequiresApi(21) public class SurfaceOrientedMeteringPointFactory extends androidx.camera.core.MeteringPointFactory {
+ ctor public SurfaceOrientedMeteringPointFactory(float, float);
+ ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
+ }
+
+ public interface SurfaceOutput extends java.io.Closeable {
+ method public void close();
+ method public default android.graphics.Matrix getSensorToBufferTransform();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
+ @RequiresApi(21) public final class SurfaceRequest {
+ method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
+ method public void clearTransformationInfoListener();
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public android.util.Size getResolution();
+ method public boolean invalidate();
+ method public void provideSurface(android.view.Surface, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceRequest.Result!>);
+ method public void setTransformationInfoListener(java.util.concurrent.Executor, androidx.camera.core.SurfaceRequest.TransformationInfoListener);
+ method public boolean willNotProvideSurface();
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.Result {
+ method public abstract int getResultCode();
+ method public abstract android.view.Surface getSurface();
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceRequest.TransformationInfo {
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract boolean hasCameraTransform();
+ method public abstract boolean isMirroring();
+ }
+
+ public static interface SurfaceRequest.TransformationInfoListener {
+ method public void onTransformationInfoUpdate(androidx.camera.core.SurfaceRequest.TransformationInfo);
+ }
+
+ @RequiresApi(21) public class TorchState {
+ field public static final int OFF = 0; // 0x0
+ field public static final int ON = 1; // 0x1
+ }
+
+ @RequiresApi(21) public abstract class UseCase {
+ method public static int snapToSurfaceRotation(@IntRange(from=0, to=359) int);
+ }
+
+ @RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
+ method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
+ method public androidx.camera.core.ViewPort? getViewPort();
+ }
+
+ public static final class UseCaseGroup.Builder {
+ ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
+ method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
+ method public androidx.camera.core.UseCaseGroup build();
+ method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
+ }
+
+ @RequiresApi(21) public final class ViewPort {
+ method public android.util.Rational getAspectRatio();
+ method public int getLayoutDirection();
+ method public int getRotation();
+ method public int getScaleType();
+ field public static final int FILL_CENTER = 1; // 0x1
+ field public static final int FILL_END = 2; // 0x2
+ field public static final int FILL_START = 0; // 0x0
+ field public static final int FIT = 3; // 0x3
+ }
+
+ public static final class ViewPort.Builder {
+ ctor public ViewPort.Builder(android.util.Rational, int);
+ method public androidx.camera.core.ViewPort build();
+ method public androidx.camera.core.ViewPort.Builder setLayoutDirection(int);
+ method public androidx.camera.core.ViewPort.Builder setScaleType(int);
+ }
+
+ @RequiresApi(21) public interface ZoomState {
+ method public float getLinearZoom();
+ method public float getMaxZoomRatio();
+ method public float getMinZoomRatio();
+ method public float getZoomRatio();
+ }
+
+}
+
+package androidx.camera.core.resolutionselector {
+
+ @RequiresApi(21) public final class AspectRatioStrategy {
+ ctor public AspectRatioStrategy(int, int);
+ method public int getFallbackRule();
+ method public int getPreferredAspectRatio();
+ field public static final int FALLBACK_RULE_AUTO = 1; // 0x1
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_16_9_FALLBACK_AUTO_STRATEGY;
+ field public static final androidx.camera.core.resolutionselector.AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+ }
+
+ @RequiresApi(21) public interface ResolutionFilter {
+ method public java.util.List<android.util.Size!> filter(java.util.List<android.util.Size!>, int);
+ }
+
+ @RequiresApi(21) public final class ResolutionSelector {
+ method public int getAllowedResolutionMode();
+ method public androidx.camera.core.resolutionselector.AspectRatioStrategy getAspectRatioStrategy();
+ method public androidx.camera.core.resolutionselector.ResolutionFilter? getResolutionFilter();
+ method public androidx.camera.core.resolutionselector.ResolutionStrategy? getResolutionStrategy();
+ field public static final int PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION = 0; // 0x0
+ field public static final int PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE = 1; // 0x1
+ }
+
+ public static final class ResolutionSelector.Builder {
+ ctor public ResolutionSelector.Builder();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector build();
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAllowedResolutionMode(int);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setAspectRatioStrategy(androidx.camera.core.resolutionselector.AspectRatioStrategy);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter);
+ method public androidx.camera.core.resolutionselector.ResolutionSelector.Builder setResolutionStrategy(androidx.camera.core.resolutionselector.ResolutionStrategy);
+ }
+
+ @RequiresApi(21) public final class ResolutionStrategy {
+ ctor public ResolutionStrategy(android.util.Size, int);
+ method public android.util.Size? getBoundSize();
+ method public int getFallbackRule();
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2; // 0x2
+ field public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1; // 0x1
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER = 4; // 0x4
+ field public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3; // 0x3
+ field public static final int FALLBACK_RULE_NONE = 0; // 0x0
+ field public static final androidx.camera.core.resolutionselector.ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY;
+ }
+
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 7d71a4f..846b3a8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -330,6 +330,18 @@
}
/**
+ * Returns if logical multi camera is supported on the device.
+ *
+ * @return true if supported, otherwise false.
+ * @see android.hardware.camera2.CameraMetadata
+ * #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ default boolean isLogicalMultiCameraSupported() {
+ return false;
+ }
+
+ /**
* Returns if {@link ImageFormat#PRIVATE} reprocessing is supported on the device.
*
* @return true if supported, otherwise false.
@@ -405,6 +417,17 @@
Collections.singleton(DynamicRange.SDR));
}
+ /**
+ * Returns a set of {@link PhysicalCameraInfo}.
+ *
+ * @return Set of {@link PhysicalCameraInfo}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ default Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ return Collections.emptySet();
+ }
+
@StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
IMPLEMENTATION_TYPE_CAMERA2_LEGACY, IMPLEMENTATION_TYPE_CAMERA2,
IMPLEMENTATION_TYPE_FAKE})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index 240375f..ca80c82 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -66,10 +66,16 @@
public static final CameraSelector DEFAULT_BACK_CAMERA =
new CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build();
- private LinkedHashSet<CameraFilter> mCameraFilterSet;
+ @NonNull
+ private final LinkedHashSet<CameraFilter> mCameraFilterSet;
- CameraSelector(LinkedHashSet<CameraFilter> cameraFilterSet) {
+ @Nullable
+ private final String mPhysicalCameraId;
+
+ CameraSelector(@NonNull LinkedHashSet<CameraFilter> cameraFilterSet,
+ @Nullable String physicalCameraId) {
mCameraFilterSet = cameraFilterSet;
+ mPhysicalCameraId = physicalCameraId;
}
/**
@@ -204,10 +210,25 @@
return currentLensFacing;
}
+ /**
+ * Returns the physical camera id.
+ *
+ * @return physical camera id.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public String getPhysicalCameraId() {
+ return mPhysicalCameraId;
+ }
+
/** Builder for a {@link CameraSelector}. */
public static final class Builder {
+ @NonNull
private final LinkedHashSet<CameraFilter> mCameraFilterSet;
+ @Nullable
+ private String mPhysicalCameraId;
+
public Builder() {
mCameraFilterSet = new LinkedHashSet<>();
}
@@ -270,10 +291,23 @@
return builder;
}
+ /**
+ * Sets the physical camera id.
+ *
+ * @param physicalCameraId physical camera id.
+ * @return this builder.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setPhysicalCameraId(@NonNull String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ return this;
+ }
+
/** Builds the {@link CameraSelector}. */
@NonNull
public CameraSelector build() {
- return new CameraSelector(mCameraFilterSet);
+ return new CameraSelector(mCameraFilterSet, mPhysicalCameraId);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index ed8b596..346b571 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -392,7 +392,7 @@
sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
- sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange());
+ sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange(), null);
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
clearPipeline();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java
new file mode 100644
index 0000000..1ab2035e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * An interface for retrieving physical camera information.
+ *
+ * <p>Applications can retrieve physical camera information via
+ * {@link CameraInfo#getPhysicalCameraInfos()}. As a comparison, {@link CameraInfo} represents
+ * logical camera information. A logical camera is a grouping of two or more of those physical
+ * cameras.
+ *
+ * <p>See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ * for more information.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+public interface PhysicalCameraInfo {
+
+ /**
+ * Returns physical camera id.
+ *
+ * @return physical camera id.
+ */
+ @NonNull
+ String getPhysicalCameraId();
+
+ /**
+ * Returns {@link android.hardware.camera2.CameraCharacteristics#LENS_POSE_REFERENCE}.
+ *
+ * @return lens pose reference.
+ */
+ @RequiresApi(28)
+ @NonNull
+ Integer getLensPoseReference();
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 02d910b..77899224 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -332,7 +332,8 @@
// output target for these two cases.
if (mSurfaceProvider != null) {
sessionConfigBuilder.addSurface(mSessionDeferrableSurface,
- streamSpec.getDynamicRange());
+ streamSpec.getDynamicRange(),
+ getPhysicalCameraId());
}
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index bd07576..8bd0481 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -152,6 +152,9 @@
@Nullable
private CameraEffect mEffect;
+ @Nullable
+ private String mPhysicalCameraId;
+
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@@ -362,6 +365,17 @@
}
}
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void setPhysicalCameraId(@NonNull String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public String getPhysicalCameraId() {
+ return mPhysicalCameraId;
+ }
+
/**
* Updates the target rotation of the use case config.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 7d101c05..cbdfa54 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -28,6 +28,7 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.ZoomState;
import androidx.lifecycle.LiveData;
@@ -129,6 +130,11 @@
return mCameraInfoInternal.isPrivateReprocessingSupported();
}
+ @Override
+ public boolean isLogicalMultiCameraSupported() {
+ return mCameraInfoInternal.isLogicalMultiCameraSupported();
+ }
+
@NonNull
@Override
public String getCameraId() {
@@ -228,4 +234,10 @@
public Object getPhysicalCameraCharacteristics(@NonNull String physicalCameraId) {
return mCameraInfoInternal.getPhysicalCameraCharacteristics(physicalCameraId);
}
+
+ @NonNull
+ @Override
+ public Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ return mCameraInfoInternal.getPhysicalCameraInfos();
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index 63bdf6d..414ddb0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -647,7 +647,7 @@
*/
@NonNull
public Builder addSurface(@NonNull DeferrableSurface surface) {
- return addSurface(surface, DynamicRange.SDR);
+ return addSurface(surface, DynamicRange.SDR, null);
}
/**
@@ -656,8 +656,10 @@
*/
@NonNull
public Builder addSurface(@NonNull DeferrableSurface surface,
- @NonNull DynamicRange dynamicRange) {
+ @NonNull DynamicRange dynamicRange,
+ @Nullable String physicalCameraId) {
OutputConfig outputConfig = OutputConfig.builder(surface)
+ .setPhysicalCameraId(physicalCameraId)
.setDynamicRange(dynamicRange)
.build();
mOutputConfigs.add(outputConfig);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 687a41b..9844499 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -278,7 +278,7 @@
propagateChildrenCamera2Interop(streamSpec.getResolution(), builder);
- builder.addSurface(mCameraEdge.getDeferrableSurface(), streamSpec.getDynamicRange());
+ builder.addSurface(mCameraEdge.getDeferrableSurface(), streamSpec.getDynamicRange(), null);
builder.addRepeatingCameraCaptureCallback(
mVirtualCameraAdapter.getParentMetadataCallback());
if (streamSpec.getImplementationOptions() != null) {
diff --git a/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/1.4.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-effects-still-portrait/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-effects/api/1.4.0-beta01.txt b/camera/camera-effects/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..e47a913
--- /dev/null
+++ b/camera/camera-effects/api/1.4.0-beta01.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.camera.effects {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method @IntRange(from=0, to=359) public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ method public abstract boolean isMirroring();
+ }
+
+ @RequiresApi(21) public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-effects/api/res-1.4.0-beta01.txt b/camera/camera-effects/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-effects/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-effects/api/restricted_1.4.0-beta01.txt b/camera/camera-effects/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..e47a913
--- /dev/null
+++ b/camera/camera-effects/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.camera.effects {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method @IntRange(from=0, to=359) public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ method public abstract boolean isMirroring();
+ }
+
+ @RequiresApi(21) public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-extensions/api/1.4.0-beta01.txt b/camera/camera-extensions/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..f365e3d
--- /dev/null
+++ b/camera/camera-extensions/api/1.4.0-beta01.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.camera.extensions {
+
+ public interface CameraExtensionsControl {
+ method public default void setExtensionStrength(@IntRange(from=0, to=100) int);
+ }
+
+ public interface CameraExtensionsInfo {
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getCurrentExtensionMode();
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getExtensionStrength();
+ method public default boolean isCurrentExtensionModeAvailable();
+ method public default boolean isExtensionStrengthAvailable();
+ }
+
+ @RequiresApi(21) public final class ExtensionMode {
+ field public static final int AUTO = 5; // 0x5
+ field public static final int BOKEH = 1; // 0x1
+ field public static final int FACE_RETOUCH = 4; // 0x4
+ field public static final int HDR = 2; // 0x2
+ field public static final int NIGHT = 3; // 0x3
+ field public static final int NONE = 0; // 0x0
+ }
+
+ @RequiresApi(21) public final class ExtensionsManager {
+ method public androidx.camera.extensions.CameraExtensionsControl? getCameraExtensionsControl(androidx.camera.core.CameraControl);
+ method public androidx.camera.extensions.CameraExtensionsInfo getCameraExtensionsInfo(androidx.camera.core.CameraInfo);
+ method public android.util.Range<java.lang.Long!>? getEstimatedCaptureLatencyRange(androidx.camera.core.CameraSelector, int);
+ method public androidx.camera.core.CameraSelector getExtensionEnabledCameraSelector(androidx.camera.core.CameraSelector, int);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.extensions.ExtensionsManager!> getInstanceAsync(android.content.Context, androidx.camera.core.CameraProvider);
+ method public boolean isExtensionAvailable(androidx.camera.core.CameraSelector, int);
+ method public boolean isImageAnalysisSupported(androidx.camera.core.CameraSelector, int);
+ }
+
+}
+
diff --git a/camera/camera-extensions/api/res-1.4.0-beta01.txt b/camera/camera-extensions/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-extensions/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-extensions/api/restricted_1.4.0-beta01.txt b/camera/camera-extensions/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..f365e3d
--- /dev/null
+++ b/camera/camera-extensions/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.camera.extensions {
+
+ public interface CameraExtensionsControl {
+ method public default void setExtensionStrength(@IntRange(from=0, to=100) int);
+ }
+
+ public interface CameraExtensionsInfo {
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getCurrentExtensionMode();
+ method public default androidx.lifecycle.LiveData<java.lang.Integer!>? getExtensionStrength();
+ method public default boolean isCurrentExtensionModeAvailable();
+ method public default boolean isExtensionStrengthAvailable();
+ }
+
+ @RequiresApi(21) public final class ExtensionMode {
+ field public static final int AUTO = 5; // 0x5
+ field public static final int BOKEH = 1; // 0x1
+ field public static final int FACE_RETOUCH = 4; // 0x4
+ field public static final int HDR = 2; // 0x2
+ field public static final int NIGHT = 3; // 0x3
+ field public static final int NONE = 0; // 0x0
+ }
+
+ @RequiresApi(21) public final class ExtensionsManager {
+ method public androidx.camera.extensions.CameraExtensionsControl? getCameraExtensionsControl(androidx.camera.core.CameraControl);
+ method public androidx.camera.extensions.CameraExtensionsInfo getCameraExtensionsInfo(androidx.camera.core.CameraInfo);
+ method public android.util.Range<java.lang.Long!>? getEstimatedCaptureLatencyRange(androidx.camera.core.CameraSelector, int);
+ method public androidx.camera.core.CameraSelector getExtensionEnabledCameraSelector(androidx.camera.core.CameraSelector, int);
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.extensions.ExtensionsManager!> getInstanceAsync(android.content.Context, androidx.camera.core.CameraProvider);
+ method public boolean isExtensionAvailable(androidx.camera.core.CameraSelector, int);
+ method public boolean isImageAnalysisSupported(androidx.camera.core.CameraSelector, int);
+ }
+
+}
+
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index edcf69a..9c5d04ae 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -290,6 +290,7 @@
}
if (mPreviewProcessor != null) {
+ mPreviewProcessor.resume();
setImageProcessor(mPreviewOutputConfig.getId(),
new ImageProcessor() {
@Override
@@ -357,6 +358,9 @@
@Override
public void onCaptureSessionEnd() {
mOnEnableDisableSessionDurationCheck.onDisableSessionInvoked();
+ if (mPreviewProcessor != null) {
+ mPreviewProcessor.pause();
+ }
List<CaptureStageImpl> captureStages = new ArrayList<>();
CaptureStageImpl captureStage1 = mPreviewExtenderImpl.onDisableSession();
Logger.d(TAG, "preview onDisableSession: " + captureStage1);
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
index bf0f327..337a01e 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
@@ -52,12 +52,15 @@
class PreviewProcessor {
private static final String TAG = "PreviewProcessor";
@NonNull
- final PreviewImageProcessorImpl mPreviewImageProcessor;
+ private final PreviewImageProcessorImpl mPreviewImageProcessor;
@NonNull
- final CaptureResultImageMatcher mCaptureResultImageMatcher = new CaptureResultImageMatcher();
- final Object mLock = new Object();
+ private final CaptureResultImageMatcher mCaptureResultImageMatcher =
+ new CaptureResultImageMatcher();
+ private final Object mLock = new Object();
@GuardedBy("mLock")
- boolean mIsClosed = false;
+ private boolean mIsClosed = false;
+ @GuardedBy("mLock")
+ private boolean mIsPaused = false;
PreviewProcessor(@NonNull PreviewImageProcessorImpl previewImageProcessor,
@NonNull Surface previewOutputSurface, @NonNull Size surfaceSize) {
@@ -72,13 +75,25 @@
@NonNull List<Pair<CaptureResult.Key, Object>> result);
}
+ void pause() {
+ synchronized (mLock) {
+ mIsPaused = true;
+ }
+ }
+
+ void resume() {
+ synchronized (mLock) {
+ mIsPaused = false;
+ }
+ }
+
void start(@NonNull OnCaptureResultCallback onResultCallback) {
mCaptureResultImageMatcher.setImageReferenceListener(
(imageReference, totalCaptureResult, captureStageId) -> {
synchronized (mLock) {
- if (mIsClosed) {
+ if (mIsClosed || mIsPaused) {
imageReference.decrement();
- Logger.d(TAG, "Ignore image in closed state");
+ Logger.d(TAG, "Ignore image in closed or paused state");
return;
}
if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_3)
diff --git a/camera/camera-lifecycle/api/1.4.0-beta01.txt b/camera/camera-lifecycle/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..5dd135e
--- /dev/null
+++ b/camera/camera-lifecycle/api/1.4.0-beta01.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.camera.lifecycle {
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+ }
+
+ @RequiresApi(21) public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner, androidx.camera.core.CameraSelector, androidx.camera.core.UseCase!...);
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner, androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup);
+ method @MainThread public androidx.camera.core.ConcurrentCamera bindToLifecycle(java.util.List<androidx.camera.core.ConcurrentCamera.SingleCameraConfig!>);
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public static void configureInstance(androidx.camera.core.CameraXConfig);
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method public java.util.List<java.util.List<androidx.camera.core.CameraInfo!>!> getAvailableConcurrentCameraInfos();
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider!> getInstance(android.content.Context);
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ method public boolean isBound(androidx.camera.core.UseCase);
+ method @MainThread public boolean isConcurrentCameraModeOn();
+ method @VisibleForTesting public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> shutdownAsync();
+ method @MainThread public void unbind(androidx.camera.core.UseCase!...);
+ method @MainThread public void unbindAll();
+ }
+
+}
+
diff --git a/camera/camera-lifecycle/api/res-1.4.0-beta01.txt b/camera/camera-lifecycle/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-lifecycle/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-lifecycle/api/restricted_1.4.0-beta01.txt b/camera/camera-lifecycle/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..5dd135e
--- /dev/null
+++ b/camera/camera-lifecycle/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.camera.lifecycle {
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+ }
+
+ @RequiresApi(21) public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner, androidx.camera.core.CameraSelector, androidx.camera.core.UseCase!...);
+ method @MainThread public androidx.camera.core.Camera bindToLifecycle(androidx.lifecycle.LifecycleOwner, androidx.camera.core.CameraSelector, androidx.camera.core.UseCaseGroup);
+ method @MainThread public androidx.camera.core.ConcurrentCamera bindToLifecycle(java.util.List<androidx.camera.core.ConcurrentCamera.SingleCameraConfig!>);
+ method @SuppressCompatibility @androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration public static void configureInstance(androidx.camera.core.CameraXConfig);
+ method public java.util.List<androidx.camera.core.CameraInfo!> getAvailableCameraInfos();
+ method public java.util.List<java.util.List<androidx.camera.core.CameraInfo!>!> getAvailableConcurrentCameraInfos();
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.camera.lifecycle.ProcessCameraProvider!> getInstance(android.content.Context);
+ method public boolean hasCamera(androidx.camera.core.CameraSelector) throws androidx.camera.core.CameraInfoUnavailableException;
+ method public boolean isBound(androidx.camera.core.UseCase);
+ method @MainThread public boolean isConcurrentCameraModeOn();
+ method @VisibleForTesting public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> shutdownAsync();
+ method @MainThread public void unbind(androidx.camera.core.UseCase!...);
+ method @MainThread public void unbindAll();
+ }
+
+}
+
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index b39c18a..2c0a5a4 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -806,6 +806,43 @@
}
@Test
+ fun bindConcurrentPhysicalCamera_isBound() {
+ ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
+
+ runBlocking(MainScope().coroutineContext) {
+ provider = ProcessCameraProvider.getInstance(context).await()
+ val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+ val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _, _ -> }.build()
+
+ val singleCameraConfig0 = SingleCameraConfig(
+ CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build(),
+ UseCaseGroup.Builder()
+ .addUseCase(useCase0)
+ .build(),
+ lifecycleOwner0)
+ val singleCameraConfig1 = SingleCameraConfig(
+ CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build(),
+ UseCaseGroup.Builder()
+ .addUseCase(useCase1)
+ .build(),
+ lifecycleOwner0)
+
+ val concurrentCamera = provider.bindToLifecycle(
+ listOf(singleCameraConfig0, singleCameraConfig1))
+
+ assertThat(concurrentCamera).isNotNull()
+ assertThat(concurrentCamera.cameras.size).isEqualTo(1)
+ assertThat(provider.isBound(useCase0)).isTrue()
+ assertThat(provider.isBound(useCase1)).isTrue()
+ assertThat(provider.isConcurrentCameraModeOn).isFalse()
+ }
+ }
+
+ @Test
fun bindConcurrentCameraTwice_isBound() {
ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
@@ -882,15 +919,8 @@
.build(),
lifecycleOwner0)
- if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
- assertThrows<IllegalArgumentException> {
- provider.bindToLifecycle(listOf(singleCameraConfig0))
- }
- assertThat(provider.isConcurrentCameraModeOn).isFalse()
- } else {
- assertThrows<UnsupportedOperationException> {
- provider.bindToLifecycle(listOf(singleCameraConfig0))
- }
+ assertThrows<IllegalArgumentException> {
+ provider.bindToLifecycle(listOf(singleCameraConfig0))
}
}
}
@@ -923,17 +953,9 @@
.build(),
lifecycleOwner1)
- if (context.packageManager.hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
- assertThrows<IllegalArgumentException> {
- provider.bindToLifecycle(
- listOf(singleCameraConfig0, singleCameraConfig1, singleCameraConfig2))
- }
- assertThat(provider.isConcurrentCameraModeOn).isFalse()
- } else {
- assertThrows<java.lang.UnsupportedOperationException> {
- provider.bindToLifecycle(
- listOf(singleCameraConfig0, singleCameraConfig1, singleCameraConfig2))
- }
+ assertThrows<java.lang.IllegalArgumentException> {
+ provider.bindToLifecycle(
+ listOf(singleCameraConfig0, singleCameraConfig1, singleCameraConfig2))
}
}
}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 536684e..11038bb 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -82,6 +82,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -453,16 +454,6 @@
@MainThread
@NonNull
public ConcurrentCamera bindToLifecycle(@NonNull List<SingleCameraConfig> singleCameraConfigs) {
- if (!mContext.getPackageManager().hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
- throw new UnsupportedOperationException("Concurrent camera is not supported on the "
- + "device");
- }
-
- if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_SINGLE) {
- throw new UnsupportedOperationException("Camera is already running, call "
- + "unbindAll() before binding more cameras");
- }
-
if (singleCameraConfigs.size() < 2) {
throw new IllegalArgumentException("Concurrent camera needs two camera configs");
}
@@ -472,37 +463,81 @@
+ "cameras at maximum.");
}
- List<CameraInfo> cameraInfosToBind = new ArrayList<>();
- CameraInfo firstCameraInfo;
- CameraInfo secondCameraInfo;
- try {
- firstCameraInfo = getCameraInfo(
- singleCameraConfigs.get(0).getCameraSelector());
- secondCameraInfo = getCameraInfo(
- singleCameraConfigs.get(1).getCameraSelector());
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Invalid camera selectors in camera configs");
- }
- cameraInfosToBind.add(firstCameraInfo);
- cameraInfosToBind.add(secondCameraInfo);
- if (!getActiveConcurrentCameraInfos().isEmpty()
- && !cameraInfosToBind.equals(getActiveConcurrentCameraInfos())) {
- throw new UnsupportedOperationException("Cameras are already running, call "
- + "unbindAll() before binding more cameras");
- }
-
- setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
List<Camera> cameras = new ArrayList<>();
- for (SingleCameraConfig config : singleCameraConfigs) {
- Camera camera = bindToLifecycle(
- config.getLifecycleOwner(),
- config.getCameraSelector(),
- config.getUseCaseGroup().getViewPort(),
- config.getUseCaseGroup().getEffects(),
- config.getUseCaseGroup().getUseCases().toArray(new UseCase[0]));
+ if (Objects.equals(singleCameraConfigs.get(0).getCameraSelector().getLensFacing(),
+ singleCameraConfigs.get(1).getCameraSelector().getLensFacing())) {
+ if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT) {
+ throw new UnsupportedOperationException("Camera is already running, call "
+ + "unbindAll() before binding more cameras");
+ }
+ if (!Objects.equals(singleCameraConfigs.get(0).getLifecycleOwner(),
+ singleCameraConfigs.get(1).getLifecycleOwner())
+ || !Objects.equals(singleCameraConfigs.get(0).getUseCaseGroup().getViewPort(),
+ singleCameraConfigs.get(1).getUseCaseGroup().getViewPort())
+ || !Objects.equals(singleCameraConfigs.get(0).getUseCaseGroup().getEffects(),
+ singleCameraConfigs.get(1).getUseCaseGroup().getEffects())) {
+ throw new IllegalArgumentException("Two camera configs need to have the same "
+ + "lifecycle owner, view port and effects");
+ }
+ LifecycleOwner lifecycleOwner = singleCameraConfigs.get(0).getLifecycleOwner();
+ CameraSelector cameraSelector = singleCameraConfigs.get(0).getCameraSelector();
+ ViewPort viewPort = singleCameraConfigs.get(0).getUseCaseGroup().getViewPort();
+ List<CameraEffect> effects = singleCameraConfigs.get(0).getUseCaseGroup().getEffects();
+ List<UseCase> useCases = new ArrayList<>();
+ for (SingleCameraConfig config : singleCameraConfigs) {
+ // Connect physical camera id with use case
+ for (UseCase useCase : config.getUseCaseGroup().getUseCases()) {
+ useCase.setPhysicalCameraId(config.getCameraSelector().getPhysicalCameraId());
+ }
+ useCases.addAll(config.getUseCaseGroup().getUseCases());
+ }
+
+ setCameraOperatingMode(CAMERA_OPERATING_MODE_SINGLE);
+ Camera camera = bindToLifecycle(lifecycleOwner, cameraSelector, viewPort,
+ effects, useCases.toArray(new UseCase[0]));
cameras.add(camera);
+ } else {
+ if (!mContext.getPackageManager().hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
+ throw new UnsupportedOperationException("Concurrent camera is not supported on the "
+ + "device");
+ }
+
+ if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_SINGLE) {
+ throw new UnsupportedOperationException("Camera is already running, call "
+ + "unbindAll() before binding more cameras");
+ }
+
+ List<CameraInfo> cameraInfosToBind = new ArrayList<>();
+ CameraInfo firstCameraInfo;
+ CameraInfo secondCameraInfo;
+ try {
+ firstCameraInfo = getCameraInfo(
+ singleCameraConfigs.get(0).getCameraSelector());
+ secondCameraInfo = getCameraInfo(
+ singleCameraConfigs.get(1).getCameraSelector());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid camera selectors in camera configs");
+ }
+ cameraInfosToBind.add(firstCameraInfo);
+ cameraInfosToBind.add(secondCameraInfo);
+ if (!getActiveConcurrentCameraInfos().isEmpty()
+ && !cameraInfosToBind.equals(getActiveConcurrentCameraInfos())) {
+ throw new UnsupportedOperationException("Cameras are already running, call "
+ + "unbindAll() before binding more cameras");
+ }
+
+ setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+ for (SingleCameraConfig config : singleCameraConfigs) {
+ Camera camera = bindToLifecycle(
+ config.getLifecycleOwner(),
+ config.getCameraSelector(),
+ config.getUseCaseGroup().getViewPort(),
+ config.getUseCaseGroup().getEffects(),
+ config.getUseCaseGroup().getUseCases().toArray(new UseCase[0]));
+ cameras.add(camera);
+ }
+ setActiveConcurrentCameraInfos(cameraInfosToBind);
}
- setActiveConcurrentCameraInfos(cameraInfosToBind);
return new ConcurrentCamera(cameras);
}
diff --git a/camera/camera-mlkit-vision/api/1.4.0-beta01.txt b/camera/camera-mlkit-vision/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..0599c25
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/1.4.0-beta01.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.camera.mlkit.vision {
+
+ @RequiresApi(21) public class MlKitAnalyzer implements androidx.camera.core.ImageAnalysis.Analyzer {
+ ctor public MlKitAnalyzer(java.util.List<com.google.mlkit.vision.interfaces.Detector<?>!>, int, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.mlkit.vision.MlKitAnalyzer.Result!>);
+ method public final void analyze(androidx.camera.core.ImageProxy);
+ method public final android.util.Size getDefaultTargetResolution();
+ method public final int getTargetCoordinateSystem();
+ method public final void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class MlKitAnalyzer.Result {
+ ctor public MlKitAnalyzer.Result(java.util.Map<com.google.mlkit.vision.interfaces.Detector<?>!,java.lang.Object!>, long, java.util.Map<com.google.mlkit.vision.interfaces.Detector<?>!,java.lang.Throwable!>);
+ method public Throwable? getThrowable(com.google.mlkit.vision.interfaces.Detector<?>);
+ method public long getTimestamp();
+ method public <T> T? getValue(com.google.mlkit.vision.interfaces.Detector<T!>);
+ }
+
+}
+
diff --git a/camera/camera-mlkit-vision/api/res-1.4.0-beta01.txt b/camera/camera-mlkit-vision/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-mlkit-vision/api/restricted_1.4.0-beta01.txt b/camera/camera-mlkit-vision/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..0599c25
--- /dev/null
+++ b/camera/camera-mlkit-vision/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,20 @@
+// Signature format: 4.0
+package androidx.camera.mlkit.vision {
+
+ @RequiresApi(21) public class MlKitAnalyzer implements androidx.camera.core.ImageAnalysis.Analyzer {
+ ctor public MlKitAnalyzer(java.util.List<com.google.mlkit.vision.interfaces.Detector<?>!>, int, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.mlkit.vision.MlKitAnalyzer.Result!>);
+ method public final void analyze(androidx.camera.core.ImageProxy);
+ method public final android.util.Size getDefaultTargetResolution();
+ method public final int getTargetCoordinateSystem();
+ method public final void updateTransform(android.graphics.Matrix?);
+ }
+
+ public static final class MlKitAnalyzer.Result {
+ ctor public MlKitAnalyzer.Result(java.util.Map<com.google.mlkit.vision.interfaces.Detector<?>!,java.lang.Object!>, long, java.util.Map<com.google.mlkit.vision.interfaces.Detector<?>!,java.lang.Throwable!>);
+ method public Throwable? getThrowable(com.google.mlkit.vision.interfaces.Detector<?>);
+ method public long getTimestamp();
+ method public <T> T? getValue(com.google.mlkit.vision.interfaces.Detector<T!>);
+ }
+
+}
+
diff --git a/camera/camera-video/api/1.4.0-beta01.txt b/camera/camera-video/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..405c299
--- /dev/null
+++ b/camera/camera-video/api/1.4.0-beta01.txt
@@ -0,0 +1,220 @@
+// Signature format: 4.0
+package androidx.camera.video {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
+ method public abstract int getAudioState();
+ method public abstract Throwable? getErrorCause();
+ method public boolean hasAudio();
+ method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
+ field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
+ field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
+ field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_MUTED = 5; // 0x5
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
+ field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
+ }
+
+ @RequiresApi(21) public class FallbackStrategy {
+ method public static androidx.camera.video.FallbackStrategy higherQualityOrLowerThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy higherQualityThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityOrHigherThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityThan(androidx.camera.video.Quality);
+ }
+
+ @RequiresApi(21) public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
+ }
+
+ @RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
+ ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
+ method public androidx.camera.video.FileDescriptorOutputOptions build();
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
+ method public java.io.File getFile();
+ }
+
+ @RequiresApi(21) public static final class FileOutputOptions.Builder {
+ ctor public FileOutputOptions.Builder(java.io.File);
+ method public androidx.camera.video.FileOutputOptions build();
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.net.Uri getCollectionUri();
+ method public android.content.ContentResolver getContentResolver();
+ method public android.content.ContentValues getContentValues();
+ field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
+ }
+
+ public static final class MediaStoreOutputOptions.Builder {
+ ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
+ method public androidx.camera.video.MediaStoreOutputOptions build();
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public abstract class OutputOptions {
+ method @IntRange(from=0) public long getDurationLimitMillis();
+ method @IntRange(from=0) public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
+ field public static final int DURATION_UNLIMITED = 0; // 0x0
+ field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class OutputResults {
+ ctor public OutputResults();
+ method public abstract android.net.Uri getOutputUri();
+ }
+
+ @RequiresApi(21) public final class PendingRecording {
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ }
+
+ @RequiresApi(21) public class Quality {
+ field public static final androidx.camera.video.Quality FHD;
+ field public static final androidx.camera.video.Quality HD;
+ field public static final androidx.camera.video.Quality HIGHEST;
+ field public static final androidx.camera.video.Quality LOWEST;
+ field public static final androidx.camera.video.Quality SD;
+ field public static final androidx.camera.video.Quality UHD;
+ }
+
+ @RequiresApi(21) public final class QualitySelector {
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality);
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality, androidx.camera.video.FallbackStrategy);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>, androidx.camera.video.FallbackStrategy);
+ method public static android.util.Size? getResolution(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ method @Deprecated public static java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.CameraInfo);
+ method @Deprecated public static boolean isQualitySupported(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ }
+
+ @RequiresApi(21) public final class Recorder implements androidx.camera.video.VideoOutput {
+ method public int getAspectRatio();
+ method public java.util.concurrent.Executor? getExecutor();
+ method public androidx.camera.video.QualitySelector getQualitySelector();
+ method public int getTargetVideoEncodingBitRate();
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo);
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo, int);
+ method public int getVideoCapabilitiesSource();
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ method @RequiresApi(26) public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileDescriptorOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.MediaStoreOutputOptions);
+ field public static final androidx.camera.video.QualitySelector DEFAULT_QUALITY_SELECTOR;
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE = 0; // 0x0
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES = 1; // 0x1
+ }
+
+ @RequiresApi(21) public static final class Recorder.Builder {
+ ctor public Recorder.Builder();
+ method public androidx.camera.video.Recorder build();
+ method public androidx.camera.video.Recorder.Builder setAspectRatio(int);
+ method public androidx.camera.video.Recorder.Builder setExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.video.Recorder.Builder setQualitySelector(androidx.camera.video.QualitySelector);
+ method public androidx.camera.video.Recorder.Builder setTargetVideoEncodingBitRate(@IntRange(from=1) int);
+ method public androidx.camera.video.Recorder.Builder setVideoCapabilitiesSource(int);
+ }
+
+ @RequiresApi(21) public final class Recording implements java.lang.AutoCloseable {
+ method public void close();
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public boolean isPersistent();
+ method public void mute(boolean);
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class RecordingStats {
+ method public abstract androidx.camera.video.AudioStats getAudioStats();
+ method public abstract long getNumBytesRecorded();
+ method public abstract long getRecordedDurationNanos();
+ }
+
+ @RequiresApi(21) public interface VideoCapabilities {
+ method public java.util.Set<androidx.camera.core.DynamicRange!> getSupportedDynamicRanges();
+ method public java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.DynamicRange);
+ method public boolean isQualitySupported(androidx.camera.video.Quality, androidx.camera.core.DynamicRange);
+ method public default boolean isStabilizationSupported();
+ }
+
+ @RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public int getMirrorMode();
+ method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isVideoStabilizationEnabled();
+ method public void setTargetRotation(int);
+ method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
+ }
+
+ @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
+ ctor public VideoCapture.Builder(T);
+ method public androidx.camera.video.VideoCapture<T!> build();
+ method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setVideoStabilizationEnabled(boolean);
+ }
+
+ @RequiresApi(21) public interface VideoOutput {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ @RequiresApi(21) public abstract class VideoRecordEvent {
+ method public androidx.camera.video.OutputOptions getOutputOptions();
+ method public androidx.camera.video.RecordingStats getRecordingStats();
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Finalize extends androidx.camera.video.VideoRecordEvent {
+ method public Throwable? getCause();
+ method public int getError();
+ method public androidx.camera.video.OutputResults getOutputResults();
+ method public boolean hasError();
+ field public static final int ERROR_DURATION_LIMIT_REACHED = 9; // 0x9
+ field public static final int ERROR_ENCODING_FAILED = 6; // 0x6
+ field public static final int ERROR_FILE_SIZE_LIMIT_REACHED = 2; // 0x2
+ field public static final int ERROR_INSUFFICIENT_STORAGE = 3; // 0x3
+ field public static final int ERROR_INVALID_OUTPUT_OPTIONS = 5; // 0x5
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NO_VALID_DATA = 8; // 0x8
+ field public static final int ERROR_RECORDER_ERROR = 7; // 0x7
+ field public static final int ERROR_RECORDING_GARBAGE_COLLECTED = 10; // 0xa
+ field public static final int ERROR_SOURCE_INACTIVE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Pause extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Resume extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Start extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Status extends androidx.camera.video.VideoRecordEvent {
+ }
+
+}
+
diff --git a/camera/camera-video/api/res-1.4.0-beta01.txt b/camera/camera-video/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-video/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-video/api/restricted_1.4.0-beta01.txt b/camera/camera-video/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..405c299
--- /dev/null
+++ b/camera/camera-video/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,220 @@
+// Signature format: 4.0
+package androidx.camera.video {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+ method public double getAudioAmplitude();
+ method public abstract int getAudioState();
+ method public abstract Throwable? getErrorCause();
+ method public boolean hasAudio();
+ method public boolean hasError();
+ field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
+ field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
+ field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
+ field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_MUTED = 5; // 0x5
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
+ field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+ }
+
+ @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
+ }
+
+ @RequiresApi(21) public class FallbackStrategy {
+ method public static androidx.camera.video.FallbackStrategy higherQualityOrLowerThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy higherQualityThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityOrHigherThan(androidx.camera.video.Quality);
+ method public static androidx.camera.video.FallbackStrategy lowerQualityThan(androidx.camera.video.Quality);
+ }
+
+ @RequiresApi(21) public final class FileDescriptorOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
+ }
+
+ @RequiresApi(21) public static final class FileDescriptorOutputOptions.Builder {
+ ctor public FileDescriptorOutputOptions.Builder(android.os.ParcelFileDescriptor);
+ method public androidx.camera.video.FileDescriptorOutputOptions build();
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileDescriptorOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public final class FileOutputOptions extends androidx.camera.video.OutputOptions {
+ method public java.io.File getFile();
+ }
+
+ @RequiresApi(21) public static final class FileOutputOptions.Builder {
+ ctor public FileOutputOptions.Builder(java.io.File);
+ method public androidx.camera.video.FileOutputOptions build();
+ method public androidx.camera.video.FileOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.FileOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public final class MediaStoreOutputOptions extends androidx.camera.video.OutputOptions {
+ method public android.net.Uri getCollectionUri();
+ method public android.content.ContentResolver getContentResolver();
+ method public android.content.ContentValues getContentValues();
+ field public static final android.content.ContentValues EMPTY_CONTENT_VALUES;
+ }
+
+ public static final class MediaStoreOutputOptions.Builder {
+ ctor public MediaStoreOutputOptions.Builder(android.content.ContentResolver, android.net.Uri);
+ method public androidx.camera.video.MediaStoreOutputOptions build();
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setContentValues(android.content.ContentValues);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setDurationLimitMillis(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setFileSizeLimit(@IntRange(from=0) long);
+ method public androidx.camera.video.MediaStoreOutputOptions.Builder setLocation(android.location.Location?);
+ }
+
+ @RequiresApi(21) public abstract class OutputOptions {
+ method @IntRange(from=0) public long getDurationLimitMillis();
+ method @IntRange(from=0) public long getFileSizeLimit();
+ method public android.location.Location? getLocation();
+ field public static final int DURATION_UNLIMITED = 0; // 0x0
+ field public static final int FILE_SIZE_UNLIMITED = 0; // 0x0
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class OutputResults {
+ ctor public OutputResults();
+ method public abstract android.net.Uri getOutputUri();
+ }
+
+ @RequiresApi(21) public final class PendingRecording {
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public androidx.camera.video.PendingRecording asPersistentRecording();
+ method @CheckResult public androidx.camera.video.Recording start(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public androidx.camera.video.PendingRecording withAudioEnabled();
+ }
+
+ @RequiresApi(21) public class Quality {
+ field public static final androidx.camera.video.Quality FHD;
+ field public static final androidx.camera.video.Quality HD;
+ field public static final androidx.camera.video.Quality HIGHEST;
+ field public static final androidx.camera.video.Quality LOWEST;
+ field public static final androidx.camera.video.Quality SD;
+ field public static final androidx.camera.video.Quality UHD;
+ }
+
+ @RequiresApi(21) public final class QualitySelector {
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality);
+ method public static androidx.camera.video.QualitySelector from(androidx.camera.video.Quality, androidx.camera.video.FallbackStrategy);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>);
+ method public static androidx.camera.video.QualitySelector fromOrderedList(java.util.List<androidx.camera.video.Quality!>, androidx.camera.video.FallbackStrategy);
+ method public static android.util.Size? getResolution(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ method @Deprecated public static java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.CameraInfo);
+ method @Deprecated public static boolean isQualitySupported(androidx.camera.core.CameraInfo, androidx.camera.video.Quality);
+ }
+
+ @RequiresApi(21) public final class Recorder implements androidx.camera.video.VideoOutput {
+ method public int getAspectRatio();
+ method public java.util.concurrent.Executor? getExecutor();
+ method public androidx.camera.video.QualitySelector getQualitySelector();
+ method public int getTargetVideoEncodingBitRate();
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo);
+ method public static androidx.camera.video.VideoCapabilities getVideoCapabilities(androidx.camera.core.CameraInfo, int);
+ method public int getVideoCapabilitiesSource();
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ method @RequiresApi(26) public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileDescriptorOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.FileOutputOptions);
+ method public androidx.camera.video.PendingRecording prepareRecording(android.content.Context, androidx.camera.video.MediaStoreOutputOptions);
+ field public static final androidx.camera.video.QualitySelector DEFAULT_QUALITY_SELECTOR;
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE = 0; // 0x0
+ field public static final int VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES = 1; // 0x1
+ }
+
+ @RequiresApi(21) public static final class Recorder.Builder {
+ ctor public Recorder.Builder();
+ method public androidx.camera.video.Recorder build();
+ method public androidx.camera.video.Recorder.Builder setAspectRatio(int);
+ method public androidx.camera.video.Recorder.Builder setExecutor(java.util.concurrent.Executor);
+ method public androidx.camera.video.Recorder.Builder setQualitySelector(androidx.camera.video.QualitySelector);
+ method public androidx.camera.video.Recorder.Builder setTargetVideoEncodingBitRate(@IntRange(from=1) int);
+ method public androidx.camera.video.Recorder.Builder setVideoCapabilitiesSource(int);
+ }
+
+ @RequiresApi(21) public final class Recording implements java.lang.AutoCloseable {
+ method public void close();
+ method @SuppressCompatibility @androidx.camera.video.ExperimentalPersistentRecording public boolean isPersistent();
+ method public void mute(boolean);
+ method public void pause();
+ method public void resume();
+ method public void stop();
+ }
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class RecordingStats {
+ method public abstract androidx.camera.video.AudioStats getAudioStats();
+ method public abstract long getNumBytesRecorded();
+ method public abstract long getRecordedDurationNanos();
+ }
+
+ @RequiresApi(21) public interface VideoCapabilities {
+ method public java.util.Set<androidx.camera.core.DynamicRange!> getSupportedDynamicRanges();
+ method public java.util.List<androidx.camera.video.Quality!> getSupportedQualities(androidx.camera.core.DynamicRange);
+ method public boolean isQualitySupported(androidx.camera.video.Quality, androidx.camera.core.DynamicRange);
+ method public default boolean isStabilizationSupported();
+ }
+
+ @RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public androidx.camera.core.DynamicRange getDynamicRange();
+ method public int getMirrorMode();
+ method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFrameRate();
+ method public int getTargetRotation();
+ method public boolean isVideoStabilizationEnabled();
+ method public void setTargetRotation(int);
+ method public static <T extends androidx.camera.video.VideoOutput> androidx.camera.video.VideoCapture<T!> withOutput(T);
+ }
+
+ @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture!> {
+ ctor public VideoCapture.Builder(T);
+ method public androidx.camera.video.VideoCapture<T!> build();
+ method public androidx.camera.video.VideoCapture.Builder<T!> setDynamicRange(androidx.camera.core.DynamicRange);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
+ method public androidx.camera.video.VideoCapture.Builder<T!> setVideoStabilizationEnabled(boolean);
+ }
+
+ @RequiresApi(21) public interface VideoOutput {
+ method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
+ }
+
+ @RequiresApi(21) public abstract class VideoRecordEvent {
+ method public androidx.camera.video.OutputOptions getOutputOptions();
+ method public androidx.camera.video.RecordingStats getRecordingStats();
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Finalize extends androidx.camera.video.VideoRecordEvent {
+ method public Throwable? getCause();
+ method public int getError();
+ method public androidx.camera.video.OutputResults getOutputResults();
+ method public boolean hasError();
+ field public static final int ERROR_DURATION_LIMIT_REACHED = 9; // 0x9
+ field public static final int ERROR_ENCODING_FAILED = 6; // 0x6
+ field public static final int ERROR_FILE_SIZE_LIMIT_REACHED = 2; // 0x2
+ field public static final int ERROR_INSUFFICIENT_STORAGE = 3; // 0x3
+ field public static final int ERROR_INVALID_OUTPUT_OPTIONS = 5; // 0x5
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NO_VALID_DATA = 8; // 0x8
+ field public static final int ERROR_RECORDER_ERROR = 7; // 0x7
+ field public static final int ERROR_RECORDING_GARBAGE_COLLECTED = 10; // 0xa
+ field public static final int ERROR_SOURCE_INACTIVE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Pause extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Resume extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Start extends androidx.camera.video.VideoRecordEvent {
+ }
+
+ @RequiresApi(21) public static final class VideoRecordEvent.Status extends androidx.camera.video.VideoRecordEvent {
+ }
+
+}
+
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index aa18150..360497e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -884,7 +884,7 @@
DynamicRange dynamicRange = streamSpec.getDynamicRange();
if (!isStreamError && mDeferrableSurface != null) {
if (isStreamActive) {
- sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange);
+ sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange, null);
} else {
sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface, dynamicRange);
}
diff --git a/camera/camera-view/api/1.4.0-beta01.txt b/camera/camera-view/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..ea8f480
--- /dev/null
+++ b/camera/camera-view/api/1.4.0-beta01.txt
@@ -0,0 +1,199 @@
+// Signature format: 4.0
+package androidx.camera.view {
+
+ @RequiresApi(21) public abstract class CameraController {
+ method @MainThread public void clearEffects();
+ method @MainThread public void clearImageAnalysisAnalyzer();
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
+ method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
+ method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
+ method @MainThread public int getImageAnalysisBackpressureStrategy();
+ method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public int getImageAnalysisOutputImageFormat();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageAnalysisResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
+ method @MainThread public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageCaptureResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getPreviewResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.core.DynamicRange getVideoCaptureDynamicRange();
+ method @MainThread public int getVideoCaptureMirrorMode();
+ method @MainThread public androidx.camera.video.QualitySelector getVideoCaptureQualitySelector();
+ method @MainThread public android.util.Range<java.lang.Integer!> getVideoCaptureTargetFrameRate();
+ method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
+ method @MainThread public boolean isImageAnalysisEnabled();
+ method @MainThread public boolean isImageCaptureEnabled();
+ method @MainThread public boolean isPinchToZoomEnabled();
+ method @MainThread public boolean isRecording();
+ method @MainThread public boolean isTapToFocusEnabled();
+ method @MainThread public boolean isVideoCaptureEnabled();
+ method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
+ method @MainThread public void setEffects(java.util.Set<androidx.camera.core.CameraEffect!>);
+ method @MainThread public void setEnabledUseCases(int);
+ method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageAnalysisBackpressureStrategy(int);
+ method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisOutputImageFormat(int);
+ method @MainThread public void setImageAnalysisResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setImageCaptureFlashMode(int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(int);
+ method @MainThread public void setImageCaptureResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureDynamicRange(androidx.camera.core.DynamicRange);
+ method @MainThread public void setVideoCaptureMirrorMode(int);
+ method @MainThread public void setVideoCaptureQualitySelector(androidx.camera.video.QualitySelector);
+ method @MainThread public void setVideoCaptureTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method @MainThread @RequiresApi(26) public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int IMAGE_ANALYSIS = 2; // 0x2
+ field public static final int IMAGE_CAPTURE = 1; // 0x1
+ field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
+ field public static final int TAP_TO_FOCUS_FOCUSED = 2; // 0x2
+ field public static final int TAP_TO_FOCUS_NOT_FOCUSED = 3; // 0x3
+ field public static final int TAP_TO_FOCUS_NOT_STARTED = 0; // 0x0
+ field public static final int TAP_TO_FOCUS_STARTED = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 4; // 0x4
+ }
+
+ @Deprecated @RequiresApi(21) public static final class CameraController.OutputSize {
+ ctor @Deprecated public CameraController.OutputSize(android.util.Size);
+ ctor @Deprecated public CameraController.OutputSize(int);
+ method @Deprecated public int getAspectRatio();
+ method @Deprecated public android.util.Size? getResolution();
+ field @Deprecated public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
+ @RequiresApi(21) public final class LifecycleCameraController extends androidx.camera.view.CameraController {
+ ctor public LifecycleCameraController(android.content.Context);
+ method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
+ method @MainThread public void unbind();
+ }
+
+ @RequiresApi(21) public final class PreviewView extends android.widget.FrameLayout {
+ ctor @UiThread public PreviewView(android.content.Context);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public android.graphics.Bitmap? getBitmap();
+ method @UiThread public androidx.camera.view.CameraController? getController();
+ method @UiThread public androidx.camera.view.PreviewView.ImplementationMode getImplementationMode();
+ method @UiThread public androidx.camera.core.MeteringPointFactory getMeteringPointFactory();
+ method @SuppressCompatibility public androidx.camera.view.transform.OutputTransform? getOutputTransform();
+ method public androidx.lifecycle.LiveData<androidx.camera.view.PreviewView.StreamState!> getPreviewStreamState();
+ method @UiThread public androidx.camera.view.PreviewView.ScaleType getScaleType();
+ method @UiThread public android.graphics.Matrix? getSensorToViewTransform();
+ method @UiThread public androidx.camera.core.Preview.SurfaceProvider getSurfaceProvider();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort(int);
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setImplementationMode(androidx.camera.view.PreviewView.ImplementationMode);
+ method @UiThread public void setScaleType(androidx.camera.view.PreviewView.ScaleType);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+ @RequiresApi(21) public enum PreviewView.ImplementationMode {
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode PERFORMANCE;
+ }
+
+ @RequiresApi(21) public enum PreviewView.ScaleType {
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_START;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_START;
+ }
+
+ public enum PreviewView.StreamState {
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState IDLE;
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState STREAMING;
+ }
+
+ @RequiresApi(21) public final class RotationProvider {
+ ctor public RotationProvider(android.content.Context);
+ method @CheckResult public boolean addListener(java.util.concurrent.Executor, androidx.camera.view.RotationProvider.Listener);
+ method public void removeListener(androidx.camera.view.RotationProvider.Listener);
+ }
+
+ public static interface RotationProvider.Listener {
+ method public void onRotationChanged(int);
+ }
+
+ @RequiresApi(21) public final class ScreenFlashView extends android.view.View {
+ ctor @UiThread public ScreenFlashView(android.content.Context);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+}
+
+package androidx.camera.view.transform {
+
+ @SuppressCompatibility @RequiresApi(21) public final class CoordinateTransform {
+ ctor public CoordinateTransform(androidx.camera.view.transform.OutputTransform, androidx.camera.view.transform.OutputTransform);
+ method public void mapPoint(android.graphics.PointF);
+ method public void mapPoints(float[]);
+ method public void mapRect(android.graphics.RectF);
+ method public void transform(android.graphics.Matrix);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class FileTransformFactory {
+ ctor public FileTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(android.content.ContentResolver, android.net.Uri) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.File) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.InputStream) throws java.io.IOException;
+ method public boolean isUsingExifOrientation();
+ method public void setUsingExifOrientation(boolean);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class ImageProxyTransformFactory {
+ ctor public ImageProxyTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(androidx.camera.core.ImageProxy);
+ method public boolean isUsingCropRect();
+ method public boolean isUsingRotationDegrees();
+ method public void setUsingCropRect(boolean);
+ method public void setUsingRotationDegrees(boolean);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class OutputTransform {
+ }
+
+}
+
+package androidx.camera.view.video {
+
+ @RequiresApi(21) public class AudioConfig {
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+ method public boolean getAudioEnabled();
+ field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+ }
+
+}
+
diff --git a/camera/camera-view/api/res-1.4.0-beta01.txt b/camera/camera-view/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-view/api/res-1.4.0-beta01.txt
diff --git a/camera/camera-view/api/restricted_1.4.0-beta01.txt b/camera/camera-view/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..ea8f480
--- /dev/null
+++ b/camera/camera-view/api/restricted_1.4.0-beta01.txt
@@ -0,0 +1,199 @@
+// Signature format: 4.0
+package androidx.camera.view {
+
+ @RequiresApi(21) public abstract class CameraController {
+ method @MainThread public void clearEffects();
+ method @MainThread public void clearImageAnalysisAnalyzer();
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @MainThread public androidx.camera.core.CameraControl? getCameraControl();
+ method @MainThread public androidx.camera.core.CameraInfo? getCameraInfo();
+ method @MainThread public androidx.camera.core.CameraSelector getCameraSelector();
+ method @MainThread public java.util.concurrent.Executor? getImageAnalysisBackgroundExecutor();
+ method @MainThread public int getImageAnalysisBackpressureStrategy();
+ method @MainThread public int getImageAnalysisImageQueueDepth();
+ method @MainThread public int getImageAnalysisOutputImageFormat();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageAnalysisResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageAnalysisTargetSize();
+ method @MainThread public int getImageCaptureFlashMode();
+ method @MainThread public java.util.concurrent.Executor? getImageCaptureIoExecutor();
+ method @MainThread public int getImageCaptureMode();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getImageCaptureResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getImageCaptureTargetSize();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> getInitializationFuture();
+ method @MainThread public androidx.camera.core.resolutionselector.ResolutionSelector? getPreviewResolutionSelector();
+ method @Deprecated @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
+ method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
+ method @MainThread public androidx.camera.core.DynamicRange getVideoCaptureDynamicRange();
+ method @MainThread public int getVideoCaptureMirrorMode();
+ method @MainThread public androidx.camera.video.QualitySelector getVideoCaptureQualitySelector();
+ method @MainThread public android.util.Range<java.lang.Integer!> getVideoCaptureTargetFrameRate();
+ method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
+ method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
+ method @MainThread public boolean isImageAnalysisEnabled();
+ method @MainThread public boolean isImageCaptureEnabled();
+ method @MainThread public boolean isPinchToZoomEnabled();
+ method @MainThread public boolean isRecording();
+ method @MainThread public boolean isTapToFocusEnabled();
+ method @MainThread public boolean isVideoCaptureEnabled();
+ method @MainThread public void setCameraSelector(androidx.camera.core.CameraSelector);
+ method @MainThread public void setEffects(java.util.Set<androidx.camera.core.CameraEffect!>);
+ method @MainThread public void setEnabledUseCases(int);
+ method @MainThread public void setImageAnalysisAnalyzer(java.util.concurrent.Executor, androidx.camera.core.ImageAnalysis.Analyzer);
+ method @MainThread public void setImageAnalysisBackgroundExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageAnalysisBackpressureStrategy(int);
+ method @MainThread public void setImageAnalysisImageQueueDepth(int);
+ method @MainThread public void setImageAnalysisOutputImageFormat(int);
+ method @MainThread public void setImageAnalysisResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageAnalysisTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setImageCaptureFlashMode(int);
+ method @MainThread public void setImageCaptureIoExecutor(java.util.concurrent.Executor?);
+ method @MainThread public void setImageCaptureMode(int);
+ method @MainThread public void setImageCaptureResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setImageCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
+ method @MainThread public void setPinchToZoomEnabled(boolean);
+ method @MainThread public void setPreviewResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector?);
+ method @Deprecated @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
+ method @MainThread public void setTapToFocusEnabled(boolean);
+ method @MainThread public void setVideoCaptureDynamicRange(androidx.camera.core.DynamicRange);
+ method @MainThread public void setVideoCaptureMirrorMode(int);
+ method @MainThread public void setVideoCaptureQualitySelector(androidx.camera.video.QualitySelector);
+ method @MainThread public void setVideoCaptureTargetFrameRate(android.util.Range<java.lang.Integer!>);
+ method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
+ method @MainThread @RequiresApi(26) public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
+ method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field public static final int IMAGE_ANALYSIS = 2; // 0x2
+ field public static final int IMAGE_CAPTURE = 1; // 0x1
+ field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
+ field public static final int TAP_TO_FOCUS_FOCUSED = 2; // 0x2
+ field public static final int TAP_TO_FOCUS_NOT_FOCUSED = 3; // 0x3
+ field public static final int TAP_TO_FOCUS_NOT_STARTED = 0; // 0x0
+ field public static final int TAP_TO_FOCUS_STARTED = 1; // 0x1
+ field public static final int VIDEO_CAPTURE = 4; // 0x4
+ }
+
+ @Deprecated @RequiresApi(21) public static final class CameraController.OutputSize {
+ ctor @Deprecated public CameraController.OutputSize(android.util.Size);
+ ctor @Deprecated public CameraController.OutputSize(int);
+ method @Deprecated public int getAspectRatio();
+ method @Deprecated public android.util.Size? getResolution();
+ field @Deprecated public static final int UNASSIGNED_ASPECT_RATIO = -1; // 0xffffffff
+ }
+
+ @RequiresApi(21) public final class LifecycleCameraController extends androidx.camera.view.CameraController {
+ ctor public LifecycleCameraController(android.content.Context);
+ method @MainThread public void bindToLifecycle(androidx.lifecycle.LifecycleOwner);
+ method @MainThread public void unbind();
+ }
+
+ @RequiresApi(21) public final class PreviewView extends android.widget.FrameLayout {
+ ctor @UiThread public PreviewView(android.content.Context);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public PreviewView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public android.graphics.Bitmap? getBitmap();
+ method @UiThread public androidx.camera.view.CameraController? getController();
+ method @UiThread public androidx.camera.view.PreviewView.ImplementationMode getImplementationMode();
+ method @UiThread public androidx.camera.core.MeteringPointFactory getMeteringPointFactory();
+ method @SuppressCompatibility public androidx.camera.view.transform.OutputTransform? getOutputTransform();
+ method public androidx.lifecycle.LiveData<androidx.camera.view.PreviewView.StreamState!> getPreviewStreamState();
+ method @UiThread public androidx.camera.view.PreviewView.ScaleType getScaleType();
+ method @UiThread public android.graphics.Matrix? getSensorToViewTransform();
+ method @UiThread public androidx.camera.core.Preview.SurfaceProvider getSurfaceProvider();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort();
+ method @UiThread public androidx.camera.core.ViewPort? getViewPort(int);
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setImplementationMode(androidx.camera.view.PreviewView.ImplementationMode);
+ method @UiThread public void setScaleType(androidx.camera.view.PreviewView.ScaleType);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+ @RequiresApi(21) public enum PreviewView.ImplementationMode {
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.view.PreviewView.ImplementationMode PERFORMANCE;
+ }
+
+ @RequiresApi(21) public enum PreviewView.ScaleType {
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FILL_START;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_CENTER;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_END;
+ enum_constant public static final androidx.camera.view.PreviewView.ScaleType FIT_START;
+ }
+
+ public enum PreviewView.StreamState {
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState IDLE;
+ enum_constant public static final androidx.camera.view.PreviewView.StreamState STREAMING;
+ }
+
+ @RequiresApi(21) public final class RotationProvider {
+ ctor public RotationProvider(android.content.Context);
+ method @CheckResult public boolean addListener(java.util.concurrent.Executor, androidx.camera.view.RotationProvider.Listener);
+ method public void removeListener(androidx.camera.view.RotationProvider.Listener);
+ }
+
+ public static interface RotationProvider.Listener {
+ method public void onRotationChanged(int);
+ }
+
+ @RequiresApi(21) public final class ScreenFlashView extends android.view.View {
+ ctor @UiThread public ScreenFlashView(android.content.Context);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int);
+ ctor @UiThread public ScreenFlashView(android.content.Context, android.util.AttributeSet?, int, int);
+ method @UiThread public androidx.camera.core.ImageCapture.ScreenFlash? getScreenFlash();
+ method @UiThread public void setController(androidx.camera.view.CameraController?);
+ method @UiThread public void setScreenFlashWindow(android.view.Window?);
+ }
+
+}
+
+package androidx.camera.view.transform {
+
+ @SuppressCompatibility @RequiresApi(21) public final class CoordinateTransform {
+ ctor public CoordinateTransform(androidx.camera.view.transform.OutputTransform, androidx.camera.view.transform.OutputTransform);
+ method public void mapPoint(android.graphics.PointF);
+ method public void mapPoints(float[]);
+ method public void mapRect(android.graphics.RectF);
+ method public void transform(android.graphics.Matrix);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class FileTransformFactory {
+ ctor public FileTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(android.content.ContentResolver, android.net.Uri) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.File) throws java.io.IOException;
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(java.io.InputStream) throws java.io.IOException;
+ method public boolean isUsingExifOrientation();
+ method public void setUsingExifOrientation(boolean);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class ImageProxyTransformFactory {
+ ctor public ImageProxyTransformFactory();
+ method public androidx.camera.view.transform.OutputTransform getOutputTransform(androidx.camera.core.ImageProxy);
+ method public boolean isUsingCropRect();
+ method public boolean isUsingRotationDegrees();
+ method public void setUsingCropRect(boolean);
+ method public void setUsingRotationDegrees(boolean);
+ }
+
+ @SuppressCompatibility @RequiresApi(21) public final class OutputTransform {
+ }
+
+}
+
+package androidx.camera.view.video {
+
+ @RequiresApi(21) public class AudioConfig {
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+ method public boolean getAudioEnabled();
+ field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+ }
+
+}
+
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
index 4c33201..441d427 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -19,7 +19,10 @@
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
@@ -41,6 +44,7 @@
import androidx.camera.core.ConcurrentCamera.SingleCameraConfig;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.MeteringPoint;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.lifecycle.ProcessCameraProvider;
@@ -50,6 +54,7 @@
import androidx.core.math.MathUtils;
import androidx.lifecycle.LifecycleOwner;
+import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
@@ -75,6 +80,7 @@
@NonNull private ToggleButton mModeButton;
@NonNull private ToggleButton mLayoutButton;
@NonNull private ToggleButton mToggleButton;
+ @NonNull private ToggleButton mDualSelfieButton;
@NonNull private LinearLayout mSideBySideLayout;
@NonNull private FrameLayout mPiPLayout;
@Nullable private ProcessCameraProvider mCameraProvider;
@@ -82,6 +88,8 @@
private boolean mIsLayoutPiP = true;
private boolean mIsFrontPrimary = true;
+ private boolean mIsDualSelfieEnabled = false;
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -96,6 +104,7 @@
mModeButton = findViewById(R.id.mode_button);
mLayoutButton = findViewById(R.id.layout_button);
mToggleButton = findViewById(R.id.toggle_button);
+ mDualSelfieButton = findViewById(R.id.dual_selfie);
boolean isConcurrentCameraSupported =
getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT);
@@ -144,6 +153,10 @@
bindPreviewForSingle(mCameraProvider);
}
});
+ mDualSelfieButton.setOnClickListener(view -> {
+ mIsDualSelfieEnabled = mDualSelfieButton.isChecked();
+ mDualSelfieButton.setChecked(mIsDualSelfieEnabled);
+ });
if (allPermissionsGranted()) {
if (mCameraProvider != null) {
mCameraProvider.unbindAll();
@@ -167,6 +180,8 @@
}
}, ContextCompat.getMainExecutor(this));
}
+
+ @SuppressLint("RestrictedApiAndroidX")
void bindPreviewForSingle(@NonNull ProcessCameraProvider cameraProvider) {
cameraProvider.unbindAll();
mSideBySideLayout.setVisibility(GONE);
@@ -186,13 +201,18 @@
previewFront.setSurfaceProvider(mSinglePreviewView.getSurfaceProvider());
Camera camera = cameraProvider.bindToLifecycle(
this, cameraSelectorFront, previewFront);
+ mDualSelfieButton.setVisibility(camera.getCameraInfo().isLogicalMultiCameraSupported()
+ ? VISIBLE : GONE);
+ mIsDualSelfieEnabled = false;
setupZoomAndTapToFocus(camera, mSinglePreviewView);
}
+
void bindPreviewForPiP(@NonNull ProcessCameraProvider cameraProvider) {
mSideBySideLayout.setVisibility(GONE);
mFrontPreviewViewForPip.setVisibility(VISIBLE);
mBackPreviewViewForPip.setVisibility(VISIBLE);
mPiPLayout.setVisibility(VISIBLE);
+ mDualSelfieButton.setVisibility(GONE);
if (mFrontPreviewView == null && mBackPreviewView == null) {
// Front
mFrontPreviewView = new PreviewView(this);
@@ -223,9 +243,11 @@
mBackPreviewView);
}
}
+
void bindPreviewForSideBySide() {
mSideBySideLayout.setVisibility(VISIBLE);
mPiPLayout.setVisibility(GONE);
+ mDualSelfieButton.setVisibility(GONE);
if (mFrontPreviewView == null && mBackPreviewView == null) {
mFrontPreviewView = new PreviewView(this);
mFrontPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
@@ -239,62 +261,121 @@
mFrontPreviewView,
mBackPreviewView);
}
+
+ @SuppressLint("RestrictedApiAndroidX")
private void bindToLifecycleForConcurrentCamera(
@NonNull ProcessCameraProvider cameraProvider,
@NonNull LifecycleOwner lifecycleOwner,
@NonNull PreviewView frontPreviewView,
@NonNull PreviewView backPreviewView) {
- Preview previewFront = new Preview.Builder()
- .build();
- CameraSelector cameraSelectorPrimary = null;
- CameraSelector cameraSelectorSecondary = null;
- for (List<CameraInfo> cameraInfoList : cameraProvider.getAvailableConcurrentCameraInfos()) {
- for (CameraInfo cameraInfo : cameraInfoList) {
+ if (mIsDualSelfieEnabled) {
+ CameraInfo cameraInfoPrimary = null;
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_FRONT) {
- cameraSelectorPrimary = cameraInfo.getCameraSelector();
- } else if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_BACK) {
- cameraSelectorSecondary = cameraInfo.getCameraSelector();
+ cameraInfoPrimary = cameraInfo;
+ break;
+ }
+ }
+ if (cameraInfoPrimary == null
+ || cameraInfoPrimary.getPhysicalCameraInfos().size() != 2) {
+ return;
+ }
+
+ String innerPhysicalCameraId = null;
+ String outerPhysicalCameraId = null;
+ for (PhysicalCameraInfo info : cameraInfoPrimary.getPhysicalCameraInfos()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (info.getLensPoseReference()
+ == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA) {
+ innerPhysicalCameraId = info.getPhysicalCameraId();
+ } else {
+ outerPhysicalCameraId = info.getPhysicalCameraId();
+ }
}
}
- if (cameraSelectorPrimary == null || cameraSelectorSecondary == null) {
- // If either a primary or secondary selector wasn't found, reset both
- // to move on to the next list of CameraInfos.
- cameraSelectorPrimary = null;
- cameraSelectorSecondary = null;
- } else {
- // If both primary and secondary camera selectors were found, we can
- // conclude the search.
- break;
+ if (Objects.equal(innerPhysicalCameraId, outerPhysicalCameraId)) {
+ return;
}
- }
- if (cameraSelectorPrimary == null || cameraSelectorSecondary == null) {
- return;
- }
- previewFront.setSurfaceProvider(frontPreviewView.getSurfaceProvider());
- SingleCameraConfig primary = new SingleCameraConfig(
- cameraSelectorPrimary,
- new UseCaseGroup.Builder()
- .addUseCase(previewFront)
- .build(),
- lifecycleOwner);
- Preview previewBack = new Preview.Builder()
- .build();
- previewBack.setSurfaceProvider(backPreviewView.getSurfaceProvider());
- SingleCameraConfig secondary = new SingleCameraConfig(
- cameraSelectorSecondary,
- new UseCaseGroup.Builder()
- .addUseCase(previewBack)
- .build(),
- lifecycleOwner);
- ConcurrentCamera concurrentCamera =
- cameraProvider.bindToLifecycle(ImmutableList.of(primary, secondary));
- setupZoomAndTapToFocus(concurrentCamera.getCameras().get(0), frontPreviewView);
- setupZoomAndTapToFocus(concurrentCamera.getCameras().get(1), backPreviewView);
+ Preview previewFront = new Preview.Builder()
+ .build();
+ previewFront.setSurfaceProvider(frontPreviewView.getSurfaceProvider());
+ SingleCameraConfig primary = new SingleCameraConfig(
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .setPhysicalCameraId(innerPhysicalCameraId)
+ .build(),
+ new UseCaseGroup.Builder()
+ .addUseCase(previewFront)
+ .build(),
+ lifecycleOwner);
+ Preview previewBack = new Preview.Builder()
+ .build();
+ previewBack.setSurfaceProvider(backPreviewView.getSurfaceProvider());
+ SingleCameraConfig secondary = new SingleCameraConfig(
+ new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .setPhysicalCameraId(outerPhysicalCameraId)
+ .build(),
+ new UseCaseGroup.Builder()
+ .addUseCase(previewBack)
+ .build(),
+ lifecycleOwner);
+ cameraProvider.bindToLifecycle(ImmutableList.of(primary, secondary));
+ } else {
+ CameraSelector cameraSelectorPrimary = null;
+ CameraSelector cameraSelectorSecondary = null;
+ for (List<CameraInfo> cameraInfoList : cameraProvider
+ .getAvailableConcurrentCameraInfos()) {
+ for (CameraInfo cameraInfo : cameraInfoList) {
+ if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_FRONT) {
+ cameraSelectorPrimary = cameraInfo.getCameraSelector();
+ } else if (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_BACK) {
+ cameraSelectorSecondary = cameraInfo.getCameraSelector();
+ }
+ }
+
+ if (cameraSelectorPrimary == null || cameraSelectorSecondary == null) {
+ // If either a primary or secondary selector wasn't found, reset both
+ // to move on to the next list of CameraInfos.
+ cameraSelectorPrimary = null;
+ cameraSelectorSecondary = null;
+ } else {
+ // If both primary and secondary camera selectors were found, we can
+ // conclude the search.
+ break;
+ }
+ }
+ if (cameraSelectorPrimary == null || cameraSelectorSecondary == null) {
+ return;
+ }
+ Preview previewFront = new Preview.Builder()
+ .build();
+ previewFront.setSurfaceProvider(frontPreviewView.getSurfaceProvider());
+ SingleCameraConfig primary = new SingleCameraConfig(
+ cameraSelectorPrimary,
+ new UseCaseGroup.Builder()
+ .addUseCase(previewFront)
+ .build(),
+ lifecycleOwner);
+ Preview previewBack = new Preview.Builder()
+ .build();
+ previewBack.setSurfaceProvider(backPreviewView.getSurfaceProvider());
+ SingleCameraConfig secondary = new SingleCameraConfig(
+ cameraSelectorSecondary,
+ new UseCaseGroup.Builder()
+ .addUseCase(previewBack)
+ .build(),
+ lifecycleOwner);
+ ConcurrentCamera concurrentCamera =
+ cameraProvider.bindToLifecycle(ImmutableList.of(primary, secondary));
+
+ setupZoomAndTapToFocus(concurrentCamera.getCameras().get(0), frontPreviewView);
+ setupZoomAndTapToFocus(concurrentCamera.getCameras().get(1), backPreviewView);
+ }
}
-
private void setupZoomAndTapToFocus(Camera camera, PreviewView previewView) {
ScaleGestureDetector scaleDetector = new ScaleGestureDetector(this,
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@@ -330,6 +411,7 @@
return true;
});
}
+
private static void updateFrontAndBackView(
boolean isFrontPrimary,
@NonNull ViewGroup frontParent,
@@ -360,6 +442,7 @@
ViewGroup.LayoutParams.MATCH_PARENT));
}
}
+
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
@@ -369,6 +452,7 @@
}
return true;
}
+
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml
index 45e472a..9c75f56 100644
--- a/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_concurrent_camera.xml
@@ -122,6 +122,22 @@
android:layout_gravity="top|right"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp" />
+
+ <ToggleButton
+ android:id="@+id/dual_selfie"
+ android:textOn="@string/dual_selfie_on"
+ android:textOff="@string/dual_selfie_off"
+ android:layout_width="46dp"
+ android:layout_height="wrap_content"
+ android:scaleType="fitXY"
+ android:textColor="#EEEEEE"
+ android:textSize="10dp"
+ android:checked="false"
+ android:background="@drawable/round_toggle_button"
+ android:visibility="gone"
+ android:layout_gravity="top|right"
+ android:layout_marginRight="15dp"
+ android:layout_marginTop="15dp" />
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/strings.xml b/camera/integration-tests/coretestapp/src/main/res/values/strings.xml
index b4fd308..2a8d205 100644
--- a/camera/integration-tests/coretestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/values/strings.xml
@@ -20,6 +20,8 @@
<string name="switch_mode">Switch Mode</string>
<string name="change_layout">Change Layout</string>
<string name="toggle_camera">Toggle Camera</string>
+ <string name="dual_selfie_on">Dual Selfie On</string>
+ <string name="dual_selfie_off">Dual Selfie Off</string>
<string name="toggle">Toggle</string>
<string name="finish">Finish</string>
<string name="is_front_primary">IsFrontPrimary</string>
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
index 308af1d..340a1c1 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
@@ -23,6 +23,7 @@
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.runtime.withFrameNanos
import androidx.compose.ui.graphics.Color
@@ -32,7 +33,9 @@
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -446,4 +449,90 @@
assertEquals(0f, childTransitionFloat.value)
}
}
+
+ @OptIn(ExperimentalTransitionApi::class)
+ @Test
+ fun addAnimationToCompletedChildTransition() {
+ rule.mainClock.autoAdvance = false
+ var value1 = 0f
+ var value2 = 0f
+ var value3 = 0f
+ lateinit var coroutineScope: CoroutineScope
+ val state = MutableTransitionState(false)
+
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ val parent = rememberTransition(state)
+ value1 = parent.animateFloat({ tween(1600, easing = LinearEasing) }) {
+ if (it) 1000f else 0f
+ }.value
+
+ val child = parent.createChildTransition { it }
+ value2 = child.animateFloat({ tween(160, easing = LinearEasing) }) {
+ if (it) 1000f else 0f
+ }.value
+
+ value3 = if (!parent.targetState) {
+ child.animateFloat({ tween(160, easing = LinearEasing) }) {
+ if (it) 0f else 1000f
+ }.value
+ } else {
+ 0f
+ }
+ }
+ coroutineScope.launch {
+ state.targetState = true
+ }
+ rule.mainClock.advanceTimeByFrame() // wait for composition
+ rule.runOnIdle {
+ assertEquals(0f, value1, 0f)
+ assertEquals(0f, value2, 0f)
+ assertEquals(0f, value3, 0f)
+ }
+ rule.mainClock.advanceTimeByFrame() // latch the animation start value
+ rule.runOnIdle {
+ assertEquals(0f, value1, 0f)
+ assertEquals(0f, value2, 0f)
+ assertEquals(0f, value3, 0f)
+ }
+ rule.mainClock.advanceTimeByFrame() // first frame of animation
+ rule.runOnIdle {
+ assertEquals(10f, value1, 0.1f)
+ assertEquals(100f, value2, 0.1f)
+ assertEquals(0f, value3, 0f) // hasn't started yet
+ }
+ rule.mainClock.advanceTimeBy(160)
+ rule.runOnIdle {
+ assertEquals(110f, value1, 0.1f)
+ assertEquals(1000f, value2, 0f)
+ assertEquals(0f, value3, 0f) // hasn't started yet
+ }
+ coroutineScope.launch {
+ state.targetState = false
+ }
+ rule.mainClock.advanceTimeByFrame() // compose the change
+ rule.runOnIdle {
+ assertEquals(120f, value1, 0.1f)
+ assertEquals(1000f, value2, 0f)
+ assertEquals(0f, value3, 0f)
+ }
+ rule.mainClock.advanceTimeByFrame()
+ var prevValue1 = 120f
+ var prevValue2 = 1000f
+ rule.runOnIdle {
+ // value1 and value2 have spring interrupted values, so we can't
+ // easily know their exact values
+ assertTrue(value1 < prevValue1)
+ prevValue1 = value1
+ assertTrue(value2 < prevValue2)
+ prevValue2 = value2
+ assertEquals(100f, value3, 0.1f)
+ }
+ rule.mainClock.advanceTimeByFrame()
+ rule.runOnIdle {
+ assertTrue(value1 < prevValue1)
+ assertTrue(value2 < prevValue2)
+ assertEquals(200f, value3, 0.1f)
+ }
+ }
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index 585c768..c5494fc 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -34,6 +34,7 @@
import androidx.compose.runtime.mutableStateListOf
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.SnapshotStateObserver
import androidx.compose.runtime.withFrameNanos
@@ -52,6 +53,7 @@
import kotlin.math.roundToLong
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -877,20 +879,27 @@
*/
// TODO: Support creating Transition outside of composition and support imperative use of Transition
@Stable
-class Transition<S> @PublishedApi internal constructor(
+class Transition<S> internal constructor(
private val transitionState: TransitionState<S>,
+ private val parentTransition: Transition<*>?,
val label: String? = null
) {
+ @PublishedApi
+ internal constructor(
+ transitionState: TransitionState<S>,
+ label: String? = null
+ ) : this(transitionState, null, label)
+
internal constructor(
initialState: S,
label: String?
- ) : this(MutableTransitionState(initialState), label)
+ ) : this(MutableTransitionState(initialState), null, label)
@PublishedApi
internal constructor(
transitionState: MutableTransitionState<S>,
label: String? = null
- ) : this(transitionState as TransitionState<S>, label)
+ ) : this(transitionState as TransitionState<S>, null, label)
/**
* Current state of the transition. This will always be the initialState of the transition
@@ -920,19 +929,30 @@
val isRunning: Boolean
get() = startTimeNanos != AnimationConstants.UnspecifiedTime
+ private var _playTimeNanos by mutableLongStateOf(0L)
+
/**
* Play time in nano-seconds. [playTimeNanos] is always non-negative. It starts from 0L at the
* beginning of the transition and increment until all child animations have finished.
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
@set:RestrictTo(RestrictTo.Scope.LIBRARY)
- var playTimeNanos by mutableLongStateOf(0L)
+ var playTimeNanos: Long
+ get() {
+ return parentTransition?.playTimeNanos ?: _playTimeNanos
+ }
+ set(value) {
+ if (parentTransition == null) {
+ _playTimeNanos = value
+ }
+ }
+
// startTimeNanos is in real frame time nanos for the root transition and
// scaled frame time for child transitions (as offset from the root start)
internal var startTimeNanos by mutableLongStateOf(AnimationConstants.UnspecifiedTime)
// This gets calculated every time child is updated/added
- internal var updateChildrenNeeded: Boolean by mutableStateOf(true)
+ private var updateChildrenNeeded: Boolean by mutableStateOf(false)
private val _animations = mutableStateListOf<TransitionAnimationState<*, *>>()
private val _transitions = mutableStateListOf<Transition<*>>()
@@ -1009,6 +1029,7 @@
} else {
(deltaT / durationScale).roundToLong()
}
+ playTimeNanos = scaledPlayTimeNanos
onFrame(scaledPlayTimeNanos, durationScale == 0f)
}
@@ -1020,8 +1041,6 @@
}
updateChildrenNeeded = false
- // Update play time
- playTimeNanos = scaledPlayTimeNanos
var allFinished = true
// Pulse new playtime
_animations.fastForEach {
@@ -1176,21 +1195,34 @@
internal fun animateTo(targetState: S) {
if (!isSeeking) {
updateTarget(targetState)
- // target != currentState adds LaunchedEffect into the tree in the same frame as
+ // target != currentState adds the effect into the tree in the same frame as
// target change.
if (targetState != currentState || isRunning || updateChildrenNeeded) {
- LaunchedEffect(this) {
- while (true) {
+ // We're using a composition-obtained scope + DisposableEffect here to give us
+ // control over coroutine dispatching
+ val coroutineScope = rememberCoroutineScope()
+ DisposableEffect(coroutineScope, this) {
+ // Launch the coroutine undispatched so the block is executed in the current
+ // frame. This is important as this initializes the state.
+ coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
val durationScale = coroutineContext.durationScale
withFrameNanos {
- // This check is very important, as isSeeking may be changed off-band
- // between the last check in composition and this callback which
- // happens in the animation callback the next frame.
if (!isSeeking) {
onFrame(it / AnimationDebugDurationScale, durationScale)
}
}
+ while (isRunning) {
+ withFrameNanos {
+ // This check is very important, as isSeeking may be changed
+ // off-band between the last check in composition and this callback
+ // which happens in the animation callback the next frame.
+ if (!isSeeking) {
+ onFrame(it / AnimationDebugDurationScale, durationScale)
+ }
+ }
+ }
}
+ onDispose { }
}
}
}
@@ -1767,11 +1799,7 @@
childLabel: String,
): Transition<T> {
val transition = remember(this) {
- Transition(MutableTransitionState(initialState), "${this.label} > $childLabel").also {
- // By setting these now, we don't have to wait a frame for the animation to start.
- it.startTimeNanos = playTimeNanos
- it.playTimeNanos = playTimeNanos
- }
+ Transition(MutableTransitionState(initialState), this, "${this.label} > $childLabel")
}
DisposableEffect(transition) {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index cdcea5f..62bdfbf 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -134,6 +134,7 @@
12200 to "1.7.0-alpha03",
12300 to "1.7.0-alpha04",
12400 to "1.7.0-alpha05",
+ 12500 to "1.7.0-alpha06",
)
/**
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ContextualFlowRowColumnTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ContextualFlowRowColumnTest.kt
index bc6097c..db46f25 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ContextualFlowRowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/ContextualFlowRowColumnTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.layout
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -25,6 +26,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
@@ -39,6 +41,7 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -421,6 +424,156 @@
}
@Test
+ fun testContextualFlowRow_horizontalArrangementStart_SeeMoreFullWidth() {
+ val eachSize = 50
+
+ val totalCount = 20
+ val positions: MutableList<Offset> = mutableListOf()
+ var seeMorePosition: Offset? = null
+ var seeMoreSize: IntSize? = null
+ var mainAxisSpacing = 10
+ var crossAxisSpacing = 20
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalDensity provides NoOpDensity
+ ) {
+ var maxLines by remember {
+ mutableStateOf(2)
+ }
+ Box(modifier = Modifier.width(320.dp).wrapContentHeight()) {
+ ContextualFlowRow(
+ itemCount = totalCount,
+ Modifier
+ .fillMaxWidth(1f)
+ .wrapContentHeight(align = Alignment.Top),
+ horizontalArrangement = Arrangement.spacedBy(mainAxisSpacing.dp),
+ verticalArrangement = Arrangement.spacedBy(crossAxisSpacing.dp),
+ maxLines = maxLines,
+ overflow = ContextualFlowRowOverflow.expandIndicator {
+ Box(modifier = Modifier
+ .fillMaxWidth(1f)
+ .height(eachSize.dp)
+ .background(Color.Green)
+ .clickable {
+ maxLines += 2
+ }.onPlaced {
+ seeMorePosition = it.positionInParent()
+ seeMoreSize = it.size
+ }
+ ) {}
+ },
+ ) { index ->
+ Box(
+ Modifier
+ .width(eachSize.dp)
+ .height(50.dp)
+ .background(Color.Green)
+ .onPlaced {
+ val positionInParent = it.positionInParent()
+ positions.add(index, positionInParent)
+ }
+ ) {}
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ var expectedXPosition = 0
+ var expectedYPosition = 0
+ Truth.assertThat(positions.size).isEqualTo(5)
+ positions.forEach { position ->
+ Truth
+ .assertThat(position.x)
+ .isEqualTo(expectedXPosition)
+
+ Truth
+ .assertThat(position.y)
+ .isEqualTo(expectedYPosition)
+ expectedXPosition += eachSize + mainAxisSpacing
+ }
+ expectedYPosition += eachSize + crossAxisSpacing
+ Truth.assertThat(seeMorePosition?.x).isEqualTo(0)
+ Truth.assertThat(seeMorePosition?.y).isEqualTo(expectedYPosition)
+ Truth.assertThat(seeMoreSize?.width).isEqualTo(320)
+ Truth.assertThat(seeMoreSize?.height).isEqualTo(eachSize)
+ }
+
+ @Test
+ fun testContextualFlowColumn_verticalArrangementTop_SeeMoreFullHeight() {
+ val eachSize = 50
+
+ val totalCount = 20
+ val positions: MutableList<Offset> = mutableListOf()
+ var seeMorePosition: Offset? = null
+ var seeMoreSize: IntSize? = null
+ var mainAxisSpacing = 10
+ var crossAxisSpacing = 20
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalDensity provides NoOpDensity
+ ) {
+ var maxLines by remember {
+ mutableStateOf(2)
+ }
+ Box(modifier = Modifier.height(320.dp).wrapContentWidth()) {
+ ContextualFlowColumn(
+ itemCount = totalCount,
+ Modifier
+ .fillMaxHeight(1f)
+ .wrapContentWidth(align = Alignment.Start),
+ verticalArrangement = Arrangement.spacedBy(mainAxisSpacing.dp),
+ horizontalArrangement = Arrangement.spacedBy(crossAxisSpacing.dp),
+ maxLines = maxLines,
+ overflow = ContextualFlowColumnOverflow.expandIndicator {
+ Box(modifier = Modifier
+ .fillMaxHeight(1f)
+ .width(eachSize.dp)
+ .clickable {
+ maxLines += 2
+ }.onPlaced {
+ seeMorePosition = it.positionInParent()
+ seeMoreSize = it.size
+ }
+ ) {}
+ }
+ ) { index ->
+ Box(
+ Modifier
+ .width(eachSize.dp)
+ .height(eachSize.dp)
+ .onPlaced {
+ val positionInParent = it.positionInParent()
+ positions.add(index, positionInParent)
+ }
+ ) {}
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ var expectedXPosition = 0
+ var expectedYPosition = 0
+ Truth.assertThat(positions.size).isEqualTo(5)
+ positions.forEach { position ->
+ Truth
+ .assertThat(position.x)
+ .isEqualTo(expectedXPosition)
+
+ Truth
+ .assertThat(position.y)
+ .isEqualTo(expectedYPosition)
+ expectedYPosition += eachSize + mainAxisSpacing
+ }
+ expectedXPosition += eachSize + crossAxisSpacing
+ Truth.assertThat(seeMorePosition?.x).isEqualTo(expectedXPosition)
+ Truth.assertThat(seeMorePosition?.y).isEqualTo(0)
+ Truth.assertThat(seeMoreSize?.height).isEqualTo(320)
+ Truth.assertThat(seeMoreSize?.width).isEqualTo(eachSize)
+ }
+
+ @Test
fun testContextualFlowRow_equalHeight() {
val listOfHeights = mutableListOf<Int>()
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
index 8a860f4..e6123e22 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/FlowRowColumnTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.layout
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -24,6 +25,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MultiContentMeasurePolicy
@@ -41,6 +43,7 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -1376,7 +1379,7 @@
}
.size(itemSize.dp)
.testTag(seeMoreTag)
- .onPlaced {
+ .onGloballyPositioned {
seeMoreShown = true
}
)
@@ -1455,7 +1458,7 @@
}
.size(itemSize.dp)
.testTag(seeMoreTag)
- .onPlaced {
+ .onGloballyPositioned {
seeMoreShown = true
}
)
@@ -2077,7 +2080,7 @@
}
.size(itemSize.dp)
.testTag(seeMoreTag)
- .onPlaced {
+ .onGloballyPositioned {
seeMoreShown = true
}
)
@@ -2095,7 +2098,7 @@
}
.size(collapseSize.dp)
.testTag(collapseTag)
- .onPlaced {
+ .onGloballyPositioned {
collapseShown = true
}
)
@@ -2217,7 +2220,7 @@
}
.size(itemSize.dp)
.testTag(seeMoreTag)
- .onPlaced {
+ .onGloballyPositioned {
seeMoreShown = true
}
)
@@ -2235,7 +2238,7 @@
}
.size(collapseSize.dp)
.testTag(collapseTag)
- .onPlaced {
+ .onGloballyPositioned {
collapseShown = true
}
)
@@ -2616,6 +2619,158 @@
}
@Test
+ fun testFlowRow_horizontalArrangementStart_SeeMoreFullWidth() {
+ val eachSize = 50
+
+ val totalCount = 20
+ val positions: MutableList<Offset> = mutableListOf()
+ var seeMorePosition: Offset? = null
+ var seeMoreSize: IntSize? = null
+ var mainAxisSpacing = 10
+ var crossAxisSpacing = 20
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalDensity provides NoOpDensity
+ ) {
+ var maxLines by remember {
+ mutableStateOf(2)
+ }
+ Box(modifier = Modifier.width(320.dp).wrapContentHeight()) {
+ FlowRow(
+ Modifier
+ .fillMaxWidth(1f)
+ .wrapContentHeight(align = Alignment.Top),
+ horizontalArrangement = Arrangement.spacedBy(mainAxisSpacing.dp),
+ verticalArrangement = Arrangement.spacedBy(crossAxisSpacing.dp),
+ maxLines = maxLines,
+ overflow = FlowRowOverflow.expandIndicator {
+ Box(modifier = Modifier
+ .fillMaxWidth(1f)
+ .height(eachSize.dp)
+ .background(Color.Green)
+ .clickable {
+ maxLines += 2
+ }.onPlaced {
+ seeMorePosition = it.positionInParent()
+ seeMoreSize = it.size
+ }
+ ) {}
+ }
+ ) {
+ repeat(totalCount) { index ->
+ Box(
+ Modifier
+ .width(eachSize.dp)
+ .height(50.dp)
+ .background(Color.Green)
+ .onPlaced {
+ val positionInParent = it.positionInParent()
+ positions.add(index, positionInParent)
+ }
+ ) {}
+ }
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ var expectedXPosition = 0
+ var expectedYPosition = 0
+ Truth.assertThat(positions.size).isEqualTo(5)
+ positions.forEach { position ->
+ Truth
+ .assertThat(position.x)
+ .isEqualTo(expectedXPosition)
+
+ Truth
+ .assertThat(position.y)
+ .isEqualTo(expectedYPosition)
+ expectedXPosition += eachSize + mainAxisSpacing
+ }
+ expectedYPosition += eachSize + crossAxisSpacing
+ Truth.assertThat(seeMorePosition?.x).isEqualTo(0)
+ Truth.assertThat(seeMorePosition?.y).isEqualTo(expectedYPosition)
+ Truth.assertThat(seeMoreSize?.width).isEqualTo(320)
+ Truth.assertThat(seeMoreSize?.height).isEqualTo(eachSize)
+ }
+
+ @Test
+ fun testFlowColumn_verticalArrangementTop_SeeMoreFullHeight() {
+ val eachSize = 50
+
+ val totalCount = 20
+ val positions: MutableList<Offset> = mutableListOf()
+ var seeMorePosition: Offset? = null
+ var seeMoreSize: IntSize? = null
+ var mainAxisSpacing = 10
+ var crossAxisSpacing = 20
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalDensity provides NoOpDensity
+ ) {
+ var maxLines by remember {
+ mutableStateOf(2)
+ }
+ Box(modifier = Modifier.height(320.dp).wrapContentWidth()) {
+ FlowColumn(
+ Modifier
+ .fillMaxHeight(1f)
+ .wrapContentWidth(align = Alignment.Start),
+ verticalArrangement = Arrangement.spacedBy(mainAxisSpacing.dp),
+ horizontalArrangement = Arrangement.spacedBy(crossAxisSpacing.dp),
+ maxLines = maxLines,
+ overflow = FlowColumnOverflow.expandIndicator {
+ Box(modifier = Modifier
+ .fillMaxHeight(1f)
+ .width(eachSize.dp)
+ .clickable {
+ maxLines += 2
+ }.onPlaced {
+ seeMorePosition = it.positionInParent()
+ seeMoreSize = it.size
+ }
+ ) {}
+ }
+ ) {
+ repeat(totalCount) { index ->
+ Box(
+ Modifier
+ .width(eachSize.dp)
+ .height(eachSize.dp)
+ .onPlaced {
+ val positionInParent = it.positionInParent()
+ positions.add(index, positionInParent)
+ }
+ ) {}
+ }
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ var expectedXPosition = 0
+ var expectedYPosition = 0
+ Truth.assertThat(positions.size).isEqualTo(5)
+ positions.forEach { position ->
+ Truth
+ .assertThat(position.x)
+ .isEqualTo(expectedXPosition)
+
+ Truth
+ .assertThat(position.y)
+ .isEqualTo(expectedYPosition)
+ expectedYPosition += eachSize + mainAxisSpacing
+ }
+ expectedXPosition += eachSize + crossAxisSpacing
+ Truth.assertThat(seeMorePosition?.x).isEqualTo(expectedXPosition)
+ Truth.assertThat(seeMorePosition?.y).isEqualTo(0)
+ Truth.assertThat(seeMoreSize?.height).isEqualTo(320)
+ Truth.assertThat(seeMoreSize?.width).isEqualTo(eachSize)
+ }
+
+ @Test
fun testFlowRow_horizontalArrangementStart_MaxLines() {
val eachSize = 20
val maxItemsInMainAxis = 5
@@ -5679,7 +5834,7 @@
Box(
Modifier
.size(20.dp)
- .onPlaced {
+ .onGloballyPositioned {
itemShown = index + 1
}
)
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 1c194ca..537b801 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
@@ -455,7 +455,7 @@
}
overflow.itemCount = itemCount
overflow.setOverflowMeasurables(
- isHorizontal,
+ this@FlowMeasureLazyPolicy,
constraints
) { canExpand, noOfItemsShown ->
val composableIndex = if (canExpand) 0 else 1
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 c595316..6535884 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
@@ -660,9 +660,9 @@
val collapseMeasurable = measurables.getOrNull(2)?.firstOrNull()
overflow.itemCount = list.size
overflow.setOverflowMeasurables(
+ this@FlowMeasurePolicy,
seeMoreMeasurable,
collapseMeasurable,
- isHorizontal,
constraints,
)
return breakDownItems(
@@ -1182,7 +1182,7 @@
val spacing = ceil(mainAxisSpacingDp.toPx()).toInt()
val crossAxisSpacing = ceil(crossAxisSpacingDp.toPx()).toInt()
val subsetConstraints = OrientationIndependentConstraints(
- mainAxisMin,
+ 0,
mainAxisMax,
0,
crossAxisMax
@@ -1347,6 +1347,7 @@
ellipsisWrapInfo?.let {
measurables.add(it.ellipsis)
+ placeables[measurables.size - 1] = it.placeable
lineIndex = endBreakLineList.lastIndex
if (it.placeEllipsisOnLastContentLine) {
val lastIndex = endBreakLineList.size - 1
@@ -1450,7 +1451,7 @@
// For weighted items, we continue to use their intrinsic widths.
// This is because their fixed sizes are only determined after we determine
// the number of items that can fit in the row/column it only lies on.
-private fun Measurable.measureAndCache(
+internal fun Measurable.measureAndCache(
measurePolicy: FlowLineMeasurePolicy,
constraints: Constraints,
storePlaceable: (Placeable?) -> Unit
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutBuildingBlocks.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutBuildingBlocks.kt
index 3dfa4ecc..d49e626 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutBuildingBlocks.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutBuildingBlocks.kt
@@ -17,6 +17,7 @@
import androidx.collection.IntIntPair
import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
import kotlin.math.max
@OptIn(ExperimentalLayoutApi::class)
@@ -35,8 +36,9 @@
class WrapEllipsisInfo(
val ellipsis: Measurable,
+ val placeable: Placeable?,
val ellipsisSize: IntIntPair,
- val placeEllipsisOnLastContentLine: Boolean,
+ var placeEllipsisOnLastContentLine: Boolean = true,
)
fun getWrapEllipsisInfo(
@@ -49,29 +51,18 @@
): WrapEllipsisInfo? {
if (!wrapInfo.isLastItemInContainer) return null
- val ellipsis = overflow.ellipsis(
+ val ellipsisInfo = overflow.ellipsisInfo(
hasNext,
lastContentLineIndex,
- getFinalMeasurable = true,
totalCrossAxisSize
) ?: return null
- val ellipsisSize = ellipsis.let {
- overflow.ellipsisSize(
- hasNext,
- lastContentLineIndex,
- totalCrossAxisSize
- )
- } ?: return null
-
val canFitLine = lastContentLineIndex >= 0 && (nextIndexInLine == 0 ||
- !(leftOverMainAxis - ellipsisSize.first < 0 || nextIndexInLine >= maxItemsInMainAxis))
+ !(leftOverMainAxis - ellipsisInfo.ellipsisSize.first < 0 ||
+ nextIndexInLine >= maxItemsInMainAxis))
- return WrapEllipsisInfo(
- ellipsis,
- ellipsisSize,
- canFitLine
- )
+ ellipsisInfo.placeEllipsisOnLastContentLine = canFitLine
+ return ellipsisInfo
}
fun getWrapInfo(
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutOverflow.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutOverflow.kt
index ed110b2..8fc7e1b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutOverflow.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayoutOverflow.kt
@@ -30,6 +30,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@@ -718,7 +719,9 @@
internal var itemShown: Int = -1
internal var itemCount = 0
private var seeMoreMeasurable: Measurable? = null
+ private var seeMorePlaceable: Placeable? = null
private var collapseMeasurable: Measurable? = null
+ private var collapsePlaceable: Placeable? = null
private var seeMoreSize: IntIntPair? = null
private var collapseSize: IntIntPair? = null
// for contextual flow row
@@ -750,42 +753,46 @@
}
}
- internal fun ellipsis(
+ internal fun ellipsisInfo(
hasNext: Boolean,
lineIndex: Int,
- getFinalMeasurable: Boolean,
totalCrossAxisSize: Int
- ): Measurable? {
+ ): FlowLayoutBuildingBlocks.WrapEllipsisInfo? {
return when (type) {
FlowLayoutOverflow.OverflowType.Visible -> null
FlowLayoutOverflow.OverflowType.Clip -> null
FlowLayoutOverflow.OverflowType.ExpandIndicator,
FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator -> {
+ var measurable: Measurable? = null
+ var placeable: Placeable? = null
+ var ellipsisSize: IntIntPair?
if (hasNext) {
- if (getFinalMeasurable) {
- getOverflowMeasurable?.invoke(
- /* isExpandable */ true,
- noOfItemsShown
- ) ?: seeMoreMeasurable
- } else {
- seeMoreMeasurable
+ measurable = getOverflowMeasurable?.invoke(
+ /* isExpandable */ true, noOfItemsShown
+ ) ?: seeMoreMeasurable
+ ellipsisSize = seeMoreSize
+ if (getOverflowMeasurable == null) {
+ placeable = seeMorePlaceable
}
} else {
- if (lineIndex < (minLinesToShowCollapse - 1) ||
- totalCrossAxisSize < (minCrossAxisSizeToShowCollapse)
- ) {
- null
- } else {
- if (getFinalMeasurable) {
- getOverflowMeasurable?.invoke(
- /* isExpandable */ false,
- noOfItemsShown
- ) ?: collapseMeasurable
- } else {
- collapseMeasurable
- }
+ if (lineIndex >= (minLinesToShowCollapse - 1) &&
+ totalCrossAxisSize >= (minCrossAxisSizeToShowCollapse)
+ ) {
+ measurable = getOverflowMeasurable?.invoke(
+ /* isExpandable */ false, noOfItemsShown
+ ) ?: collapseMeasurable
+ }
+ ellipsisSize = collapseSize
+ if (getOverflowMeasurable == null) {
+ placeable = collapsePlaceable
}
}
+ measurable ?: return null
+ FlowLayoutBuildingBlocks.WrapEllipsisInfo(
+ measurable,
+ placeable,
+ ellipsisSize!!
+ )
}
}
}
@@ -810,6 +817,7 @@
)
this.seeMoreSize = IntIntPair(mainAxisSize, crossAxisSize)
this.seeMoreMeasurable = item as? Measurable
+ this.seeMorePlaceable = null
}
collapseMeasurable?.let { item ->
val mainAxisSize = item.mainAxisMin(
@@ -819,11 +827,63 @@
val crossAxisSize = item.crossAxisMin(isHorizontal, mainAxisSize)
this.collapseSize = IntIntPair(mainAxisSize, crossAxisSize)
this.collapseMeasurable = item as? Measurable
+ this.collapsePlaceable = null
}
}
internal fun setOverflowMeasurables(
- isHorizontal: Boolean,
+ measurePolicy: FlowLineMeasurePolicy,
+ seeMoreMeasurable: Measurable?,
+ collapseMeasurable: Measurable?,
+ constraints: Constraints,
+ ) {
+ val isHorizontal = measurePolicy.isHorizontal
+ val orientation = if (isHorizontal)
+ LayoutOrientation.Horizontal else LayoutOrientation.Vertical
+ val orientationIndependentConstraints =
+ OrientationIndependentConstraints(constraints, orientation)
+ .copy(mainAxisMin = 0, crossAxisMin = 0)
+ val finalConstraints = orientationIndependentConstraints.toBoxConstraints(orientation)
+ seeMoreMeasurable?.let { item ->
+ item.measureAndCache(
+ measurePolicy,
+ finalConstraints
+ ) { placeable ->
+ var mainAxisSize = 0
+ var crossAxisSize = 0
+ placeable?.let {
+ with(measurePolicy) {
+ mainAxisSize = placeable.mainAxisSize()
+ crossAxisSize = placeable.crossAxisSize()
+ }
+ }
+ seeMoreSize = IntIntPair(mainAxisSize, crossAxisSize)
+ seeMorePlaceable = placeable
+ }
+ this.seeMoreMeasurable = item
+ }
+ collapseMeasurable?.let { item ->
+ item.measureAndCache(
+ measurePolicy,
+ finalConstraints
+ ) { placeable ->
+ var mainAxisSize = 0
+ var crossAxisSize = 0
+ placeable?.let {
+ with(measurePolicy) {
+ mainAxisSize = placeable.mainAxisSize()
+ crossAxisSize = placeable.crossAxisSize()
+ }
+ }
+ this.collapseSize = IntIntPair(mainAxisSize, crossAxisSize)
+ collapsePlaceable = placeable
+ }
+ this.collapseMeasurable = item
+ }
+ }
+
+ internal fun setOverflowMeasurables(
+ measurePolicy: FlowLineMeasurePolicy,
constraints: Constraints,
getOverflowMeasurable: ((isExpandable: Boolean, numberOfItemsShown: Int) -> Measurable?)
) {
@@ -836,9 +896,9 @@
/* isExpandable */ false, /* numberOfItemsShown */ 0
)
setOverflowMeasurables(
+ measurePolicy,
seeMoreMeasurable,
collapseMeasurable,
- isHorizontal,
constraints
)
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 032cf78..5d21cbf 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -391,6 +391,7 @@
public final class AnchoredDraggableKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1748,9 +1749,13 @@
@androidx.compose.runtime.Stable public final class TextFieldState {
ctor public TextFieldState(optional String initialText, optional long initialSelection);
method public inline void edit(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.text.input.TextFieldBuffer,kotlin.Unit> block);
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getText();
+ method public androidx.compose.ui.text.TextRange? getComposition();
+ method public long getSelection();
+ method public CharSequence getText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.text.input.UndoState getUndoState();
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence text;
+ property public final androidx.compose.ui.text.TextRange? composition;
+ property public final long selection;
+ property public final CharSequence text;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.UndoState undoState;
}
@@ -1762,11 +1767,10 @@
public final class TextFieldStateKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> valueAsFlow(androidx.compose.foundation.text.input.TextFieldState);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class TextObfuscationMode {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 652aacc..c291ab6 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -393,6 +393,7 @@
public final class AnchoredDraggableKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1752,10 +1753,14 @@
method @kotlin.PublishedApi internal void commitEdit(androidx.compose.foundation.text.input.TextFieldBuffer newValue);
method public inline void edit(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.text.input.TextFieldBuffer,kotlin.Unit> block);
method @kotlin.PublishedApi internal void finishEditing();
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getText();
+ method public androidx.compose.ui.text.TextRange? getComposition();
+ method public long getSelection();
+ method public CharSequence getText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.text.input.UndoState getUndoState();
- method @kotlin.PublishedApi internal androidx.compose.foundation.text.input.TextFieldBuffer startEdit(androidx.compose.foundation.text.input.TextFieldCharSequence value);
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence text;
+ method @kotlin.PublishedApi internal androidx.compose.foundation.text.input.TextFieldBuffer startEdit();
+ property public final androidx.compose.ui.text.TextRange? composition;
+ property public final long selection;
+ property public final CharSequence text;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.UndoState undoState;
}
@@ -1767,11 +1772,10 @@
public final class TextFieldStateKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> valueAsFlow(androidx.compose.foundation.text.input.TextFieldState);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class TextObfuscationMode {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 9a91c06..ab1b2e3 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -91,6 +91,7 @@
ComposableDemo("Scrollable with focused child") { ScrollableFocusedChildDemo() },
ComposableDemo("Window insets") { WindowInsetsDemo() },
ComposableDemo("Marquee") { BasicMarqueeDemo() },
- DemoCategory("Pointer Icon", PointerIconDemos)
+ DemoCategory("Pointer Icon", PointerIconDemos),
+ DemoCategory("Long screenshots", ScrollingScreenshotsDemos),
)
)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollingScreenshotDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollingScreenshotDemo.kt
new file mode 100644
index 0000000..166cecc
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollingScreenshotDemo.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import android.content.Context
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.TextView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.fillMaxHeight
+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.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.material.BottomAppBar
+import androidx.compose.material.Button
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.primarySurface
+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.graphics.Color
+import androidx.compose.ui.scrollcapture.ComposeFeatureFlag_LongScreenshotsEnabled
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Dialog
+
+val ScrollingScreenshotsDemos = listOf(
+ ComposableDemo("Single, small, eager list") { SingleEagerListDemo() },
+ ComposableDemo("Single, small, lazy list") { SingleLazyListDemo() },
+ ComposableDemo("Single, full-screen list") { SingleFullScreenListDemo() },
+ ComposableDemo("Lazy list with content padding") { LazyListContentPaddingDemo() },
+ ComposableDemo("Big viewport nested in smaller outer viewport") { BigInLittleDemo() },
+ ComposableDemo("Scrollable in dialog") { InDialogDemo() },
+ ComposableDemo("Nested AndroidView") { AndroidViewDemo() },
+)
+
+@Composable
+private fun SingleEagerListDemo() {
+ var fullWidth by remember { mutableStateOf(false) }
+ var fullHeight by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ FeatureFlagToggle()
+
+ Text(
+ "This is some scrollable content. When a screenshot is taken, it should let you " +
+ "capture the entire content, not just the part currently visible.",
+ style = MaterialTheme.typography.caption
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Full-width")
+ Switch(fullWidth, onCheckedChange = { fullWidth = it })
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Full-height")
+ Switch(fullHeight, onCheckedChange = { fullHeight = it })
+ }
+ Divider()
+
+ Column(
+ Modifier
+ .border(1.dp, Color.Black)
+ .fillMaxWidth(fraction = if (fullWidth) 1f else 0.75f)
+ .fillMaxHeight(fraction = if (fullHeight) 1f else 0.75f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ repeat(50) { index ->
+ Button(
+ onClick = {},
+ Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Text("Button $index")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SingleLazyListDemo() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ FeatureFlagToggle()
+
+ Text(
+ "This is some scrollable content. When a screenshot is taken, it should let you " +
+ "capture the entire content, not just the part currently visible.",
+ style = MaterialTheme.typography.caption
+ )
+
+ LazyColumn(
+ Modifier
+ .border(1.dp, Color.Black)
+ .fillMaxWidth(fraction = 0.75f)
+ .height(200.dp)
+ ) {
+ items(50) { index ->
+ Button(
+ onClick = {},
+ Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Text("Button $index")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SingleFullScreenListDemo() {
+ Column {
+ FeatureFlagToggle()
+
+ LazyColumn(Modifier.fillMaxSize()) {
+ items(50) { index ->
+ Button(
+ onClick = {},
+ Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Text("Button $index")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LazyListContentPaddingDemo() {
+ Column {
+ FeatureFlagToggle()
+
+ Scaffold(
+ modifier = Modifier
+ .padding(8.dp)
+ .border(1.dp, Color.Black),
+ topBar = {
+ TopAppBar(
+ title = { Text("Top bar") },
+ backgroundColor = MaterialTheme.colors.primarySurface.copy(alpha = 0.5f)
+ )
+ },
+ bottomBar = {
+ BottomAppBar(
+ backgroundColor = MaterialTheme.colors.primarySurface.copy(alpha = 0.5f)
+ ) { Text("Bottom bar") }
+ }
+ ) { contentPadding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Red),
+ contentPadding = contentPadding
+ ) {
+ items(15) { index ->
+ Button(
+ onClick = {},
+ Modifier
+ .background(Color.LightGray)
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Text("Button $index")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun BigInLittleDemo() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ FeatureFlagToggle()
+
+ Text(
+ "This is a small scroll container that has a much larger scroll container inside it. " +
+ "The inner scroll container should be captured.",
+ style = MaterialTheme.typography.caption
+ )
+
+ LazyColumn(
+ Modifier
+ .border(1.dp, Color.Black)
+ .weight(1f)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ items(4) {
+ Text(
+ "Header $it",
+ Modifier
+ .fillMaxWidth()
+ .background(Color.LightGray)
+ .padding(16.dp)
+ )
+ Box(
+ Modifier
+ .background(Color.Magenta)
+ .fillParentMaxHeight(0.5f)
+// .height(400.dp)
+ .padding(horizontal = 16.dp)
+ ) {
+ SingleFullScreenListDemo()
+ }
+ Text(
+ "Footer $it",
+ Modifier
+ .fillMaxWidth()
+ .background(Color.LightGray)
+ .padding(16.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun InDialogDemo() {
+ Column {
+ FeatureFlagToggle()
+
+ // Need a scrolling list in the below screen to check that the scrollable in the dialog is
+ // selected instead.
+ LazyColumn(Modifier.fillMaxSize()) {
+ items(50) { index ->
+ var showDialog by remember { mutableStateOf(false) }
+ Button(
+ onClick = { showDialog = true },
+ Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Text("Open dialog ($index)")
+ }
+
+ if (showDialog) {
+ Dialog(onDismissRequest = { showDialog = false }) {
+ Box(
+ Modifier
+ .fillMaxSize(fraction = 0.5f)
+ .background(Color.LightGray)
+ ) {
+ SingleFullScreenListDemo()
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AndroidViewDemo() {
+ class DemoAndroidView(context: Context) : LinearLayout(context) {
+ init {
+ orientation = VERTICAL
+ addView(TextView(context).also { it.text = "AndroidView Header" })
+ addView(ScrollView(context).apply {
+ setBackgroundColor(android.graphics.Color.CYAN)
+ addView(LinearLayout(context).apply {
+ orientation = VERTICAL
+ repeat(20) {
+ addView(TextView(context).apply {
+ setPadding(20, 20, 20, 20)
+ text = "Item $it"
+ })
+ }
+ })
+ }, LayoutParams(MATCH_PARENT, 0, 1f))
+ addView(TextView(context).also { it.text = "AndroidView Footer" })
+ }
+ }
+
+ Column {
+ FeatureFlagToggle()
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(10) {
+ Text("Compose item", Modifier.padding(16.dp))
+ }
+ item {
+ AndroidView(
+ factory = ::DemoAndroidView,
+ modifier = Modifier
+ .background(Color.Magenta)
+ .fillParentMaxHeight(0.5f)
+ .padding(horizontal = 16.dp)
+ )
+ }
+ items(5) {
+ Text("Compose item", Modifier.padding(16.dp))
+ }
+ }
+ }
+}
+
+@Suppress("DEPRECATION")
+@Composable
+private fun FeatureFlagToggle() {
+ Column(Modifier.background(Color.Yellow.copy(alpha = 0.5f))) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(Modifier.weight(1f)) {
+ Text("Enable long screenshots")
+ Text(
+ "The long screenshots feature is behind a feature flag while under " +
+ "development. ",
+ style = MaterialTheme.typography.caption
+ )
+ }
+ Switch(
+ checked = ComposeFeatureFlag_LongScreenshotsEnabled,
+ onCheckedChange = { ComposeFeatureFlag_LongScreenshotsEnabled = it },
+ )
+ }
+ Divider()
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
index 2558c13..b3bcb53 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
@@ -79,7 +79,7 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InsertReplaceDeleteDemo() {
- val text = remember { TextFieldState("abc def ghi") }
+ val state = remember { TextFieldState("abc def ghi") }
var prefixEnabled by remember { mutableStateOf(true) }
var suffixEnabled by remember { mutableStateOf(true) }
var middleWedge by remember { mutableStateOf(true) }
@@ -136,7 +136,7 @@
}
var isFirstFieldFocused by remember { mutableStateOf(false) }
BasicTextField(
- state = text,
+ state = state,
onTextLayout = { textLayoutResultProvider = it },
modifier = Modifier
.alignByBaseline()
@@ -149,7 +149,7 @@
// Only draw selection outline when not focused.
if (isFirstFieldFocused) return@drawWithContent
val textLayoutResult = textLayoutResultProvider() ?: return@drawWithContent
- val selection = text.text.selection
+ val selection = state.selection
if (selection.collapsed) {
val cursorRect = textLayoutResult.getCursorRect(selection.start)
drawLine(
@@ -175,7 +175,7 @@
modifier = Modifier.alignBy { (it.measuredHeight * 0.75f).toInt() }
)
BasicTextField(
- state = text,
+ state = state,
modifier = Modifier
.alignByBaseline()
.weight(0.5f)
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
index baed998..2604fea 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
@@ -44,12 +44,11 @@
import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.forEachChange
import androidx.compose.foundation.text.input.forEachChangeReversed
-import androidx.compose.foundation.text.input.forEachTextValue
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
-import androidx.compose.foundation.text.input.textAsFlow
import androidx.compose.foundation.text.input.then
+import androidx.compose.foundation.text.input.valueAsFlow
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
@@ -224,7 +223,7 @@
/** Called while the view model is active, e.g. from a LaunchedEffect. */
suspend fun run() {
- searchFieldState.forEachTextValue { queryText ->
+ searchFieldState.valueAsFlow().collectLatest { queryText ->
// Start a new search every time the user types something valid. If the previous
// search is still being processed when the text is changed, it will be cancelled
// and this code will run again with the latest query text.
@@ -426,41 +425,6 @@
})
}
-@Sampled
-fun BasicTextFieldForEachTextValueSample() {
- class SearchViewModel {
- val searchFieldState = TextFieldState()
- var searchResults: List<String> by mutableStateOf(emptyList())
- private set
-
- /** Called while the view model is active, e.g. from a LaunchedEffect. */
- suspend fun run() {
- searchFieldState.forEachTextValue { queryText ->
- // Start a new search every time the user types something. If the previous search
- // is still being processed when the text is changed, it will be cancelled and this
- // code will run again with the latest query text.
- searchResults = performSearch(query = queryText)
- }
- }
-
- private suspend fun performSearch(query: CharSequence): List<String> {
- TODO()
- }
- }
-
- @Composable
- fun SearchScreen(viewModel: SearchViewModel) {
- Column {
- BasicTextField(viewModel.searchFieldState)
- LazyColumn {
- items(viewModel.searchResults) {
- TODO()
- }
- }
- }
- }
-}
-
@OptIn(FlowPreview::class)
@Suppress("RedundantSuspendModifier")
@Sampled
@@ -472,7 +436,7 @@
/** Called while the view model is active, e.g. from a LaunchedEffect. */
suspend fun run() {
- searchFieldState.textAsFlow()
+ searchFieldState.valueAsFlow()
// Let fast typers get multiple keystrokes in before kicking off a search.
.debounce(500)
// collectLatest cancels the previous search if it's still running when there's a
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
index 14a7720..356546a 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
@@ -19,7 +19,6 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -46,7 +45,6 @@
)
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun StringTextField(
value: String,
@@ -198,20 +196,19 @@
}
private fun observeTextState(fireOnValueChanged: Boolean = true) {
- lateinit var text: TextFieldCharSequence
+ lateinit var value: TextFieldValue
observeReads {
- text = state.text
+ value = TextFieldValue(
+ state.text.toString(),
+ state.selection,
+ state.composition
+ )
}
// This code is outside of the observeReads lambda so we don't observe any state reads the
// callback happens to do.
if (fireOnValueChanged) {
- val newValue = TextFieldValue(
- text = text.toString(),
- selection = text.selection,
- composition = text.composition
- )
- onValueChanged(newValue)
+ onValueChanged(value)
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt
index a828f4b..3c4ec9c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollTest.kt
@@ -47,6 +47,7 @@
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -102,6 +103,7 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import org.junit.After
import org.junit.Assert.assertEquals
@@ -522,6 +524,90 @@
}
@Test
+ fun scroller_semanticsScrollByOffset_isAnimated() {
+ rule.mainClock.autoAdvance = false
+ val scrollState = ScrollState(initial = 0)
+
+ createScrollableContent(scrollState = scrollState)
+
+ rule.waitForIdle()
+ assertThat(scrollState.value).isEqualTo(0)
+ assertThat(scrollState.maxValue).isGreaterThan(100) // If this fails, just add more items
+
+ val action = rule.onNodeWithTag(scrollerTag)
+ .fetchSemanticsNode().config[SemanticsActions.ScrollByOffset]
+ scope.launch(start = CoroutineStart.UNDISPATCHED) {
+ when (config.orientation) {
+ Vertical -> action(Offset(0f, 100f))
+ Horizontal -> action(Offset(100f, 0f))
+ }
+ }
+
+ // We haven't advanced time yet, make sure it's still zero
+ assertThat(scrollState.value).isEqualTo(0)
+
+ // Advance and make sure we're partway through
+ // Note that we need two frames for the animation to actually happen
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(scrollState.value).isGreaterThan(0)
+ assertThat(scrollState.value).isLessThan(100)
+
+ // Finish the scroll, make sure we're at the target
+ rule.mainClock.advanceTimeBy(5000)
+ assertThat(scrollState.value).isEqualTo(100)
+ }
+
+ @Test
+ fun scroller_semanticsScrollByOffset_returnsConsumedScroll() {
+ val scrollState = ScrollState(initial = 0)
+ var consumedScroll = Offset.Unspecified
+
+ createScrollableContent(scrollState = scrollState)
+
+ rule.waitForIdle()
+ assertThat(scrollState.value).isEqualTo(0)
+ assertThat(scrollState.maxValue).isGreaterThan(100) // If this fails, just add more items
+
+ val action = rule.onNodeWithTag(scrollerTag).fetchSemanticsNode()
+ .config[SemanticsActions.ScrollByOffset]
+
+ scope.launch {
+ consumedScroll = when (config.orientation) {
+ Vertical -> action(Offset(0f, 100f))
+ Horizontal -> action(Offset(100f, 0f))
+ }
+ }
+ rule.runOnIdle {
+ assertThat(consumedScroll).isEqualTo(
+ when (config.orientation) {
+ Vertical -> Offset(0f, 100f)
+ Horizontal -> Offset(100f, 0f)
+ }
+ )
+ }
+
+ // Try to scroll again, only consume part.
+ val expectedConsumed = scrollState.maxValue - scrollState.value
+ val impossibleScrollRequest = scrollState.maxValue + 10f
+ // b/330698760
+ scope.launch(DisableAnimationMotionDurationScale) {
+ consumedScroll = when (config.orientation) {
+ Vertical -> action(Offset(0f, impossibleScrollRequest))
+ Horizontal -> action(Offset(impossibleScrollRequest, 0f))
+ }
+ }
+ rule.runOnIdle {
+ assertThat(consumedScroll).isEqualTo(
+ when (config.orientation) {
+ Vertical -> Offset(0f, expectedConsumed.toFloat())
+ Horizontal -> Offset(expectedConsumed.toFloat(), 0f)
+ }
+ )
+ }
+ }
+
+ @Test
fun scroller_touchInputEnabled_shouldHaveSemanticsInfo() {
val scrollState = ScrollState(initial = 0)
val scrollNode = rule.onNodeWithTag(scrollerTag)
@@ -841,9 +927,11 @@
}
}
rule.setContent {
- Box(Modifier.intrinsicMainAxisSize(IntrinsicSize.Min)
- .scrollerWithOrientation()
- .then(layoutModifier)
+ Box(
+ Modifier
+ .intrinsicMainAxisSize(IntrinsicSize.Min)
+ .scrollerWithOrientation()
+ .then(layoutModifier)
)
}
rule.waitForIdle()
@@ -882,9 +970,11 @@
}
}
rule.setContent {
- Box(Modifier.intrinsicCrossAxisSize(IntrinsicSize.Min)
- .scrollerWithOrientation()
- .then(layoutModifier)
+ Box(
+ Modifier
+ .intrinsicCrossAxisSize(IntrinsicSize.Min)
+ .scrollerWithOrientation()
+ .then(layoutModifier)
)
}
rule.waitForIdle()
@@ -923,9 +1013,11 @@
}
}
rule.setContent {
- Box(Modifier.intrinsicMainAxisSize(IntrinsicSize.Max)
- .scrollerWithOrientation()
- .then(layoutModifier)
+ Box(
+ Modifier
+ .intrinsicMainAxisSize(IntrinsicSize.Max)
+ .scrollerWithOrientation()
+ .then(layoutModifier)
)
}
rule.waitForIdle()
@@ -964,9 +1056,11 @@
}
}
rule.setContent {
- Box(Modifier.intrinsicCrossAxisSize(IntrinsicSize.Max)
- .scrollerWithOrientation()
- .then(layoutModifier)
+ Box(
+ Modifier
+ .intrinsicCrossAxisSize(IntrinsicSize.Max)
+ .scrollerWithOrientation()
+ .then(layoutModifier)
)
}
rule.waitForIdle()
@@ -1092,9 +1186,12 @@
val content: @Composable () -> Unit = {
repeat(25) {
- Box(modifier = Modifier.size(100.dp)
- .padding(2.dp)
- .background(Color.Red))
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(2.dp)
+ .background(Color.Red)
+ )
}
}
@@ -1449,4 +1546,9 @@
onRemeasure.invoke()
}
}
+
+ private object DisableAnimationMotionDurationScale : MotionDurationScale {
+ override val scaleFactor: Float
+ get() = 0f
+ }
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index a5c32eb..dc9ae1e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -870,7 +870,7 @@
val velocityThreshold = 100.dp
val state = AnchoredDraggableState(
initialValue = A,
- velocityThreshold = { 0f },
+ velocityThreshold = { with(rule.density) { velocityThreshold.toPx() } },
positionalThreshold = { Float.POSITIVE_INFINITY },
snapAnimationSpec = tween(),
decayAnimationSpec = DefaultDecayAnimationSpec
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableOverscrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableOverscrollTest.kt
index 8cb9c88..5cbdb10 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableOverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableOverscrollTest.kt
@@ -37,6 +37,7 @@
import androidx.compose.foundation.overscroll
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.WithTouchSlop
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -293,9 +294,10 @@
// assert that applyToFling was called but there is no remaining velocity for overscroll
// because flinging was not towards the min/max anchors
assertThat(overscrollEffect.applyToFlingCalledCount).isEqualTo(1)
- assertThat(abs(overscrollEffect.flingOverscrollVelocity.x)).isEqualTo(0f)
+ assertThat(abs(overscrollEffect.flingOverscrollVelocity.x)).isWithin(1f).of(0f)
}
+ @OptIn(ExperimentalComposeUiApi::class)
@Test
fun anchoredDraggable_swipeWithVelocity_notEnoughVelocityForOverscroll() {
val overscrollEffect = TestOverscrollEffect()
@@ -365,7 +367,7 @@
// assert that applyToFling was called but there is no remaining velocity for overscroll
// because velocity is small
assertThat(overscrollEffect.applyToFlingCalledCount).isEqualTo(1)
- assertThat(abs(overscrollEffect.flingOverscrollVelocity.x)).isEqualTo(0f)
+ assertThat(abs(overscrollEffect.flingOverscrollVelocity.x)).isWithin(1f).of(0f)
}
private val DefaultPositionalThreshold: (totalDistance: Float) -> Float = {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
index 84885e5..970617f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -40,6 +40,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
+import androidx.compose.foundation.gestures.animateToWithDecay
import androidx.compose.foundation.gestures.snapTo
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -897,7 +898,7 @@
}
@Test
- fun anchoredDraggable_customDrag_snapsToClosestAnchor() = runBlocking {
+ fun anchoredDraggable_customDrag_doesNotSnapToClosestAnchor() = runBlocking {
val state = AnchoredDraggableState(
initialValue = A,
positionalThreshold = defaultPositionalThreshold,
@@ -916,13 +917,13 @@
}
assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.requireOffset()).isEqualTo(200f)
+ assertThat(state.requireOffset()).isEqualTo(150f)
state.anchoredDrag {
dragTo(260f)
}
assertThat(state.currentValue).isEqualTo(C)
- assertThat(state.requireOffset()).isEqualTo(300f)
+ assertThat(state.requireOffset()).isEqualTo(260f)
}
@Test
@@ -1513,10 +1514,58 @@
assertThat(state.offset).isEqualTo(positionB)
// since offset == positionB, decay animation is used
- assertThat(inspectDecayAnimationSpec.animationWasExecutions).isEqualTo(1)
+ assertThat(inspectDecayAnimationSpec.animationWasExecutions).isEqualTo(0)
assertThat(tweenAnimationSpec.animationWasExecutions).isEqualTo(0)
}
+ @Test
+ fun anchoredDraggable_animateTo_alreadyAtTarget_noOps() {
+ val state = AnchoredDraggableState(
+ initialValue = B,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold,
+ snapAnimationSpec = defaultAnimationSpec,
+ decayAnimationSpec = defaultDecayAnimationSpec,
+ anchors = DraggableAnchors {
+ A at 0f
+ B at 200f
+ C at 300f
+ }
+ )
+ val clock = HandPumpTestFrameClock()
+ val scope = CoroutineScope(clock)
+
+ assertThat(state.offset).isEqualTo(200f)
+ scope.launch { state.animateTo(B) }
+ runBlocking { clock.advanceByFrame() } // Advance only one frame, we should be done
+ assertThat(state.offset).isEqualTo(200f)
+ }
+
+ @Test
+ fun anchoredDraggable_animateToWithDecay_alreadyAtTarget_noOps() {
+ val state = AnchoredDraggableState(
+ initialValue = B,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold,
+ snapAnimationSpec = defaultAnimationSpec,
+ decayAnimationSpec = defaultDecayAnimationSpec,
+ anchors = DraggableAnchors {
+ A at 0f
+ B at 200f
+ C at 300f
+ }
+ )
+ val clock = HandPumpTestFrameClock()
+ val scope = CoroutineScope(clock)
+
+ assertThat(state.offset).isEqualTo(200f)
+ scope.launch {
+ state.animateToWithDecay(B, velocity = 100f)
+ }
+ runBlocking { clock.advanceByFrame() } // Advance only one frame, we should be done
+ assertThat(state.offset).isEqualTo(200f)
+ }
+
private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> { }
private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldImeSelectionChangesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldImeSelectionChangesTest.kt
index 3a7e1f3..32b32fc 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldImeSelectionChangesTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldImeSelectionChangesTest.kt
@@ -93,6 +93,22 @@
}
@Test
+ fun perform_setComposingText() {
+ val state = TextFieldState("Hello")
+ inputMethodInterceptor.setTextFieldTestContent {
+ BasicTextField(state = state, modifier = Modifier.testTag(Tag))
+ }
+ rule.onNodeWithTag(Tag).requestFocus()
+
+ inputMethodInterceptor.withInputConnection {
+ setComposingRegion(0, 5)
+ setComposingText("World", 1)
+ }
+
+ imm.expectCall("updateSelection(5, 5, 0, 5)")
+ }
+
+ @Test
fun perform_sendKeyEvent() {
val state = TextFieldState("Hello")
lateinit var view: View
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
index 3ad3c19..4a2d653 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
@@ -385,7 +385,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(2, 3))
+ assertThat(state.selection).isEqualTo(TextRange(2, 3))
}
}
@@ -406,7 +406,7 @@
rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(2))
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -517,7 +517,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.PasteText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(state.text.toString()).isEqualTo("Hello World!")
}
}
@@ -548,7 +548,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.PasteText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(9))
+ assertThat(state.selection).isEqualTo(TextRange(9))
assertThat(state.text.toString()).isEqualTo("Heo Word!")
}
}
@@ -611,7 +611,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CopyText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
}
@@ -633,7 +633,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo(" World!")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
}
}
@@ -659,7 +659,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Hello World!")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index f5952d3..6e216ca 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -1134,7 +1134,7 @@
rule.waitForIdle()
assertThat(tfs.text.toString()).isEqualTo(longText)
- assertThat(tfs.text.selection).isEqualTo(TextRange(longText.length))
+ assertThat(tfs.selection).isEqualTo(TextRange(longText.length))
}
@Test
@@ -1155,7 +1155,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(imm.expectCall("updateSelection(0, 5, -1, -1)"))
}
}
@@ -1230,7 +1230,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Worldo")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
index 4944195..96cb15d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
@@ -273,7 +273,7 @@
// assertThat selection happened
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
index bcadfb2..2fe532a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
@@ -50,7 +50,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -78,7 +78,7 @@
rule.runOnIdle {
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 12))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 12))
}
}
@@ -109,7 +109,7 @@
rule.runOnIdle {
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 12))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 12))
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
index 36aa45b..bc7f1d1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
@@ -437,7 +437,7 @@
listOf(0, 1, 3, 4).forEachIndexed { i, expectedCursor ->
rule.runOnIdle {
assertWithMessage("After pressing right arrow $i times")
- .that(state.text.selection).isEqualTo(TextRange(expectedCursor))
+ .that(state.selection).isEqualTo(TextRange(expectedCursor))
}
rule.onNodeWithTag(Tag).performKeyInput {
pressKey(Key.DirectionRight)
@@ -477,7 +477,7 @@
).forEachIndexed { i, expectedSelection ->
rule.runOnIdle {
assertWithMessage("After pressing shift+right arrow $i times")
- .that(state.text.selection).isEqualTo(expectedSelection)
+ .that(state.selection).isEqualTo(expectedSelection)
}
rule.onNodeWithTag(Tag).performKeyInput {
withKeyDown(Key.ShiftLeft) {
@@ -519,7 +519,7 @@
).forEachIndexed { i, expectedSelection ->
rule.runOnIdle {
assertWithMessage("After pressing shift+left arrow $i times")
- .that(state.text.selection).isEqualTo(expectedSelection)
+ .that(state.selection).isEqualTo(expectedSelection)
}
rule.onNodeWithTag(Tag).performKeyInput {
withKeyDown(Key.ShiftLeft) {
@@ -815,7 +815,7 @@
.that(performSelectionOnVisualText(write)).isTrue()
rule.runOnIdle {
assertWithMessage("Visual selection $write to mapped")
- .that(text.selection).isEqualTo(expected)
+ .that(selection).isEqualTo(expected)
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
index e062e6e..a982889 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
@@ -125,7 +125,7 @@
private var textLayoutResult: (() -> TextLayoutResult?)? = null
private val cursorRect: Rect
// assume selection is collapsed
- get() = textLayoutResult?.invoke()?.getCursorRect(state.text.selection.start)
+ get() = textLayoutResult?.invoke()?.getCursorRect(state.selection.start)
?: Rect.Zero
private val cursorSize: DpSize by lazy {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
index 90f6621..ff0558e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
@@ -78,9 +78,9 @@
@Test
fun nonTextContent_isNotAccepted() {
rule.setContentAndTestDragAndDrop {
- val startSelection = state.text.selection
+ val startSelection = state.selection
drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(startSelection)
+ assertThat(state.selection).isEqualTo(startSelection)
}
}
@@ -93,7 +93,7 @@
) {
val accepted = drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
assertThat(accepted).isTrue()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -101,7 +101,7 @@
fun textContent_isAccepted() {
rule.setContentAndTestDragAndDrop {
drag(Offset(fontSize.toPx() * 2, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -109,11 +109,11 @@
fun draggingText_updatesSelection() {
rule.setContentAndTestDragAndDrop {
drag(Offset(fontSize.toPx() * 1, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
drag(Offset(fontSize.toPx() * 2, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
drag(Offset(fontSize.toPx() * 3, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
}
@@ -125,11 +125,11 @@
}
) {
drag(Offset(fontSize.toPx() * 1, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
drag(Offset(fontSize.toPx() * 3, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
}
@@ -140,9 +140,9 @@
modifier = Modifier.width(300.dp)
) {
drag(Offset.Zero, "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
drag(Offset(295.dp.toPx(), 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
}
@@ -354,7 +354,7 @@
" Awesome"
)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange("Hello Awesome".length))
+ assertThat(state.selection).isEqualTo(TextRange("Hello Awesome".length))
assertThat(state.text.toString()).isEqualTo("Hello Awesome World!")
}
}
@@ -378,7 +378,7 @@
}
drag(Offset(fontSize.toPx() * 5, 10f), clipData)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange("Hello Awesome".length))
+ assertThat(state.selection).isEqualTo(TextRange("Hello Awesome".length))
assertThat(state.text.toString()).isEqualTo("Hello Awesome World!")
assertThat(receivedContent.clipEntry.clipData.itemCount).isEqualTo(2)
assertThat(receivedContent.clipEntry.firstUriOrNull()).isEqualTo(defaultUri)
@@ -402,7 +402,7 @@
}
drag(Offset(fontSize.toPx() * 5, 10f), clipData)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(state.text.toString()).isEqualTo("Hello World!")
assertThat(receivedContent.clipEntry.clipData.itemCount).isEqualTo(2)
assertThat(receivedContent.clipEntry.firstUriOrNull()).isEqualTo(defaultUri)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
index bafdbfa..2e16280 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
@@ -689,7 +689,7 @@
fun expectedSelection(selection: TextRange) {
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(selection)
+ assertThat(state.selection).isEqualTo(selection)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
index 599b9bf..7787cf8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
@@ -77,7 +77,7 @@
click(center + Offset(1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
}
rule.onNodeWithTag(Tag).performTouchInput {
@@ -87,7 +87,7 @@
click(center + Offset(-1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
}
@@ -124,7 +124,7 @@
click(topRight)
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(indexOfA))
+ assertThat(text.selection).isEqualTo(TextRange(indexOfA))
}
assertCursor(indexOfA)
@@ -134,7 +134,7 @@
click(bottomLeft)
}
rule.runOnIdle {
- assertThat(text.text.selection)
+ assertThat(text.selection)
.isEqualTo(TextRange(indexOfA + 1))
}
assertCursor(indexOfA + replacement.length)
@@ -164,7 +164,7 @@
click(center + Offset(1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
assertCursor(5)
@@ -175,7 +175,7 @@
click(center + Offset(-1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
assertCursor(1)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
index 768fd70..c10b343 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
@@ -78,13 +78,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(3))
+ assertThat(text.selection).isEqualTo(TextRange(3))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(4))
+ assertThat(text.selection).isEqualTo(TextRange(4))
}
@Test
@@ -100,13 +100,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(4))
+ assertThat(text.selection).isEqualTo(TextRange(4))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(3))
+ assertThat(text.selection).isEqualTo(TextRange(3))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
}
@Test
@@ -181,13 +181,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
}
@Test
@@ -203,13 +203,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
index 8781298..6a566ee 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
@@ -458,7 +458,7 @@
rule.runOnIdle {
assertThat(scrollState.value).isEqualTo(scrollState.maxValue)
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -591,9 +591,9 @@
rule.onNodeWithTag("field").assertTextEquals("aaaaaaaaaa")
rule.waitUntil(
"scrollState.value (${scrollState.value}) == 0 && " +
- "state.text.selection (${state.text.selection}) == TextRange(0)"
+ "state.selection (${state.selection}) == TextRange(0)"
) {
- scrollState.value == 0 && state.text.selection == TextRange(0)
+ scrollState.value == 0 && state.selection == TextRange(0)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
index 73e32c2..f944f79 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
@@ -125,8 +125,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
@@ -186,8 +186,8 @@
// Monitoring update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(Offset(67f, 89f)),
innerTextFieldBounds = Rect.Zero,
@@ -208,8 +208,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
@@ -225,8 +225,8 @@
// Monitoring update.
val expectedInfo2 = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(Offset(67f, 89f)),
innerTextFieldBounds = Rect.Zero,
@@ -247,8 +247,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
index d848cac..687eb4c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
@@ -82,15 +82,15 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(center) }
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(left + 1f, top + 1f)) } // topLeft
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(right - 1f, top + 1f)) } // topRight
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(left + 1f, bottom - 1f)) } // bottomLeft
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(right - 1f, bottom - 1f)) } // bottomRight
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
}
}
@@ -111,7 +111,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -133,7 +133,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(right - (fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -155,7 +155,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(right - (fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -175,7 +175,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -195,7 +195,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 4).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -217,7 +217,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(fontSize.toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -242,7 +242,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(6))
+ assertThat(state.selection).isEqualTo(TextRange(6))
assertThat(scrollState.value).isGreaterThan(0)
}
}
@@ -268,7 +268,7 @@
performTouchInput { click(Offset(fontSize.toPx(), bottom - 1f)) }
}
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(scrollState.value).isGreaterThan(0)
}
@@ -300,6 +300,6 @@
performTouchInput { click(Offset(2 * fontSize.toPx(), centerY)) }
}
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
index 728e321..8b210f4 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
@@ -102,7 +102,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(2 * fontSize.value).dp + cursorWidth / 2,
@@ -155,7 +155,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
@Test
@@ -177,7 +177,7 @@
click(Offset(fontSize.toPx() * 8, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
5 * fontSizeDp + cursorWidth / 2,
@@ -206,7 +206,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(2 * fontSize.value).dp + cursorWidth / 2,
@@ -233,7 +233,7 @@
click(Offset(fontSize.toPx() * 8, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(5 * fontSize.value).dp + cursorWidth / 2,
@@ -604,7 +604,7 @@
swipeToRight(fontSizePx * 5)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
// region ltr drag tests
@@ -629,7 +629,7 @@
swipeToRight(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -653,7 +653,7 @@
swipeToLeft(fontSizePx)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -678,7 +678,7 @@
swipeToRight(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -702,7 +702,7 @@
swipeToLeft(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
@@ -730,7 +730,7 @@
swipeToRight(getTextFieldWidth() * 3)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -757,12 +757,12 @@
swipeToRight(fontSizePx * 12, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(12))
+ assertThat(state.selection).isEqualTo(TextRange(12))
}
swipeToRight(fontSizePx * 2, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(14))
+ assertThat(state.selection).isEqualTo(TextRange(14))
}
}
@@ -792,7 +792,7 @@
swipeToRight(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -818,7 +818,7 @@
swipeToLeft(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -844,7 +844,7 @@
swipeToRight(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
@@ -870,7 +870,7 @@
swipeToLeft(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -902,7 +902,7 @@
swipeToLeft(getTextFieldWidth() * 3)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -936,12 +936,12 @@
swipeToLeft(fontSizePx * 12, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(12))
+ assertThat(state.selection).isEqualTo(TextRange(12))
}
swipeToLeft(fontSizePx * 2, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(14))
+ assertThat(state.selection).isEqualTo(TextRange(14))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
index 5d0f3ce..509edf6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
@@ -129,7 +129,7 @@
}
rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -183,7 +183,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
@@ -203,8 +203,8 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isNotEqualTo(TextRange(7, 8))
- assertThat(state.text.selection.collapsed).isFalse()
+ assertThat(state.selection).isNotEqualTo(TextRange(7, 8))
+ assertThat(state.selection.collapsed).isFalse()
}
@Test
@@ -232,7 +232,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(20, 23))
+ assertThat(state.selection).isEqualTo(TextRange(20, 23))
}
@Test
@@ -262,7 +262,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
@@ -282,7 +282,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
@Test
@@ -302,7 +302,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
@Test
@@ -322,7 +322,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 15))
+ assertThat(state.selection).isEqualTo(TextRange(4, 15))
}
@Test
@@ -342,7 +342,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 15))
+ assertThat(state.selection).isEqualTo(TextRange(4, 15))
}
@Test
@@ -364,7 +364,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
//region RTL
@@ -386,7 +386,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
@Test
@@ -406,7 +406,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
@Test
@@ -426,7 +426,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 11))
+ assertThat(state.selection).isEqualTo(TextRange(0, 11))
}
@Test
@@ -446,7 +446,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 11))
+ assertThat(state.selection).isEqualTo(TextRange(0, 11))
}
@Test
@@ -470,7 +470,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
index 43f45e8..466b0a6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
@@ -335,13 +335,13 @@
rule.onNodeWithTag(TAG).performTouchInput { swipeLeft() }
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(TAG).performTouchInput { swipeRight() }
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -368,7 +368,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(TAG).performTouchInput {
@@ -376,7 +376,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -412,7 +412,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(containerTag).performTouchInput {
@@ -420,7 +420,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -456,7 +456,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(containerTag).performTouchInput {
@@ -464,7 +464,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -485,7 +485,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx * 4)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
}
@@ -506,7 +506,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx * 4)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
}
@@ -529,7 +529,7 @@
doubleClick(Offset(fontSizePx * 5, fontSizePx / 2)) // middle word
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
}
@@ -554,8 +554,8 @@
doubleClick(Offset(fontSizePx * 3.5f, fontSizePx / 2))
}
rule.runOnIdle {
- assertThat(state.text.selection).isNotEqualTo(TextRange(3, 4))
- assertThat(state.text.selection.collapsed).isFalse()
+ assertThat(state.selection).isNotEqualTo(TextRange(3, 4))
+ assertThat(state.selection.collapsed).isFalse()
}
}
@@ -585,7 +585,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx)
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -615,7 +615,7 @@
// make sure that we also swipe to start on the first line
swipeToLeft(Handle.SelectionStart, fontSizePx * 10)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -640,7 +640,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx)
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -669,7 +669,7 @@
swipeToRight(Handle.SelectionEnd, layoutResult.size.width.toFloat())
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -691,7 +691,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx * 2) // only move by 2 characters
rule.runOnIdle {
// selection extends by a word
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
}
@@ -713,7 +713,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx * 2) // only move by 2 characters
rule.runOnIdle {
// selection extends by a word
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
}
@@ -735,7 +735,7 @@
swipeToRight(Handle.SelectionStart, fontSizePx) // only move by a single character
rule.runOnIdle {
// selection shrinks by a character
- assertThat(state.text.selection).isEqualTo(TextRange(5, 7))
+ assertThat(state.selection).isEqualTo(TextRange(5, 7))
}
}
@@ -757,7 +757,7 @@
swipeToLeft(Handle.SelectionEnd, fontSizePx) // only move by a single character
rule.runOnIdle {
// selection shrinks by a character
- assertThat(state.text.selection).isEqualTo(TextRange(4, 6))
+ assertThat(state.selection).isEqualTo(TextRange(4, 6))
}
}
@@ -778,7 +778,7 @@
swipeToRight(Handle.SelectionStart, fontSizePx * 7)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(11, 7))
+ assertThat(state.selection).isEqualTo(TextRange(11, 7))
}
}
@@ -799,7 +799,7 @@
swipeToLeft(Handle.SelectionEnd, fontSizePx * 7)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 0))
+ assertThat(state.selection).isEqualTo(TextRange(4, 0))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
index 25801cc..c7d8baa 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
@@ -78,7 +78,7 @@
textNode.performKeyInput { pressKey(Key.Back) }
val expected = TextRange(3, 3)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
}
}
@@ -106,7 +106,7 @@
textNode.performKeyInput { pressKey(Key.Back) }
val expected = TextRange(3, 3)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
assertThat(backPressed).isEqualTo(0)
}
}
@@ -129,7 +129,7 @@
// should have no effect
textNode.performKeyInput { keyDown(Key.Back) }
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
index 0106d69..2577a43 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
@@ -193,7 +193,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5, 2))
+ assertThat(state.selection).isEqualTo(TextRange(5, 2))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
@@ -206,7 +206,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -226,7 +226,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -243,7 +243,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -262,7 +262,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Shown)
}
}
@@ -494,7 +494,7 @@
selectAllOption?.invoke()
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
rule.runOnIdle {
assertThat(selectAllOption).isNull()
}
@@ -615,7 +615,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Heworldllo")
- assertThat(state.text.selection).isEqualTo(TextRange(7))
+ assertThat(state.selection).isEqualTo(TextRange(7))
}
}
@@ -688,7 +688,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -715,7 +715,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("ello")
assertThat(state.text.toString()).isEqualTo("H World!")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
}
@@ -747,7 +747,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("ello")
assertThat(state.text.toString()).isEqualTo("Hello World!")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
index cca04f5..7ac3f35 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
@@ -143,7 +143,7 @@
fun assertSelectionEquals(selectionRange: Pair<Int, Int>) {
val (start, end) = selectionRange
- assertThat(textFieldState.text.selection).isEqualTo(TextRange(start, end))
+ assertThat(textFieldState.selection).isEqualTo(TextRange(start, end))
}
fun assertNoMagnifierExists() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
index e0051fd..901345d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
@@ -212,7 +212,7 @@
state.undoState.undo()
rule.runOnIdle {
- assertThat(state.text.selection).isNotEqualTo(TextRange(7))
+ assertThat(state.selection).isNotEqualTo(TextRange(7))
}
state.undoState.redo()
@@ -368,7 +368,7 @@
private fun TextFieldState.assertTextAndSelection(text: String, selection: TextRange) {
rule.runOnIdle {
assertThat(this.text.toString()).isEqualTo(text)
- assertThat(this.text.selection).isEqualTo(selection)
+ assertThat(this.selection).isEqualTo(selection)
}
}
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
index d90df9e..861830f 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/AndroidTextInputSession.android.kt
@@ -80,8 +80,8 @@
composeImm.updateSelection(
selectionStart = newSelection.min,
selectionEnd = newSelection.max,
- compositionStart = oldComposition?.min ?: -1,
- compositionEnd = oldComposition?.max ?: -1
+ compositionStart = newComposition?.min ?: -1,
+ compositionEnd = newComposition?.max ?: -1
)
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
index 6dbf160..9437e55 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
@@ -37,7 +37,7 @@
assertNotNull(restoredState)
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 5))
}
@Test
@@ -57,7 +57,7 @@
assertThat(restoredState.undoState.canUndo).isTrue()
restoredState.undoState.undo()
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 5))
}
private object TestSaverScope : SaverScope {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
index 5aab181..4afc7a0 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.input
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.ui.text.TextRange
@@ -27,6 +28,7 @@
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
@@ -47,21 +49,21 @@
fun defaultInitialTextAndSelection() {
val state = TextFieldState()
assertThat(state.text.toString()).isEqualTo("")
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
fun customInitialTextAndDefaultSelection() {
val state = TextFieldState(initialText = "hello")
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
fun customInitialTextAndSelection() {
val state = TextFieldState(initialText = "hello", initialSelection = TextRange(0, 1))
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 1))
+ assertThat(state.selection).isEqualTo(TextRange(0, 1))
}
@Test
@@ -290,7 +292,7 @@
replace(0, 0, "hello")
placeCursorAtEnd()
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
@@ -299,7 +301,7 @@
replace(0, 0, "hello")
placeCursorBeforeCharAt(2)
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -321,7 +323,7 @@
replace(0, 0, "hello")
selectAll()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
@Test
@@ -330,7 +332,7 @@
replace(0, 0, "hello")
selection = TextRange(1, 4)
}
- assertThat(state.text.selection).isEqualTo(TextRange(1, 4))
+ assertThat(state.selection).isEqualTo(TextRange(1, 4))
}
@Test
@@ -398,14 +400,7 @@
fun setTextAndPlaceCursorAtEnd_works() {
state.setTextAndPlaceCursorAtEnd("Hello")
assertThat(state.text.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
- }
-
- @Test
- fun setTextAndSelectAll_works() {
- state.setTextAndSelectAll("Hello")
- assertThat(state.text.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
@@ -466,11 +461,11 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
assertThat(texts).hasSize(1)
- assertThat(texts.single()).isSameInstanceAs(state.text)
+ assertThat(texts.single()).isSameInstanceAs(state.value)
assertThat(texts.single().toString()).isEqualTo("hello")
assertThat(texts.single().selection).isEqualTo(TextRange(5))
}
@@ -479,10 +474,10 @@
fun forEachValue_fires_whenTextChanged() = runTestWithSnapshotsThenCancelChildren {
val state = TextFieldState(initialSelection = TextRange(0))
val texts = mutableListOf<TextFieldCharSequence>()
- val initialText = state.text
+ val initialSelection = state.selection
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -491,9 +486,9 @@
}
assertThat(texts).hasSize(2)
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello")
- assertThat(texts.last().selection).isEqualTo(initialText.selection)
+ assertThat(texts.last().selection).isEqualTo(initialSelection)
}
@Test
@@ -502,7 +497,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -510,7 +505,7 @@
}
assertThat(texts).hasSize(2)
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello")
assertThat(texts.last().selection).isEqualTo(TextRange(5))
}
@@ -521,7 +516,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -536,7 +531,7 @@
assertThat(texts).hasSize(3)
assertThat(texts[1].toString()).isEqualTo("hello")
- assertThat(texts[2]).isSameInstanceAs(state.text)
+ assertThat(texts[2]).isSameInstanceAs(state.value)
assertThat(texts[2].toString()).isEqualTo("hello world")
}
@@ -547,7 +542,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -556,7 +551,7 @@
placeCursorAtEnd()
}
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello world")
}
@@ -567,7 +562,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
val snapshot = Snapshot.takeMutableSnapshot()
@@ -583,7 +578,7 @@
snapshot.apply()
snapshot.dispose()
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
}
@Test
@@ -593,7 +588,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
val snapshot = Snapshot.takeMutableSnapshot()
@@ -617,7 +612,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue {
+ state.valueAsFlow().collectLatest {
texts += it
awaitCancellation()
}
@@ -631,6 +626,32 @@
.inOrder()
}
+ @Test
+ fun snapshotFlowOfText_onlyFiresIfContentChanges() {
+ runTestWithSnapshotsThenCancelChildren {
+ val state = TextFieldState()
+ val texts = mutableListOf<CharSequence>()
+
+ launch(Dispatchers.Unconfined) {
+ snapshotFlow {
+ state.text
+ }.collect {
+ texts += it
+ }
+ }
+
+ state.edit { append("a") }
+ state.edit { append("b") }
+ state.edit { placeCursorBeforeCharAt(0) }
+ state.edit { placeCursorAtEnd() }
+ state.edit { append("c") }
+
+ assertThat(texts.map { it.toString() })
+ .containsExactly("", "a", "ab", "abc")
+ .inOrder()
+ }
+ }
+
private fun runTestWithSnapshotsThenCancelChildren(testBody: suspend TestScope.() -> Unit) {
val globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
// This is normally done by the compose runtime.
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
index 168217a..bd3f689 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
@@ -36,7 +36,7 @@
val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
val state = TextFieldState(firstValue)
- assertThat(state.text).isEqualTo(firstValue)
+ assertThat(state.value).isEqualTo(firstValue)
}
@Test
@@ -51,7 +51,7 @@
}
state.editAsUser { commitText("X", 1) }
- val newState = state.text
+ val newState = state.value
assertThat(newState.toString()).isEqualTo("XABCDE")
assertThat(newState.selection.min).isEqualTo(1)
@@ -73,7 +73,7 @@
}
state.editAsUser { setSelection(0, 2) }
- val newState = state.text
+ val newState = state.value
assertThat(newState.toString()).isEqualTo("ABCDE")
assertThat(newState.selection.min).isEqualTo(0)
@@ -241,13 +241,13 @@
val newValue =
TextFieldCharSequence(
"cd",
- state.text.selection,
- state.text.composition
+ state.selection,
+ state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
assertThat(resetCalled).isEqualTo(1)
assertThat(selectionCalled).isEqualTo(1)
}
@@ -267,13 +267,13 @@
val newValue =
TextFieldCharSequence(
state.text,
- state.text.selection,
- state.text.composition
+ state.selection,
+ state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isEqualTo(composition)
+ assertThat(state.composition).isEqualTo(composition)
}
@Test
@@ -290,13 +290,13 @@
val newValue =
TextFieldCharSequence(
state.text,
- state.text.selection,
+ state.selection,
composition = TextRange(0, 2)
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
}
@Test
@@ -312,13 +312,13 @@
// change the composition
val newValue = TextFieldCharSequence(
state.text,
- state.text.selection,
+ state.selection,
composition = TextRange(0, 1)
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
}
@Test
@@ -340,13 +340,13 @@
val newValue = TextFieldCharSequence(
state.text,
selection = newSelection,
- composition = state.text.composition
+ composition = state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isEqualTo(composition)
- assertThat(state.text.selection).isEqualTo(newSelection)
+ assertThat(state.composition).isEqualTo(composition)
+ assertThat(state.selection).isEqualTo(newSelection)
}
@Test
@@ -541,7 +541,7 @@
state.editAsUser { setComposingRegion(2, 3) }
- assertThat(state.text.composition).isEqualTo(TextRange(2, 3))
+ assertThat(state.composition).isEqualTo(TextRange(2, 3))
}
@Test
@@ -552,7 +552,7 @@
state.editAsUser { setComposingRegion(2, 3) }
- assertThat(state.text.composition).isEqualTo(TextRange(2, 3))
+ assertThat(state.composition).isEqualTo(TextRange(2, 3))
}
private fun TextFieldState(
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
index 029b76d..f9ce97e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
@@ -43,7 +43,7 @@
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
}
@@ -59,7 +59,7 @@
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
}
@@ -73,19 +73,19 @@
TransformedTextFieldState(state, outputTransformation = outputTransformation)
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(4))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
@@ -102,19 +102,19 @@
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(4))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(0))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
@@ -130,11 +130,11 @@
TransformedTextFieldState(state, outputTransformation = outputTransformation)
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1, 3))
+ assertThat(state.selection).isEqualTo(TextRange(1, 3))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(2))
}
@@ -149,11 +149,11 @@
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(2))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1, 3))
+ assertThat(state.selection).isEqualTo(TextRange(1, 3))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(0))
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
index 6c07153..ea0d8d1 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
@@ -76,7 +76,7 @@
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ac")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -160,15 +160,15 @@
state.typeAtStart("c") // "|a" -> "c|a"
assertThat(state.text.toString()).isEqualTo("ca")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("a")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -182,15 +182,15 @@
state.typeAtEnd("g") // "defg|"
assertThat(state.text.toString()).isEqualTo("defg")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("def")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("abcd")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
@Test
@@ -202,15 +202,15 @@
state.deleteAt(2) // "de|"
assertThat(state.text.toString()).isEqualTo("de")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("def")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -222,15 +222,15 @@
state.typeAtEnd("c") // "ab\nc|"
assertThat(state.text.toString()).isEqualTo("ab\nc")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab\n")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -243,11 +243,11 @@
state.type("d") // "d|c"
assertThat(state.text.toString()).isEqualTo("dc")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("abc")
- assertThat(state.text.selection).isEqualTo(TextRange(2, 0))
+ assertThat(state.selection).isEqualTo(TextRange(2, 0))
}
@Test
@@ -258,14 +258,14 @@
state.type("e") // "e|cd"
assertThat(state.text.toString()).isEqualTo("ecd")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo() // "|ab|cd"
state.undoState.undo() // "|abc"
state.undoState.redo() // "abcd|"
assertThat(state.text.toString()).isEqualTo("abcd")
- assertThat(state.text.selection).isEqualTo(TextRange(4, 4))
+ assertThat(state.selection).isEqualTo(TextRange(4, 4))
}
@Test
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 5ca3dbde..670fe29 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -39,6 +39,7 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Measurable
@@ -51,10 +52,10 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.getScrollViewportLength
import androidx.compose.ui.semantics.horizontalScrollAxisRange
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollByOffset
import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastRoundToInt
@@ -366,9 +367,14 @@
}
)
- getScrollViewportLength {
- it.add(state.viewportSize.toFloat())
- true
+ scrollByOffset { offset ->
+ if (isVertical) {
+ val consumed = (state as ScrollableState).animateScrollBy(offset.y)
+ Offset(0f, consumed)
+ } else {
+ val consumed = (state as ScrollableState).animateScrollBy(offset.x)
+ Offset(consumed, 0f)
+ }
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index 0d055ac..9adcd08 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -60,6 +60,401 @@
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+// start Overscroll
+// This file contains both a Modifier.anchoredDraggable overload with overscroll and without
+// Everything below the Overscroll part should be copied to M2/M3 and kept in sync.
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
+ * When the drag ends, the offset will be animated to one of the anchors and when that anchor is
+ * reached, the value of the [AnchoredDraggableState] will also be updated to the value
+ * corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom
+ * drag will behave like bottom to top, and a left to right drag will behave like right to left.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to
+ * the internal [Modifier.draggable].
+ * @param overscrollEffect optional effect to dispatch any excess delta or velocity to. The excess
+ * delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an
+ * [overscrollEffect], make sure to apply [androidx.compose.foundation.overscroll] to render the
+ * effect as well.
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ */
+@ExperimentalFoundationApi
+fun <T> Modifier.anchoredDraggable(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ interactionSource: MutableInteractionSource? = null,
+ overscrollEffect: OverscrollEffect? = null,
+ startDragImmediately: Boolean = state.isAnimationRunning
+): Modifier = this then AnchoredDraggableOverscrollElement(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ overscrollEffect = overscrollEffect,
+ startDragImmediately = startDragImmediately
+)
+
+@OptIn(ExperimentalFoundationApi::class)
+private class AnchoredDraggableOverscrollElement<T>(
+ private val state: AnchoredDraggableState<T>,
+ private val orientation: Orientation,
+ private val enabled: Boolean,
+ private val reverseDirection: Boolean,
+ private val interactionSource: MutableInteractionSource?,
+ private val startDragImmediately: Boolean,
+ private val overscrollEffect: OverscrollEffect?,
+) : ModifierNodeElement<AnchoredDraggableOverscrollNode<T>>() {
+ override fun create() = AnchoredDraggableOverscrollNode(
+ state,
+ orientation,
+ enabled,
+ reverseDirection,
+ interactionSource,
+ startDragImmediately,
+ overscrollEffect,
+ )
+
+ override fun update(node: AnchoredDraggableOverscrollNode<T>) {
+ node.update(
+ state,
+ orientation,
+ enabled,
+ reverseDirection,
+ interactionSource,
+ overscrollEffect,
+ startDragImmediately
+ )
+ }
+
+ override fun hashCode(): Int {
+ var result = state.hashCode()
+ result = 31 * result + orientation.hashCode()
+ result = 31 * result + enabled.hashCode()
+ result = 31 * result + reverseDirection.hashCode()
+ result = 31 * result + interactionSource.hashCode()
+ result = 31 * result + startDragImmediately.hashCode()
+ result = 31 * result + overscrollEffect.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+
+ if (other !is AnchoredDraggableOverscrollElement<*>) return false
+
+ if (state != other.state) return false
+ if (orientation != other.orientation) return false
+ if (enabled != other.enabled) return false
+ if (reverseDirection != other.reverseDirection) return false
+ if (interactionSource != other.interactionSource) return false
+ if (startDragImmediately != other.startDragImmediately) return false
+ if (overscrollEffect != other.overscrollEffect) return false
+
+ return true
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "anchoredDraggable"
+ properties["state"] = state
+ properties["orientation"] = orientation
+ properties["enabled"] = enabled
+ properties["reverseDirection"] = reverseDirection
+ properties["interactionSource"] = interactionSource
+ properties["startDragImmediately"] = startDragImmediately
+ properties["overscrollEffect"] = overscrollEffect
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class AnchoredDraggableOverscrollNode<T>(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean,
+ reverseDirection: Boolean,
+ interactionSource: MutableInteractionSource?,
+ startDragImmediately: Boolean,
+ private var overscrollEffect: OverscrollEffect?,
+) : AnchoredDraggableNode<T>(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ startDragImmediately = startDragImmediately
+) {
+ override suspend fun AnchoredDragScope.anchoredDrag(
+ forEachDelta: suspend ((dragDelta: DragEvent.DragDelta) -> Unit) -> Unit
+ ) {
+ forEachDelta { dragDelta ->
+ if (overscrollEffect == null) {
+ dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().toFloat()))
+ } else {
+ overscrollEffect!!.applyToScroll(
+ delta = dragDelta.delta.reverseIfNeeded(),
+ source = NestedScrollSource.Drag
+ ) { deltaForDrag ->
+ val dragOffset = state.newOffsetForDelta(deltaForDrag.toFloat())
+ val consumedDelta = (dragOffset - state.requireOffset()).toOffset()
+ dragTo(dragOffset)
+ consumedDelta
+ }
+ }
+ }
+ }
+
+ override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
+ if (overscrollEffect == null) {
+ state.settle(velocity.reverseIfNeeded().toFloat()).toVelocity()
+ } else {
+ overscrollEffect!!.applyToFling(
+ velocity = velocity.reverseIfNeeded()
+ ) { availableVelocity ->
+ val consumed = state.settle(availableVelocity.toFloat()).toVelocity()
+ val currentOffset = state.requireOffset()
+ val minAnchor = state.anchors.minAnchor()
+ val maxAnchor = state.anchors.maxAnchor()
+ // return consumed velocity only if we are reaching the min/max anchors
+ if (currentOffset >= maxAnchor || currentOffset <= minAnchor) {
+ consumed
+ } else {
+ availableVelocity
+ }
+ }
+ }
+ }
+
+ fun update(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean,
+ reverseDirection: Boolean,
+ interactionSource: MutableInteractionSource?,
+ overscrollEffect: OverscrollEffect?,
+ startDragImmediately: Boolean
+ ) {
+ this.overscrollEffect = overscrollEffect
+ update(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ startDragImmediately = startDragImmediately
+ )
+ }
+}
+// end Overscroll
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
+ * When the drag ends, the offset will be animated to one of the anchors and when that anchor is
+ * reached, the value of the [AnchoredDraggableState] will also be updated to the value
+ * corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom
+ * drag will behave like bottom to top, and a left to right drag will behave like right to left.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to
+ * the internal [Modifier.draggable].
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ */
+@ExperimentalFoundationApi
+fun <T> Modifier.anchoredDraggable(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ interactionSource: MutableInteractionSource? = null,
+ startDragImmediately: Boolean = state.isAnimationRunning
+): Modifier = this then AnchoredDraggableElement(
+ state = state,
+ orientation = orientation,
+ enabled = enabled,
+ reverseDirection = reverseDirection,
+ interactionSource = interactionSource,
+ startDragImmediately = startDragImmediately
+)
+
+@OptIn(ExperimentalFoundationApi::class)
+private class AnchoredDraggableElement<T>(
+ private val state: AnchoredDraggableState<T>,
+ private val orientation: Orientation,
+ private val enabled: Boolean,
+ private val reverseDirection: Boolean,
+ private val interactionSource: MutableInteractionSource?,
+ private val startDragImmediately: Boolean
+) : ModifierNodeElement<AnchoredDraggableNode<T>>() {
+ override fun create() = AnchoredDraggableNode(
+ state,
+ orientation,
+ enabled,
+ reverseDirection,
+ interactionSource,
+ startDragImmediately
+ )
+
+ override fun update(node: AnchoredDraggableNode<T>) {
+ node.update(
+ state,
+ orientation,
+ enabled,
+ reverseDirection,
+ interactionSource,
+ startDragImmediately
+ )
+ }
+
+ override fun hashCode(): Int {
+ var result = state.hashCode()
+ result = 31 * result + orientation.hashCode()
+ result = 31 * result + enabled.hashCode()
+ result = 31 * result + reverseDirection.hashCode()
+ result = 31 * result + interactionSource.hashCode()
+ result = 31 * result + startDragImmediately.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+
+ if (other !is AnchoredDraggableElement<*>) return false
+
+ if (state != other.state) return false
+ if (orientation != other.orientation) return false
+ if (enabled != other.enabled) return false
+ if (reverseDirection != other.reverseDirection) return false
+ if (interactionSource != other.interactionSource) return false
+ if (startDragImmediately != other.startDragImmediately) return false
+
+ return true
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "anchoredDraggable"
+ properties["state"] = state
+ properties["orientation"] = orientation
+ properties["enabled"] = enabled
+ properties["reverseDirection"] = reverseDirection
+ properties["interactionSource"] = interactionSource
+ properties["startDragImmediately"] = startDragImmediately
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private open class AnchoredDraggableNode<T>(
+ protected var state: AnchoredDraggableState<T>,
+ protected var orientation: Orientation,
+ enabled: Boolean,
+ protected var reverseDirection: Boolean,
+ interactionSource: MutableInteractionSource?,
+ protected var startDragImmediately: Boolean
+) : DragGestureNode(
+ canDrag = AlwaysDrag,
+ enabled = enabled,
+ interactionSource = interactionSource
+) {
+
+ open suspend fun AnchoredDragScope.anchoredDrag(
+ forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit
+ ) {
+ forEachDelta { dragDelta ->
+ dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().toFloat()))
+ }
+ }
+
+ override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
+ state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
+ }
+
+ override val pointerDirectionConfig: PointerDirectionConfig
+ get() = orientation.toPointerDirectionConfig()
+
+ override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
+
+ override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
+ state.settle(velocity.reverseIfNeeded().toFloat()).toVelocity()
+ }
+
+ override fun startDragImmediately(): Boolean = startDragImmediately
+
+ fun update(
+ state: AnchoredDraggableState<T>,
+ orientation: Orientation,
+ enabled: Boolean,
+ reverseDirection: Boolean,
+ interactionSource: MutableInteractionSource?,
+ startDragImmediately: Boolean
+ ) {
+ var resetPointerInputHandling = false
+
+ if (this.state != state) {
+ this.state = state
+ resetPointerInputHandling = true
+ }
+ if (this.orientation != orientation) {
+ this.orientation = orientation
+ resetPointerInputHandling = true
+ }
+
+ if (this.reverseDirection != reverseDirection) {
+ this.reverseDirection = reverseDirection
+ resetPointerInputHandling = true
+ }
+
+ this.startDragImmediately = startDragImmediately
+
+ update(
+ enabled = enabled,
+ interactionSource = interactionSource,
+ isResetPointerInputHandling = resetPointerInputHandling,
+ )
+ }
+
+ protected fun Float.toOffset() = Offset(
+ x = if (orientation == Orientation.Horizontal) this else 0f,
+ y = if (orientation == Orientation.Vertical) this else 0f,
+ )
+
+ protected fun Float.toVelocity() = Velocity(
+ x = if (orientation == Orientation.Horizontal) this else 0f,
+ y = if (orientation == Orientation.Vertical) this else 0f,
+ )
+
+ protected fun Velocity.toFloat() =
+ if (orientation == Orientation.Vertical) this.y else this.x
+
+ protected fun Offset.toFloat() =
+ if (orientation == Orientation.Vertical) this.y else this.x
+
+ protected fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
+ protected fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
+}
+
+private val AlwaysDrag: (PointerInputChange) -> Boolean = { true }
+
/**
* Structure that represents the anchors of a [AnchoredDraggableState].
*
@@ -183,244 +578,6 @@
}
/**
- * Enable drag gestures between a set of predefined values.
- *
- * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
- * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
- * When the drag ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [AnchoredDraggableState] will also be updated to the value
- * corresponding to the new anchor.
- *
- * Dragging is constrained between the minimum and maximum anchors.
- *
- * @param state The associated [AnchoredDraggableState].
- * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
- * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
- * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom
- * drag will behave like bottom to top, and a left to right drag will behave like right to left.
- * @param interactionSource Optional [MutableInteractionSource] that will passed on to
- * the internal [Modifier.draggable].
- * @param overscrollEffect optional effect to dispatch any excess delta or velocity to. The excess
- * delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an
- * [overscrollEffect], make sure to apply [androidx.compose.foundation.overscroll] to render the
- * effect as well.
- * @param startDragImmediately when set to false, [draggable] will start dragging only when the
- * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
- * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
- */
-@ExperimentalFoundationApi
-fun <T> Modifier.anchoredDraggable(
- state: AnchoredDraggableState<T>,
- orientation: Orientation,
- enabled: Boolean = true,
- reverseDirection: Boolean = false,
- interactionSource: MutableInteractionSource? = null,
- overscrollEffect: OverscrollEffect? = null,
- startDragImmediately: Boolean = state.isAnimationRunning
-): Modifier = this then AnchoredDraggableElement(
- state = state,
- orientation = orientation,
- enabled = enabled,
- reverseDirection = reverseDirection,
- interactionSource = interactionSource,
- overscrollEffect = overscrollEffect,
- startDragImmediately = startDragImmediately
-)
-
-@OptIn(ExperimentalFoundationApi::class)
-private class AnchoredDraggableElement<T>(
- private val state: AnchoredDraggableState<T>,
- private val orientation: Orientation,
- private val enabled: Boolean,
- private val reverseDirection: Boolean,
- private val interactionSource: MutableInteractionSource?,
- private val overscrollEffect: OverscrollEffect?,
- private val startDragImmediately: Boolean
-) : ModifierNodeElement<AnchoredDraggableNode<T>>() {
- override fun create(): AnchoredDraggableNode<T> {
- return AnchoredDraggableNode(
- state,
- orientation,
- enabled,
- reverseDirection,
- interactionSource,
- overscrollEffect,
- { startDragImmediately }
- )
- }
-
- override fun update(node: AnchoredDraggableNode<T>) {
- node.update(
- state,
- orientation,
- enabled,
- reverseDirection,
- interactionSource,
- overscrollEffect,
- { startDragImmediately }
- )
- }
-
- override fun hashCode(): Int {
- var result = state.hashCode()
- result = 31 * result + orientation.hashCode()
- result = 31 * result + enabled.hashCode()
- result = 31 * result + reverseDirection.hashCode()
- result = 31 * result + interactionSource.hashCode()
- result = 31 * result + overscrollEffect.hashCode()
- result = 31 * result + startDragImmediately.hashCode()
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
-
- if (other !is AnchoredDraggableElement<*>) return false
-
- if (state != other.state) return false
- if (orientation != other.orientation) return false
- if (enabled != other.enabled) return false
- if (reverseDirection != other.reverseDirection) return false
- if (interactionSource != other.interactionSource) return false
- if (overscrollEffect != other.overscrollEffect) return false
- if (startDragImmediately != other.startDragImmediately) return false
-
- return true
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "anchoredDraggable"
- properties["state"] = state
- properties["orientation"] = orientation
- properties["enabled"] = enabled
- properties["reverseDirection"] = reverseDirection
- properties["interactionSource"] = interactionSource
- properties["overscrollEffect"] = overscrollEffect
- properties["startDragImmediately"] = startDragImmediately
- }
-}
-
-@ExperimentalFoundationApi
-private class AnchoredDraggableNode<T>(
- private var state: AnchoredDraggableState<T>,
- private var orientation: Orientation,
- enabled: Boolean,
- private var reverseDirection: Boolean,
- interactionSource: MutableInteractionSource?,
- private var overscrollEffect: OverscrollEffect?,
- private var startDragImmediately: () -> Boolean
-) : DragGestureNode(
- canDrag = AlwaysDrag,
- enabled = enabled,
- interactionSource = interactionSource
-) {
-
- override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
- state.anchoredDrag(MutatePriority.Default) {
- forEachDelta { dragDelta ->
- if (overscrollEffect == null) {
- dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().toFloat()))
- } else {
- overscrollEffect!!.applyToScroll(
- delta = dragDelta.delta.reverseIfNeeded(),
- source = NestedScrollSource.Drag
- ) { deltaForDrag ->
- val dragOffset = state.newOffsetForDelta(deltaForDrag.toFloat())
- val consumedDelta = (dragOffset - state.requireOffset()).toOffset()
- dragTo(dragOffset)
- consumedDelta
- }
- }
- }
- }
- }
-
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = orientation.toPointerDirectionConfig()
-
- override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
-
- override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
- if (overscrollEffect == null) {
- state.settle(velocity.reverseIfNeeded().toFloat()).toVelocity()
- } else {
- overscrollEffect!!.applyToFling(
- velocity = velocity.reverseIfNeeded()
- ) { availableVelocity ->
- val consumed = state.settle(availableVelocity.toFloat()).toVelocity()
- val currentOffset = state.requireOffset()
- val minAnchor = state.anchors.minAnchor()
- val maxAnchor = state.anchors.maxAnchor()
- // return consumed velocity only if we are reaching the min/max anchors
- if (currentOffset >= maxAnchor || currentOffset <= minAnchor) {
- consumed
- } else {
- availableVelocity
- }
- }
- }
- }
-
- override fun startDragImmediately(): Boolean = startDragImmediately.invoke()
-
- fun update(
- state: AnchoredDraggableState<T>,
- orientation: Orientation,
- enabled: Boolean,
- reverseDirection: Boolean,
- interactionSource: MutableInteractionSource?,
- overscrollEffect: OverscrollEffect?,
- startDragImmediately: () -> Boolean
- ) {
- var resetPointerInputHandling = false
-
- if (this.state != state) {
- this.state = state
- resetPointerInputHandling = true
- }
- if (this.orientation != orientation) {
- this.orientation = orientation
- resetPointerInputHandling = true
- }
-
- if (this.reverseDirection != reverseDirection) {
- this.reverseDirection = reverseDirection
- resetPointerInputHandling = true
- }
-
- this.overscrollEffect = overscrollEffect
- this.startDragImmediately = startDragImmediately
-
- update(
- enabled = enabled,
- interactionSource = interactionSource,
- isResetPointerInputHandling = resetPointerInputHandling,
- )
- }
-
- private fun Float.toOffset() = Offset(
- x = if (orientation == Orientation.Horizontal) this else 0f,
- y = if (orientation == Orientation.Vertical) this else 0f,
- )
-
- fun Float.toVelocity() = Velocity(
- x = if (orientation == Orientation.Horizontal) this else 0f,
- y = if (orientation == Orientation.Vertical) this else 0f,
- )
-
- private fun Velocity.toFloat() =
- if (orientation == Orientation.Vertical) this.y else this.x
-
- private fun Offset.toFloat() =
- if (orientation == Orientation.Vertical) this.y else this.x
-
- private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
- private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
-}
-
-private val AlwaysDrag: (PointerInputChange) -> Boolean = { true }
-
-/**
* State of the [anchoredDraggable] modifier.
* Use the constructor overload with anchors if the anchors are defined in composition, or update
* the anchors using [updateAnchors].
@@ -732,11 +889,29 @@
// In the first invocation, we do not have a direction. The previous offset will be
// NaN in the first invocation of dragTo; so we will only initialize in the second
// invocation when we have a direction to calculate the thresholds with
- update(isMovingForward)
+ // update(isMovingForward)
+ initialize(isMovingForward)
+ val crossedThresholdTowardsNextAnchor = if (isMovingForward) {
+ newOffset >= absoluteThresholdToCross
+ } else {
+ newOffset <= absoluteThresholdToCross
+ }
+ if (crossedThresholdTowardsNextAnchor) {
+ update(isMovingForward)
+ }
initialized = true
}
}
+ fun initialize(isMovingForward: Boolean) {
+ val currentAnchorPosition = anchors.positionOf(currentValue)
+ val nextAnchor = anchors.closestAnchor(offset, isMovingForward) ?: currentValue
+ val nextAnchorPosition = anchors.positionOf(nextAnchor!!)
+ val relativeThreshold = (nextAnchorPosition - currentAnchorPosition) / 2f
+ absoluteThresholdToCross = currentAnchorPosition + relativeThreshold
+ nextValue = nextAnchor
+ }
+
fun update(isMovingForward: Boolean) {
val currentAnchorPosition = anchors.positionOf(currentValue)
min = anchors.minAnchor()
@@ -785,11 +960,13 @@
anchoredDragScope.block(latestAnchors)
}
val closest = anchors.closestAnchor(offset)
- if (closest != null && confirmValueChange.invoke(closest)) {
+ if (closest != null) {
val closestAnchorOffset = anchors.positionOf(closest)
- anchoredDragScope.dragTo(closestAnchorOffset, lastVelocity)
- settledValue = closest
- currentValue = closest
+ val isAtClosestAnchor = abs(offset - closestAnchorOffset) < 0.5f
+ if (isAtClosestAnchor && confirmValueChange.invoke(closest)) {
+ settledValue = closest
+ currentValue = closest
+ }
}
}
}
@@ -938,9 +1115,9 @@
) {
with(anchoredDragScope) {
val targetOffset = anchors.positionOf(latestTarget)
- if (!targetOffset.isNaN()) {
+ var prev = if (offset.isNaN()) 0f else offset
+ if (!targetOffset.isNaN() && prev != targetOffset) {
debugLog { "Target animation is used" }
- var prev = if (offset.isNaN()) 0f else offset
animate(prev, targetOffset, velocity, snapAnimationSpec) { value, velocity ->
// Our onDrag coerces the value within the bounds, but an animation may
// overshoot, for example a spring animation or an overshooting interpolator
@@ -997,44 +1174,48 @@
val targetOffset = anchors.positionOf(latestTarget)
if (!targetOffset.isNaN()) {
var prev = if (offset.isNaN()) 0f else offset
- // If targetOffset is not in the same direction as the direction of the drag (sign
- // of the velocity) we fall back to using target animation.
- // If the component is at the target offset already, we use decay animation that will
- // not consume any velocity.
- if (velocity * (targetOffset - prev) < 0f || velocity == 0f) {
- animateTo(velocity, this, anchors, latestTarget)
- remainingVelocity = 0f
- } else {
- val projectedDecayOffset = decayAnimationSpec.calculateTargetValue(prev, velocity)
- debugLog {
- "offset = $prev\tvelocity = $velocity\t" +
- "targetOffset = $targetOffset\tprojectedOffset = $projectedDecayOffset"
- }
-
- val canDecayToTarget = if (velocity > 0) {
- projectedDecayOffset >= targetOffset
- } else {
- projectedDecayOffset <= targetOffset
- }
- if (canDecayToTarget) {
- debugLog { "Decay animation is used" }
- AnimationState(prev, velocity)
- .animateDecay(decayAnimationSpec) {
- if (abs(value) >= abs(targetOffset)) {
- val finalValue = value.coerceToTarget(targetOffset)
- dragTo(finalValue, this.velocity)
- remainingVelocity = if (this.velocity.isNaN()) 0f else this.velocity
- prev = finalValue
- cancelAnimation()
- } else {
- dragTo(value, this.velocity)
- remainingVelocity = this.velocity
- prev = value
- }
- }
- } else {
+ if (prev != targetOffset) {
+ // If targetOffset is not in the same direction as the direction of the drag (sign
+ // of the velocity) we fall back to using target animation.
+ // If the component is at the target offset already, we use decay animation that will
+ // not consume any velocity.
+ if (velocity * (targetOffset - prev) < 0f || velocity == 0f) {
animateTo(velocity, this, anchors, latestTarget)
remainingVelocity = 0f
+ } else {
+ val projectedDecayOffset =
+ decayAnimationSpec.calculateTargetValue(prev, velocity)
+ debugLog {
+ "offset = $prev\tvelocity = $velocity\t" +
+ "targetOffset = $targetOffset\tprojectedOffset = $projectedDecayOffset"
+ }
+
+ val canDecayToTarget = if (velocity > 0) {
+ projectedDecayOffset >= targetOffset
+ } else {
+ projectedDecayOffset <= targetOffset
+ }
+ if (canDecayToTarget) {
+ debugLog { "Decay animation is used" }
+ AnimationState(prev, velocity)
+ .animateDecay(decayAnimationSpec) {
+ if (abs(value) >= abs(targetOffset)) {
+ val finalValue = value.coerceToTarget(targetOffset)
+ dragTo(finalValue, this.velocity)
+ remainingVelocity =
+ if (this.velocity.isNaN()) 0f else this.velocity
+ prev = finalValue
+ cancelAnimation()
+ } else {
+ dragTo(value, this.velocity)
+ remainingVelocity = this.velocity
+ prev = value
+ }
+ }
+ } else {
+ animateTo(velocity, this, anchors, latestTarget)
+ remainingVelocity = 0f
+ }
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
index 169b1c5..2848666 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
@@ -40,9 +40,7 @@
state.canScrollForward
)
- override suspend fun animateScrollBy(delta: Float) {
- state.animateScrollBy(delta)
- }
+ override suspend fun animateScrollBy(delta: Float): Float = state.animateScrollBy(delta)
override suspend fun scrollToItem(index: Int) {
state.scrollToItem(index)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 208f87c4..537b368 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -46,9 +46,8 @@
state.canScrollForward
)
- override suspend fun animateScrollBy(delta: Float) {
+ override suspend fun animateScrollBy(delta: Float): Float =
state.animateScrollBy(delta)
- }
override suspend fun scrollToItem(index: Int) {
state.scrollToItem(index)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
index ce101ad..18a65d3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.node.invalidateSemantics
@@ -33,6 +34,7 @@
import androidx.compose.ui.semantics.indexForKey
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollByOffset
import androidx.compose.ui.semantics.scrollToIndex
import androidx.compose.ui.semantics.verticalScrollAxisRange
import kotlinx.coroutines.launch
@@ -135,6 +137,7 @@
private var scrollByAction: ((x: Float, y: Float) -> Boolean)? = null
private var scrollToIndexAction: ((Int) -> Boolean)? = null
+ private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null
init {
updateCachedSemanticsValues()
@@ -188,6 +191,10 @@
scrollToIndex(action = it)
}
+ scrollByOffsetAction?.let {
+ scrollByOffset(action = it)
+ }
+
getScrollViewportLength {
it.add((state.viewport - state.contentPadding).toFloat())
true
@@ -217,6 +224,20 @@
null
}
+ scrollByOffsetAction = if (userScrollEnabled) {
+ { offset ->
+ if (isVertical) {
+ val consumed = state.animateScrollBy(offset.y)
+ Offset(0f, consumed)
+ } else {
+ val consumed = state.animateScrollBy(offset.x)
+ Offset(consumed, 0f)
+ }
+ }
+ } else {
+ null
+ }
+
scrollToIndexAction = if (userScrollEnabled) {
{ index ->
val itemProvider = itemProviderLambda()
@@ -242,7 +263,7 @@
val maxScrollOffset: Float
fun collectionInfo(): CollectionInfo
- suspend fun animateScrollBy(delta: Float)
+ suspend fun animateScrollBy(delta: Float): Float
suspend fun scrollToItem(index: Int)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
index 1d551a5..72f0894 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridSemantics.kt
@@ -46,9 +46,8 @@
state.canScrollForward
)
- override suspend fun animateScrollBy(delta: Float) {
+ override suspend fun animateScrollBy(delta: Float): Float =
state.animateScrollBy(delta)
- }
override suspend fun scrollToItem(index: Int) {
state.scrollToItem(index)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
index e91f380..13ec0b5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutSemanticState.kt
@@ -30,9 +30,7 @@
override val maxScrollOffset: Float
get() = state.layoutInfo.calculateNewMaxScrollOffset(state.pageCount).toFloat()
- override suspend fun animateScrollBy(delta: Float) {
- state.animateScrollBy(delta)
- }
+ override suspend fun animateScrollBy(delta: Float): Float = state.animateScrollBy(delta)
override suspend fun scrollToItem(index: Int) {
state.scrollToPage(index)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
index 5b32a59..14daca4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
@@ -77,6 +77,16 @@
): TextFieldCharSequence = TextFieldCharSequenceWrapper(text, selection, composition)
/**
+ * Returns the backing CharSequence object that this TextFieldCharSequence is wrapping. This is
+ * useful for external equality comparisons that cannot use [TextFieldCharSequence.contentEquals].
+ */
+internal fun TextFieldCharSequence.getBackingCharSequence(): CharSequence {
+ return when (this) {
+ is TextFieldCharSequenceWrapper -> this.text
+ }
+}
+
+/**
* Copies the contents of this sequence from [[sourceStartIndex], [sourceEndIndex]) into
* [destination] starting at [destinationOffset].
*/
@@ -94,11 +104,22 @@
@OptIn(ExperimentalFoundationApi::class)
private class TextFieldCharSequenceWrapper(
- private val text: CharSequence,
+ text: CharSequence,
selection: TextRange,
composition: TextRange?
) : TextFieldCharSequence {
+ /**
+ * If this TextFieldCharSequence is actually a copy of another, make sure to use the backing
+ * CharSequence object to stop unnecessary nesting and logic that depends on exact equality of
+ * CharSequence comparison that's using [CharSequence.equals].
+ */
+ val text: CharSequence = if (text is TextFieldCharSequenceWrapper) {
+ text.text
+ } else {
+ text
+ }
+
override val length: Int
get() = text.length
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index 2a587b9..2e879b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -35,7 +35,6 @@
import androidx.compose.ui.text.coerceIn
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
internal fun TextFieldState(initialValue: TextFieldValue): TextFieldState {
return TextFieldState(
@@ -49,8 +48,11 @@
* cursor or selection.
*
* To change the text field contents programmatically, call [edit], [setTextAndSelectAll],
- * [setTextAndPlaceCursorAtEnd], or [clearText]. To observe the value of the field over time, call
- * [forEachTextValue] or [textAsFlow].
+ * [setTextAndPlaceCursorAtEnd], or [clearText]. Individual parts of the state like [text],
+ * [selection], or [composition] can be read from any snapshot restart scope like Composable
+ * functions. To observe these members from outside a restart scope, use
+ * `snapshotFlow { textFieldState.text }` or `snapshotFlow { textFieldState.selection }`. To
+ * observe the entirety of state including text, selection, and composition, call [valueAsFlow].
*
* When instantiating this class from a composable, use [rememberTextFieldState] to automatically
* save and restore the field state. For more advanced use cases, pass [TextFieldState.Saver] to
@@ -96,29 +98,67 @@
private var isEditing: Boolean by mutableStateOf(false)
/**
- * The current text and selection. This value will automatically update when the user enters
- * text or otherwise changes the text field contents. To change it programmatically, call
- * [edit].
+ * The current text, selection, and composing region. This value will automatically update when
+ * the user enters text or otherwise changes the text field contents. To change it
+ * programmatically, call [edit].
*
* This is backed by snapshot state, so reading this property in a restartable function (e.g.
* a composable function) will cause the function to restart when the text field's value
* changes.
*
- * To observe changes to this property outside a restartable function, see [forEachTextValue]
- * and [textAsFlow].
+ * To observe changes to this property outside a restartable function, see [valueAsFlow].
*
* @sample androidx.compose.foundation.samples.BasicTextFieldTextDerivedStateSample
*
* @see edit
- * @see forEachTextValue
- * @see textAsFlow
+ * @see valueAsFlow
*/
- var text: TextFieldCharSequence by mutableStateOf(
+ internal var value: TextFieldCharSequence by mutableStateOf(
TextFieldCharSequence(initialText, initialSelection)
)
private set
/**
+ * The current text content. This value will automatically update when the user enters text or
+ * otherwise changes the text field contents. To change it programmatically, call [edit].
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { text }`.
+ *
+ * @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
+ *
+ * @see edit
+ * @see snapshotFlow
+ */
+ val text: CharSequence get() = value.getBackingCharSequence()
+
+ /**
+ * The current selection range. If the selection is collapsed, it represents cursor location.
+ * This value will automatically update when the user enters text or otherwise changes the text
+ * field selection range. To change it programmatically, call [edit].
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { selection }`.
+ *
+ * @see edit
+ * @see snapshotFlow
+ * @see TextFieldCharSequence.selection
+ */
+ val selection: TextRange get() = value.selection
+
+ /**
+ * The current composing range dictated by the IME. If null, there is no composing region.
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { composition }`.
+ *
+ * @see edit
+ * @see snapshotFlow
+ * @see TextFieldCharSequence.composition
+ */
+ val composition: TextRange? get() = value.composition
+
+ /**
* Runs [block] with a mutable version of the current state. The block can make changes to the
* text and cursor/selection. See the documentation on [TextFieldBuffer] for a more detailed
* description of the available operations.
@@ -133,7 +173,7 @@
* @see setTextAndSelectAll
*/
inline fun edit(block: TextFieldBuffer.() -> Unit) {
- val mutableValue = startEdit(text)
+ val mutableValue = startEdit()
try {
mutableValue.block()
commitEdit(mutableValue)
@@ -143,7 +183,7 @@
}
override fun toString(): String =
- "TextFieldState(selection=${text.selection}, text=\"$text\")"
+ "TextFieldState(selection=$selection, text=\"$text\")"
/**
* Undo history controller for this TextFieldState.
@@ -159,7 +199,7 @@
@Suppress("ShowingMemberInHiddenClass")
@PublishedApi
- internal fun startEdit(value: TextFieldCharSequence): TextFieldBuffer {
+ internal fun startEdit(): TextFieldBuffer {
check(!isEditing) {
"TextFieldState does not support concurrent or nested editing."
}
@@ -218,7 +258,7 @@
undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
block: EditingBuffer.() -> Unit
) {
- val previousValue = text
+ val previousValue = value
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
@@ -249,7 +289,7 @@
* a public API.
*/
internal inline fun editWithNoSideEffects(block: EditingBuffer.() -> Unit) {
- val previousValue = text
+ val previousValue = value
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
@@ -260,7 +300,7 @@
composition = mainBuffer.composition
)
- text = afterEditValue
+ value = afterEditValue
sendChangesToIme(
oldValue = previousValue,
newValue = afterEditValue,
@@ -281,24 +321,24 @@
)
if (inputTransformation == null) {
- val oldValue = text
- text = afterEditValue
+ val oldValue = value
+ value = afterEditValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
restartImeIfContentChanges = restartImeIfContentChanges
)
- recordEditForUndo(previousValue, text, mainBuffer.changeTracker, undoBehavior)
+ recordEditForUndo(previousValue, value, mainBuffer.changeTracker, undoBehavior)
return
}
- val oldValue = text
+ val oldValue = value
// if only difference is composition, don't run filter, don't send it to undo manager
if (afterEditValue.contentEquals(oldValue) &&
afterEditValue.selection == oldValue.selection
) {
- text = afterEditValue
+ value = afterEditValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
@@ -307,22 +347,22 @@
return
}
- val mutableValue = TextFieldBuffer(
+ val textFieldBuffer = TextFieldBuffer(
initialValue = afterEditValue,
sourceValue = oldValue,
initialChanges = mainBuffer.changeTracker
)
inputTransformation.transformInput(
originalValue = oldValue,
- valueWithChanges = mutableValue
+ valueWithChanges = textFieldBuffer
)
// If neither the text nor the selection changed, we want to preserve the composition.
// Otherwise, the IME will reset it anyway.
- val afterFilterValue = mutableValue.toTextFieldCharSequence(
+ val afterFilterValue = textFieldBuffer.toTextFieldCharSequence(
composition = afterEditValue.composition
)
if (afterFilterValue == afterEditValue) {
- text = afterFilterValue
+ value = afterFilterValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
@@ -332,7 +372,7 @@
resetStateAndNotifyIme(afterFilterValue)
}
// mutableValue contains all the changes from user and the filter.
- recordEditForUndo(previousValue, text, mutableValue.changes, undoBehavior)
+ recordEditForUndo(previousValue, value, textFieldBuffer.changes, undoBehavior)
}
/**
@@ -445,15 +485,15 @@
}
val finalValue = TextFieldCharSequence(
- if (textChanged) newValue else bufferState,
- mainBuffer.selection,
- mainBuffer.composition
+ text = if (textChanged) newValue else bufferState,
+ selection = mainBuffer.selection,
+ composition = mainBuffer.composition
)
// value must be set before notifyImeListeners are called. Even though we are sending the
// previous and current values, a system callback may request the latest state e.g. IME
// restartInput call is handled before notifyImeListeners return.
- text = finalValue
+ value = finalValue
sendChangesToIme(
oldValue = bufferState,
@@ -489,8 +529,8 @@
override fun SaverScope.save(value: TextFieldState): Any? {
return listOf(
value.text.toString(),
- value.text.selection.start,
- value.text.selection.end,
+ value.selection.start,
+ value.selection.end,
with(TextUndoManager.Companion.Saver) {
save(value.textUndoManager)
}
@@ -514,13 +554,14 @@
}
/**
- * Returns a [Flow] of the values of [TextFieldState.text] as seen from the global snapshot.
+ * Returns a [Flow] of the values of [TextFieldState.text], [TextFieldState.selection], and
+ * [TextFieldState.composition] as seen from the global snapshot.
* The initial value is emitted immediately when the flow is collected.
*
* @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
*/
@ExperimentalFoundationApi
-fun TextFieldState.textAsFlow(): Flow<TextFieldCharSequence> = snapshotFlow { text }
+fun TextFieldState.valueAsFlow(): Flow<TextFieldCharSequence> = snapshotFlow { value }
/**
* Create and remember a [TextFieldState]. The state is remembered using [rememberSaveable] and so
@@ -609,28 +650,3 @@
placeCursorAtEnd()
}
}
-
-/**
- * Invokes [block] with the value of [TextFieldState.text], and every time the value is changed.
- *
- * The caller will be suspended until its coroutine is cancelled. If the text is changed while
- * [block] is suspended, [block] will be cancelled and re-executed with the new value immediately.
- * [block] will never be executed concurrently with itself.
- *
- * To get access to a [Flow] of [TextFieldState.text] over time, use [textAsFlow].
- *
- * Warning: Do not update the value of the [TextFieldState] from [block]. If you want to perform
- * either a side effect when text is changed, or filter it in some way, use an
- * [InputTransformation].
- *
- * @sample androidx.compose.foundation.samples.BasicTextFieldForEachTextValueSample
- *
- * @see textAsFlow
- */
-@ExperimentalFoundationApi
-suspend fun TextFieldState.forEachTextValue(
- block: suspend (TextFieldCharSequence) -> Unit
-): Nothing {
- textAsFlow().collectLatest(block)
- error("textAsFlow expected not to complete without exception")
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
index c5b1415..990e981 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
@@ -116,7 +116,7 @@
derivedStateOf {
// text is a state read. transformation may also perform state reads when ran.
calculateTransformedText(
- untransformedText = textFieldState.text,
+ untransformedValue = textFieldState.value,
outputTransformation = transformation,
wedgeAffinity = selectionWedgeAffinity
)
@@ -129,7 +129,7 @@
calculateTransformedText(
// These are state reads. codepointTransformation may also perform state reads
// when ran.
- untransformedText = outputTransformedText?.value?.text ?: textFieldState.text,
+ untransformedValue = outputTransformedText?.value?.text ?: textFieldState.value,
codepointTransformation = transformation,
wedgeAffinity = selectionWedgeAffinity
)
@@ -141,7 +141,7 @@
* [CodepointTransformation] applied.
*/
val untransformedText: TextFieldCharSequence
- get() = textFieldState.text
+ get() = textFieldState.value
/**
* The text that should be presented to the user in most cases. If an [OutputTransformation] is
@@ -433,22 +433,22 @@
/**
* Applies an [OutputTransformation] to a [TextFieldCharSequence], returning the
- * transformed text content, the selection/cursor from the [untransformedText] mapped to the
+ * transformed text content, the selection/cursor from the [untransformedValue] mapped to the
* offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to map
* offsets in both directions between the transformed and untransformed text.
*
- * This function is relatively expensive, since it creates a copy of [untransformedText], so
+ * This function is relatively expensive, since it creates a copy of [untransformedValue], so
* its result should be cached.
*/
@kotlin.jvm.JvmStatic
private fun calculateTransformedText(
- untransformedText: TextFieldCharSequence,
+ untransformedValue: TextFieldCharSequence,
outputTransformation: OutputTransformation,
wedgeAffinity: SelectionWedgeAffinity
): TransformedText? {
val offsetMappingCalculator = OffsetMappingCalculator()
val buffer = TextFieldBuffer(
- initialValue = untransformedText,
+ initialValue = untransformedValue,
offsetMappingCalculator = offsetMappingCalculator
)
@@ -464,11 +464,11 @@
// Pass the calculator explicitly since the one on transformedText won't be updated
// yet.
selection = mapToTransformed(
- range = untransformedText.selection,
+ range = untransformedValue.selection,
mapping = offsetMappingCalculator,
wedgeAffinity = wedgeAffinity
),
- composition = untransformedText.composition?.let {
+ composition = untransformedValue.composition?.let {
mapToTransformed(
range = it,
mapping = offsetMappingCalculator,
@@ -481,16 +481,16 @@
/**
* Applies a [CodepointTransformation] to a [TextFieldCharSequence], returning the
- * transformed text content, the selection/cursor from the [untransformedText] mapped to the
+ * transformed text content, the selection/cursor from the [untransformedValue] mapped to the
* offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to map
* offsets in both directions between the transformed and untransformed text.
*
- * This function is relatively expensive, since it creates a copy of [untransformedText], so
+ * This function is relatively expensive, since it creates a copy of [untransformedValue], so
* its result should be cached.
*/
@kotlin.jvm.JvmStatic
private fun calculateTransformedText(
- untransformedText: TextFieldCharSequence,
+ untransformedValue: TextFieldCharSequence,
codepointTransformation: CodepointTransformation,
wedgeAffinity: SelectionWedgeAffinity
): TransformedText? {
@@ -498,10 +498,10 @@
// This is the call to external code. Returns same instance if no codepoints change.
val transformedText =
- untransformedText.toVisualText(codepointTransformation, offsetMappingCalculator)
+ untransformedValue.toVisualText(codepointTransformation, offsetMappingCalculator)
// Avoid allocations + mapping if there weren't actually any transformations.
- if (transformedText === untransformedText) {
+ if (transformedText === untransformedValue) {
return null
}
@@ -510,11 +510,11 @@
// Pass the calculator explicitly since the one on transformedText won't be updated
// yet.
selection = mapToTransformed(
- untransformedText.selection,
+ untransformedValue.selection,
offsetMappingCalculator,
wedgeAffinity
),
- composition = untransformedText.composition?.let {
+ composition = untransformedValue.composition?.let {
mapToTransformed(it, offsetMappingCalculator, wedgeAffinity)
}
)
diff --git a/compose/material/material-icons-core/build.gradle b/compose/material/material-icons-core/build.gradle
index d51d9d4..8e2366a 100644
--- a/compose/material/material-icons-core/build.gradle
+++ b/compose/material/material-icons-core/build.gradle
@@ -111,6 +111,7 @@
androidx {
name = "Compose Material Icons Core"
type = LibraryType.PUBLISHED_KOTLIN_ONLY_LIBRARY
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2020"
description = "Compose Material Design core icons. This module contains the most commonly used set of Material icons."
legacyDisableKotlinStrictApiMode = true
diff --git a/compose/material/material-icons-core/samples/build.gradle b/compose/material/material-icons-core/samples/build.gradle
index 2d7f7f1..c2120d47 100644
--- a/compose/material/material-icons-core/samples/build.gradle
+++ b/compose/material/material-icons-core/samples/build.gradle
@@ -44,6 +44,7 @@
androidx {
name = "Compose UI Core Material Icons Samples"
type = LibraryType.SAMPLES
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2019"
description = "Contains the sample code for the Androidx Compose UI Core Material Icons"
}
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index e13629d..568f7f0 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -125,6 +125,7 @@
androidx {
name = "Compose Material Icons Extended"
type = LibraryType.PUBLISHED_KOTLIN_ONLY_LIBRARY
+ mavenVersion = LibraryVersions.COMPOSE
// This module has a large number (5000+) of generated source files and so doc generation /
// API tracking will simply take too long
runApiTasks = new RunApiTasks.No("Five thousand generated source files")
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index b32975c..179c44c 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -42,6 +42,7 @@
androidx {
name = "Compose Material Navigation"
publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2024"
description = "Compose Material integration with Navigation"
samples(projectOrArtifact(":compose:material:material-navigation-samples"))
diff --git a/compose/material/material-navigation/samples/build.gradle b/compose/material/material-navigation/samples/build.gradle
index 7e3acd6..b0a9deb 100644
--- a/compose/material/material-navigation/samples/build.gradle
+++ b/compose/material/material-navigation/samples/build.gradle
@@ -42,6 +42,7 @@
androidx {
name = "Compose Material Navigation Integration Samples"
type = LibraryType.SAMPLES
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2024"
description = "Samples for Compose Material integration with Navigation"
}
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index ee16cb3..7cbf897 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -118,6 +118,7 @@
androidx {
name = "Compose Material Ripple"
type = LibraryType.PUBLISHED_KOTLIN_ONLY_LIBRARY
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2020"
description = "Material ripple used to build interactive components"
// Disable strict API mode for MPP builds as it will fail to compile androidAndroidTest
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 335d309..8ccafcf 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -149,6 +149,7 @@
androidx {
name = "Compose Material Components"
type = LibraryType.PUBLISHED_KOTLIN_ONLY_LIBRARY
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2018"
description = "Compose Material Design Components library"
legacyDisableKotlinStrictApiMode = true
diff --git a/compose/material/material/samples/build.gradle b/compose/material/material/samples/build.gradle
index 649d091..daf6d1e 100644
--- a/compose/material/material/samples/build.gradle
+++ b/compose/material/material/samples/build.gradle
@@ -48,6 +48,7 @@
androidx {
name = "Compose Material Components Samples"
type = LibraryType.SAMPLES
+ mavenVersion = LibraryVersions.COMPOSE
inceptionYear = "2019"
description = "Contains the sample code for the AndroidX Compose Material components."
}
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index c60dfc7..7925121 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -71,8 +71,8 @@
}
public final class PaneScaffoldDirectiveKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculateDensePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculateStandardPaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneScaffoldScope {
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index c60dfc7..7925121 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -71,8 +71,8 @@
}
public final class PaneScaffoldDirectiveKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculateDensePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculateStandardPaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirective(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
+ method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
@SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface PaneScaffoldScope {
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
index c7be96d..bc9db43 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
@@ -376,7 +376,7 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun SampleThreePaneScaffoldStandardMode() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
)
val scaffoldValue = calculateThreePaneScaffoldValue(
@@ -394,7 +394,7 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun SampleThreePaneScaffoldDenseMode() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
currentWindowAdaptiveInfo()
)
val scaffoldValue = calculateThreePaneScaffoldValue(
@@ -415,7 +415,7 @@
paneExpansionState: PaneExpansionState,
paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
) {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
)
val scaffoldValue = calculateThreePaneScaffoldValue(
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
index 1d6be9b..668f867 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
@@ -33,7 +33,7 @@
class PaneScaffoldDirectiveTest {
@Test
fun test_calculateStandardPaneScaffoldDirective_compactWidth() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(400, 800),
Posture()
@@ -48,7 +48,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_mediumWidth() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(750, 900),
Posture()
@@ -63,7 +63,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_expandedWidth() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(1200, 800),
Posture()
@@ -78,7 +78,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_tabletop() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(isTabletop = true)
@@ -93,7 +93,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_compactWidth() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(400, 800),
Posture()
@@ -108,7 +108,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_mediumWidth() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(750, 900),
Posture()
@@ -123,7 +123,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_expandedWidth() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(1200, 800),
Posture()
@@ -138,7 +138,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_tabletop() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(isTabletop = true)
@@ -153,7 +153,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_alwaysAvoidHinge() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -166,7 +166,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_avoidOccludingHinge() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -179,7 +179,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_avoidSeparatingHinge() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -192,7 +192,7 @@
@Test
fun test_calculateStandardPaneScaffoldDirective_neverAvoidHinge() {
- val scaffoldDirective = calculateStandardPaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirective(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -205,7 +205,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_alwaysAvoidHinge() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -218,7 +218,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_avoidOccludingHinge() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -231,7 +231,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_avoidSeparatingHinge() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
@@ -244,7 +244,7 @@
@Test
fun test_calculateDensePaneScaffoldDirective_neverAvoidHinge() {
- val scaffoldDirective = calculateDensePaneScaffoldDirective(
+ val scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
WindowAdaptiveInfo(
WindowSizeClass(700, 800),
Posture(hingeList = hingeList)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index c4977e1..42a842b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -30,7 +30,7 @@
import androidx.window.core.layout.WindowWidthSizeClass
/**
- * Calculates the standard [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
+ * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
* method with [currentWindowAdaptiveInfo] to acquire Material-recommended adaptive layout
* settings of the current activity window.
*
@@ -43,9 +43,8 @@
* vertical hinges.
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
-// TODO(b/285144647): Add more details regarding the use scenarios of this function.
@ExperimentalMaterial3AdaptiveApi
-fun calculateStandardPaneScaffoldDirective(
+fun calculatePaneScaffoldDirective(
windowAdaptiveInfo: WindowAdaptiveInfo,
verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
): PaneScaffoldDirective {
@@ -87,9 +86,13 @@
}
/**
- * Calculates the dense-mode [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
+ * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
* method with [currentWindowAdaptiveInfo] to acquire Material-recommended dense-mode adaptive
- * layout settings of the current activity window.
+ * layout settings of the current activity window. Note that this function results in a dual-pane
+ * layout when the [WindowWidthSizeClass] is [WindowWidthSizeClass.MEDIUM], while
+ * [calculatePaneScaffoldDirective] results in a single-pane layout instead. We recommend to use
+ * [calculatePaneScaffoldDirective], unless you have a strong use case to show two panes on
+ * a medium-width window, which can make your layout look too packed.
*
* See more details on the [Material design guideline site]
* (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).
@@ -100,9 +103,8 @@
* vertical hinges.
* @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
*/
-// TODO(b/285144647): Add more details regarding the use scenarios of this function.
@ExperimentalMaterial3AdaptiveApi
-fun calculateDensePaneScaffoldDirective(
+fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo: WindowAdaptiveInfo,
verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
): PaneScaffoldDirective {
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 0540d83..2518f1a 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
@@ -69,7 +69,7 @@
* freely pipeline the relevant adaptive signals and use them as input of the scaffold function
* to render the final adaptive layout.
*
- * It's recommended to use [ThreePaneScaffold] with [calculateStandardPaneScaffoldDirective],
+ * It's recommended to use [ThreePaneScaffold] with [calculatePaneScaffoldDirective],
* [calculateThreePaneScaffoldValue] to follow the Material design guidelines on adaptive
* programming.
*
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
index b419700..74742c8a 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
@@ -29,7 +29,7 @@
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
-import androidx.compose.material3.adaptive.layout.calculateStandardPaneScaffoldDirective
+import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.calculateThreePaneScaffoldValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -148,7 +148,7 @@
* This type must be storable in a Bundle. Used to customize navigation behavior (for example,
* [BackNavigationBehavior]). If this customization is unneeded, you can pass [Nothing].
* @param scaffoldDirective the current layout directives to follow. The default value will be
- * calculated with [calculateStandardPaneScaffoldDirective] using
+ * calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from
* the current context.
* @param adaptStrategies adaptation strategies of each pane.
@@ -162,7 +162,7 @@
@Composable
fun <T> rememberListDetailPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
- calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
+ calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
ListDetailPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
@@ -186,7 +186,7 @@
* This type must be storable in a Bundle. Used to customize navigation behavior (for example,
* [BackNavigationBehavior]). If this customization is unneeded, you can pass [Nothing].
* @param scaffoldDirective the current layout directives to follow. The default value will be
- * calculated with [calculateStandardPaneScaffoldDirective] using
+ * calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from
* the current context.
* @param adaptStrategies adaptation strategies of each pane.
@@ -200,7 +200,7 @@
@Composable
fun <T> rememberSupportingPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
- calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()),
+ calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
SupportingPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
index 6a35918..ec79327 100644
--- a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
@@ -46,7 +46,7 @@
* color. If [imageVector] has no intrinsic size, this component will use the
* recommended default size. Icon is an opinionated component designed to be used with single-color
* icons so that they can be tinted correctly for the component they are placed in. For multicolored
- * icons and icons that should not be tinted, use [Color.Unspecified] for [tint]. For generic images
+ * icons and icons that should not be tinted, use null for [tint]. For generic images
* that should not be tinted, and do not follow the recommended icon size, use the generic
* [androidx.compose.foundation.Image] instead. For a clickable icon, see [IconButton].
*
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index e58e6b2..db4fc05 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -622,17 +622,6 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
- @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
- enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection EndToStart;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection StartToEnd;
- }
-
- @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue Default;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToEnd;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToStart;
- }
-
@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;
}
@@ -1068,8 +1057,10 @@
}
public final class NavigationDrawerKt {
+ method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(kotlin.jvm.functions.Function0<kotlin.Unit> label, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.NavigationDrawerItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
@@ -1213,7 +1204,7 @@
method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class RangeSliderState {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class RangeSliderState {
ctor public RangeSliderState(optional float activeRangeStart, optional float activeRangeEnd, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
method public float getActiveRangeEnd();
method public float getActiveRangeStart();
@@ -1222,6 +1213,7 @@
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
method public void setActiveRangeEnd(float);
method public void setActiveRangeStart(float);
+ method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
property public final float activeRangeEnd;
property public final float activeRangeStart;
property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
@@ -1508,7 +1500,7 @@
property @Deprecated public final float[] tickFractions;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
ctor public SliderState(optional float value, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
method public void dispatchRawDelta(float delta);
method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1516,6 +1508,7 @@
method public int getSteps();
method public float getValue();
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
+ method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
method public void setValue(float);
property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
property public final int steps;
@@ -1619,7 +1612,6 @@
}
public final class SwipeToDismissBoxKt {
- method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissBoxValue> directions);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, optional boolean gesturesEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.material3.SwipeToDismissBoxValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissBoxValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
}
@@ -1631,7 +1623,6 @@
method public androidx.compose.material3.SwipeToDismissBoxValue getDismissDirection();
method @FloatRange(from=0.0, to=1.0) public float getProgress();
method public androidx.compose.material3.SwipeToDismissBoxValue getTargetValue();
- method @Deprecated public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
method public float requireOffset();
method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? snapTo(androidx.compose.material3.SwipeToDismissBoxValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -2105,6 +2096,46 @@
}
+package androidx.compose.material3.carousel {
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior multiBrowseFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior noSnapFlingBehavior();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior singleAdvanceFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public CarouselState(optional int currentItem, optional @FloatRange(from=-0.5, to=0.5) float currentItemOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public float dispatchRawDelta(float delta);
+ method public androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> getItemCountState();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setItemCountState(androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>>);
+ property public boolean isScrollInProgress;
+ property public final androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> itemCountState;
+ field public static final androidx.compose.material3.carousel.CarouselState.Companion Companion;
+ }
+
+ public static final class CarouselState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> Saver;
+ }
+
+ public final class CarouselStateKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ }
+
+}
+
package androidx.compose.material3.pulltorefresh {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index e58e6b2..db4fc05 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -622,17 +622,6 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
- @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
- enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection EndToStart;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection StartToEnd;
- }
-
- @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue Default;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToEnd;
- enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToStart;
- }
-
@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;
}
@@ -1068,8 +1057,10 @@
}
public final class NavigationDrawerKt {
+ method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(kotlin.jvm.functions.Function0<kotlin.Unit> label, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.NavigationDrawerItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
@@ -1213,7 +1204,7 @@
method @androidx.compose.runtime.Composable public static void RadioButton(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit>? onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.RadioButtonColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class RangeSliderState {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class RangeSliderState {
ctor public RangeSliderState(optional float activeRangeStart, optional float activeRangeEnd, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
method public float getActiveRangeEnd();
method public float getActiveRangeStart();
@@ -1222,6 +1213,7 @@
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
method public void setActiveRangeEnd(float);
method public void setActiveRangeStart(float);
+ method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
property public final float activeRangeEnd;
property public final float activeRangeStart;
property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
@@ -1508,7 +1500,7 @@
property @Deprecated public final float[] tickFractions;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SliderState implements androidx.compose.foundation.gestures.DraggableState {
ctor public SliderState(optional float value, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
method public void dispatchRawDelta(float delta);
method public suspend Object? drag(androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -1516,6 +1508,7 @@
method public int getSteps();
method public float getValue();
method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> getValueRange();
+ method public void setOnValueChangeFinished(kotlin.jvm.functions.Function0<kotlin.Unit>?);
method public void setValue(float);
property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished;
property public final int steps;
@@ -1619,7 +1612,6 @@
}
public final class SwipeToDismissBoxKt {
- method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissBoxValue> directions);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissBoxState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, optional boolean gesturesEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.material3.SwipeToDismissBoxValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissBoxValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
}
@@ -1631,7 +1623,6 @@
method public androidx.compose.material3.SwipeToDismissBoxValue getDismissDirection();
method @FloatRange(from=0.0, to=1.0) public float getProgress();
method public androidx.compose.material3.SwipeToDismissBoxValue getTargetValue();
- method @Deprecated public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
method public float requireOffset();
method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? snapTo(androidx.compose.material3.SwipeToDismissBoxValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -2105,6 +2096,46 @@
}
+package androidx.compose.material3.carousel {
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior multiBrowseFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior noSnapFlingBehavior();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior singleAdvanceFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public CarouselState(optional int currentItem, optional @FloatRange(from=-0.5, to=0.5) float currentItemOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public float dispatchRawDelta(float delta);
+ method public androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> getItemCountState();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setItemCountState(androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>>);
+ property public boolean isScrollInProgress;
+ property public final androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> itemCountState;
+ field public static final androidx.compose.material3.carousel.CarouselState.Companion Companion;
+ }
+
+ public static final class CarouselState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> Saver;
+ }
+
+ public final class CarouselStateKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ }
+
+}
+
package androidx.compose.material3.pulltorefresh {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 814b0841..af81870 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -444,7 +444,7 @@
BottomSheets,
Buttons,
Card,
- // Carousel, // TODO: Re-enable when ready
+ Carousel,
Checkboxes,
Chips,
DatePickers,
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 e7b564a..037d0dbf 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
@@ -39,7 +39,6 @@
import androidx.compose.material3.samples.ButtonSample
import androidx.compose.material3.samples.ButtonWithIconSample
import androidx.compose.material3.samples.CardSample
-import androidx.compose.material3.samples.CarouselSample
import androidx.compose.material3.samples.CheckboxSample
import androidx.compose.material3.samples.CheckboxWithTextSample
import androidx.compose.material3.samples.ChipGroupReflowSample
@@ -79,6 +78,8 @@
import androidx.compose.material3.samples.FilterChipSample
import androidx.compose.material3.samples.FilterChipWithLeadingIconSample
import androidx.compose.material3.samples.FloatingActionButtonSample
+import androidx.compose.material3.samples.HorizontalMultiBrowseCarouselSample
+import androidx.compose.material3.samples.HorizontalUncontainedCarouselSample
import androidx.compose.material3.samples.IconButtonSample
import androidx.compose.material3.samples.IconToggleButtonSample
import androidx.compose.material3.samples.IndeterminateCircularProgressIndicatorSample
@@ -326,11 +327,18 @@
private const val CarouselExampleSourceUrl = "$SampleSourceUrl/CarouselSamples.kt"
val CarouselExamples = listOf(
Example(
- name = ::CarouselSample.name,
+ name = ::HorizontalMultiBrowseCarouselSample.name,
description = CarouselExampleDescription,
sourceUrl = CarouselExampleSourceUrl
) {
- CarouselSample()
+ HorizontalMultiBrowseCarouselSample()
+ },
+ Example(
+ name = ::HorizontalUncontainedCarouselSample.name,
+ description = CarouselExampleDescription,
+ sourceUrl = CarouselExampleSourceUrl
+ ) {
+ HorizontalUncontainedCarouselSample()
}
)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index a55a9bd..5410945 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -17,16 +17,18 @@
package androidx.compose.material3.samples
import androidx.annotation.DrawableRes
+import androidx.annotation.Sampled
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
+import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
+import androidx.compose.material3.carousel.rememberCarouselState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
@@ -35,16 +37,19 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
@Preview
+@Sampled
@Composable
-fun CarouselSample() {
+fun HorizontalMultiBrowseCarouselSample() {
+
data class CarouselItem(
val id: Int,
@DrawableRes val imageResId: Int,
@StringRes val contentDescriptionResId: Int
)
- val Items = listOf(
+ val items = listOf(
CarouselItem(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
CarouselItem(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
CarouselItem(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
@@ -52,23 +57,69 @@
CarouselItem(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
)
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- state = rememberLazyListState()
- ) {
- itemsIndexed(Items) { _, item ->
- Card(
- modifier = Modifier
- .width(350.dp)
- .height(200.dp),
- ) {
- Image(
- painter = painterResource(id = item.imageResId),
- contentDescription = stringResource(item.contentDescriptionResId),
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop
- )
- }
+ HorizontalMultiBrowseCarousel(
+ state = rememberCarouselState { items.count() },
+ modifier = Modifier
+ .width(412.dp)
+ .height(221.dp),
+ preferredItemWidth = 186.dp,
+ itemSpacing = 8.dp,
+ contentPadding = PaddingValues(horizontal = 16.dp)
+ ) { i ->
+ val item = items[i]
+ Card(
+ modifier = Modifier
+ .height(205.dp)
+ ) {
+ Image(
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun HorizontalUncontainedCarouselSample() {
+
+ data class CarouselItem(
+ val id: Int,
+ @DrawableRes val imageResId: Int,
+ @StringRes val contentDescriptionResId: Int
+ )
+
+ val items = listOf(
+ CarouselItem(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
+ CarouselItem(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
+ CarouselItem(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
+ CarouselItem(3, R.drawable.carousel_image_4, R.string.carousel_image_4_description),
+ CarouselItem(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
+ )
+ HorizontalUncontainedCarousel(
+ state = rememberCarouselState { items.count() },
+ modifier = Modifier
+ .width(412.dp)
+ .height(221.dp),
+ itemWidth = 186.dp,
+ itemSpacing = 8.dp,
+ contentPadding = PaddingValues(horizontal = 16.dp)
+ ) { i ->
+ val item = items[i]
+ Card(
+ modifier = Modifier
+ .height(205.dp)
+ ) {
+ Image(
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
}
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
index f21529e..6d07a51 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
@@ -16,7 +16,6 @@
package androidx.compose.material3.samples
-import androidx.activity.compose.BackHandler
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -24,10 +23,27 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Bookmarks
+import androidx.compose.material.icons.filled.CalendarMonth
+import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Email
-import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Group
+import androidx.compose.material.icons.filled.Headphones
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.JoinFull
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Laptop
+import androidx.compose.material.icons.filled.Map
+import androidx.compose.material.icons.filled.Navigation
+import androidx.compose.material.icons.filled.Outbox
+import androidx.compose.material.icons.filled.PushPin
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material.icons.filled.Radio
import androidx.compose.material3.Button
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
@@ -58,24 +74,45 @@
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
- ModalDrawerSheet {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- scope.launch { drawerState.close() }
- selectedItem.value = item
- },
- modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
- )
+ ModalDrawerSheet(drawerState) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
}
}
},
@@ -101,22 +138,43 @@
@Composable
fun PermanentNavigationDrawerSample() {
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(240.dp)) {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- selectedItem.value = item
- },
- modifier = Modifier.padding(horizontal = 12.dp)
- )
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ }
}
}
},
@@ -140,30 +198,46 @@
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
- BackHandler(enabled = drawerState.isOpen) {
- scope.launch {
- drawerState.close()
- }
- }
DismissibleNavigationDrawer(
drawerState = drawerState,
drawerContent = {
- DismissibleDrawerSheet {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- scope.launch { drawerState.close() }
- selectedItem.value = item
- },
- modifier = Modifier.padding(horizontal = 12.dp)
- )
+ DismissibleDrawerSheet(drawerState) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ }
}
}
},
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
index 06d8653..3abf32b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
@@ -19,11 +19,34 @@
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Build
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.ShoppingCart
+import androidx.compose.material.icons.filled.ThumbUp
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -117,6 +140,167 @@
.captureToImage()
.assertAgainstGolden(screenshotRule, goldenName)
}
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress0AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress0AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress25AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.25f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress25AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress50AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.5f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress50AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress75AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.75f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress75AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress100AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 1f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress100AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress0AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress0AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress25AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.25f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress25AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress50AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.5f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress50AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress75AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.75f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress75AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress100AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 1f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden(
+ "navigationDrawer_predictiveBack_progress100AndSwipeEdgeRight"
+ )
+ }
}
private val ContainerTestTag = "container"
+
+private val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Build,
+ Icons.Default.Check,
+ Icons.Default.DateRange,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Home,
+ Icons.Default.Info,
+ Icons.Default.Lock,
+ Icons.Default.Notifications,
+ Icons.Default.Place,
+ Icons.Default.Refresh,
+ Icons.Default.ShoppingCart,
+ Icons.Default.ThumbUp,
+ Icons.Default.Warning,
+)
+
+@Composable
+private fun ModalNavigationDrawerPredictiveBack(progress: Float, swipeEdgeLeft: Boolean) {
+ val maxScaleXDistanceGrow: Float
+ val maxScaleXDistanceShrink: Float
+ val maxScaleYDistance: Float
+ with(LocalDensity.current) {
+ maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
+ maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
+ maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
+ }
+
+ val drawerPredictiveBackState = DrawerPredictiveBackState().apply {
+ update(
+ progress = progress,
+ swipeEdgeLeft = swipeEdgeLeft,
+ isRtl = false,
+ maxScaleXDistanceGrow = maxScaleXDistanceGrow,
+ maxScaleXDistanceShrink = maxScaleXDistanceShrink,
+ maxScaleYDistance = maxScaleYDistance
+ )
+ }
+
+ ModalNavigationDrawer(
+ modifier = Modifier.testTag(ContainerTestTag),
+ drawerState = rememberDrawerState(DrawerValue.Open),
+ drawerContent = {
+ // Use the internal DrawerSheet instead of ModalDrawerSheet so we can simulate different
+ // back progress values for the test, and avoid the real PredictiveBackHandler.
+ DrawerSheet(
+ drawerPredictiveBackState,
+ DrawerDefaults.windowInsets,
+ Modifier,
+ DrawerDefaults.shape,
+ DrawerDefaults.modalContainerColor,
+ contentColorFor(DrawerDefaults.modalContainerColor),
+ DrawerDefaults.ModalDrawerElevation
+ ) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == Icons.Default.AccountCircle,
+ onClick = {},
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
+ }
+ }
+ },
+ content = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+ )
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
index 9a9ef7d..740d6ec 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderScreenshotTest.kt
@@ -277,7 +277,6 @@
activeTickColor = Color.Yellow,
inactiveTickColor = Color.Magenta
)
-
)
}
}
@@ -301,7 +300,6 @@
disabledActiveTickColor = Color.Magenta,
disabledInactiveTickColor = Color.Cyan
)
-
)
}
}
@@ -310,6 +308,19 @@
@OptIn(ExperimentalMaterial3Api::class)
@Test
+ fun sliderTest_min_corner() {
+ rule.setMaterialContent(lightColorScheme()) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ Slider(
+ remember { SliderState(0.91f) }
+ )
+ }
+ }
+ assertSliderAgainstGolden("slider_min_corner")
+ }
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Test
fun rangeSliderTest_middle_no_gap() {
rule.setMaterialContent(lightColorScheme()) {
Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 80e6eee..75d56ed 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.TargetedFlingBehavior
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
@@ -193,9 +194,12 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- val strategy = Strategy { _, _ ->
- keylineList
- }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val outOfBoundsNum = calculateOutOfBounds(strategy)
// With this strategy, we expect 3 loaded items
val loadedItems = 3
@@ -219,7 +223,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 380f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
// Max offset should only add item spacing between each item
val expectedMaxScrollOffset = (186f * 10) + (8f * 9) - 380f
@@ -278,6 +287,7 @@
flingBehavior = flingBehavior(state),
modifier = modifier.testTag(CarouselTestTag),
itemSpacing = 0.dp,
+ contentPadding = PaddingValues(0.dp),
content = content,
)
}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
index d8bebd7..43399f0 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
@@ -146,15 +146,6 @@
val scope = remember(expanded, onExpandedChange, config, view, density) {
object : ExposedDropdownMenuBoxScope() {
override fun Modifier.menuAnchor(): Modifier = this
- .onGloballyPositioned {
- anchorCoordinates = it
- anchorWidth = it.size.width
- menuMaxHeight = calculateMaxHeight(
- windowBounds = view.rootView.getWindowBounds(),
- anchorBounds = anchorCoordinates.getAnchorBounds(),
- verticalMargin = verticalMargin,
- )
- }
.expandable(
expanded = expanded,
onExpandedChange = { onExpandedChange(!expanded) },
@@ -179,7 +170,17 @@
}
}
- Box(modifier) {
+ Box(
+ modifier.onGloballyPositioned {
+ anchorCoordinates = it
+ anchorWidth = it.size.width
+ menuMaxHeight = calculateMaxHeight(
+ windowBounds = view.rootView.getWindowBounds(),
+ anchorBounds = anchorCoordinates.getAnchorBounds(),
+ verticalMargin = verticalMargin,
+ )
+ }
+ ) {
scope.content()
}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
new file mode 100644
index 0000000..535c610
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.activity.BackEventCompat
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.core.animate
+import androidx.compose.material3.internal.PredictiveBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.coroutines.cancellation.CancellationException
+import kotlinx.coroutines.launch
+
+/**
+ * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState]
+ * based on back progress.
+ *
+ * @param drawerState state of the drawer
+ * @param content content of the rest of the UI
+ */
+@Composable
+internal actual fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+) {
+ val drawerPredictiveBackState = remember { DrawerPredictiveBackState() }
+ val scope = rememberCoroutineScope()
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val maxScaleXDistanceGrow: Float
+ val maxScaleXDistanceShrink: Float
+ val maxScaleYDistance: Float
+ with(LocalDensity.current) {
+ maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
+ maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
+ maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
+ }
+
+ PredictiveBackHandler(enabled = drawerState.isOpen) { progress ->
+ try {
+ progress.collect { backEvent ->
+ drawerPredictiveBackState.update(
+ PredictiveBack.transform(backEvent.progress),
+ backEvent.swipeEdge == BackEventCompat.EDGE_LEFT,
+ isRtl,
+ maxScaleXDistanceGrow,
+ maxScaleXDistanceShrink,
+ maxScaleYDistance
+ )
+ }
+ } catch (e: CancellationException) {
+ drawerPredictiveBackState.clear()
+ } finally {
+ if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) {
+ // If swipe edge matches drawer gravity and we've stretched the drawer horizontally,
+ // un-stretch it smoothly so that it hides completely during the drawer close.
+ scope.launch {
+ animate(
+ initialValue = drawerPredictiveBackState.scaleXDistance,
+ targetValue = 0f
+ ) { value, _ -> drawerPredictiveBackState.scaleXDistance = value }
+ drawerPredictiveBackState.clear()
+ }
+ }
+ drawerState.close()
+ }
+ }
+
+ LaunchedEffect(drawerState.isClosed) {
+ if (drawerState.isClosed) {
+ drawerPredictiveBackState.clear()
+ }
+ }
+
+ content(drawerPredictiveBackState)
+}
+
+internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp
+internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp
+internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
index 9e32c8e..a6b9613 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
@@ -97,7 +97,12 @@
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
// Test strategy that is start aligned:
@@ -127,7 +132,12 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
// Test strategy that is start aligned:
@@ -150,6 +160,11 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(1000f, 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
}
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
index 84738c7..830559f 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -39,7 +39,12 @@
itemSpacing = 0f,
itemCount = 10,
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(500f, 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 500f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.itemMainAxisSize).isEqualTo(itemSize)
}
@@ -53,10 +58,12 @@
preferredItemSize = itemSize,
itemSpacing = 0f,
itemCount = 10,
- )!!
+ )!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = 100f,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val minSmallItemSize: Float = with(Density) { CarouselDefaults.MinSmallItemSize.toPx() }
val keylines = strategy.defaultKeylines
@@ -80,10 +87,12 @@
preferredItemSize = 200f,
itemSpacing = 0f,
itemCount = 10,
- )!!
+ )!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = minSmallItemSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
@@ -118,7 +127,9 @@
)!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
@@ -144,7 +155,12 @@
itemSpacing = 0f,
itemCount = 3,
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(carouselSize, 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = carouselSize,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val keylines = strategy.defaultKeylines
// We originally expect a keyline list of [xSmall-Large-Large-Medium-Small-xSmall], but with
@@ -155,6 +171,7 @@
assertThat(keylines[3].size).isLessThan(keylines[2].size)
}
+ @Test
fun testMultiBrowse_adjustsForItemSpacing() {
val keylineList = multiBrowseKeylineList(
density = Density,
@@ -163,7 +180,12 @@
itemSpacing = 8f,
itemCount = 10
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(380f, 8f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(keylineList.firstFocal.size).isEqualTo(186f)
// Ensure the first visible item is large and aligned with the start of the container
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
index 06052ff..595e34c 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -36,7 +36,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
@@ -67,7 +69,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val endKeylineList = strategy.getEndKeylines()
@@ -89,7 +93,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val startKeylineList = strategy.getStartKeylines()
@@ -112,7 +118,9 @@
val strategy = Strategy { _, _ -> defaultKeylines }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val startSteps = listOf(
@@ -219,7 +227,9 @@
val strategy = Strategy { _, _ -> defaultKeylines }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val endSteps = listOf(
@@ -354,8 +364,18 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
- strategy2.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
+ strategy2.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isTrue()
assertThat(strategy1.hashCode()).isEqualTo(strategy2.hashCode())
@@ -372,8 +392,18 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
- strategy2.apply(availableSpace = 500f + 1f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
+ strategy2.apply(
+ availableSpace = 500f + 1f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isFalse()
assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -390,7 +420,12 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isFalse()
assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -403,7 +438,12 @@
val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
val defaultKeylineList = createStartAlignedKeylineList()
- val strategy = Strategy { _, _ -> defaultKeylineList }.apply(carouselMainAxisSize, 0f)
+ val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+ availableSpace = carouselMainAxisSize,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
.isEqualTo(defaultKeylineList)
@@ -419,7 +459,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 380f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val middleStep = strategy.endKeylineSteps[1]
val actualMiddleOffsets = middleStep.map { it.offset }.toFloatArray()
@@ -453,7 +498,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 768f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 768f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.startKeylineSteps).hasSize(3)
assertThat(strategy.endKeylineSteps).hasSize(3)
@@ -507,6 +557,38 @@
assertThat(e2ActualUnadjustedOffsets).isEqualTo(e2ExpectedUnadjustedOffsets)
}
+ @Test
+ fun testCenterStrategy_stepsShouldAccountForContentPadding() {
+ val strategy = Strategy { availableSpace, itemSpacing ->
+ keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
+ add(10f, isAnchor = true)
+ add(50f)
+ add(100f)
+ add(200f)
+ add(100f)
+ add(50f)
+ add(10f, isAnchor = true)
+ }
+ }.apply(500f, 0f, 16f, 24f)
+
+ val lastStartStep = strategy.startKeylineSteps.last()
+
+ val firstFocalLeft = lastStartStep.firstFocal.offset -
+ (lastStartStep.firstFocal.size / 2f)
+ val lastNonAnchorRight = lastStartStep.lastNonAnchor.offset +
+ (lastStartStep.lastNonAnchor.size / 2f)
+ val lastEndStep = strategy.endKeylineSteps.last()
+ val lastFocalRight = lastEndStep.lastFocal.offset +
+ (lastEndStep.lastFocal.size / 2f)
+ val firstNonAnchorLeft = lastEndStep.firstNonAnchor.offset -
+ (lastEndStep.firstNonAnchor.size / 2f)
+
+ assertThat(firstFocalLeft).isEqualTo(16f)
+ assertThat(lastNonAnchorRight).isEqualTo(500f)
+ assertThat(lastFocalRight).isEqualTo(500f - 24f)
+ assertThat(firstNonAnchorLeft).isWithin(.01f).of(0f)
+ }
+
private fun assertEqualWithFloatTolerance(
tolerance: Float,
actual: Keyline,
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
index 9f7ba239..cf6aa49 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
@@ -41,7 +41,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -67,7 +69,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -96,7 +100,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -134,7 +140,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index ca40239..e9da174 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -51,6 +51,7 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -61,8 +62,11 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
@@ -79,6 +83,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
@@ -106,7 +111,6 @@
*/
@Suppress("NotCloseable")
@Stable
-@OptIn(ExperimentalMaterial3Api::class)
class DrawerState(
initialValue: DrawerValue,
confirmStateChange: (DrawerValue) -> Boolean = { true }
@@ -303,7 +307,6 @@
* @param scrimColor color of the scrim that obscures content when the drawer is open
* @param content content of the rest of the UI
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModalNavigationDrawer(
drawerContent: @Composable () -> Unit,
@@ -406,7 +409,6 @@
* @param gesturesEnabled whether or not the drawer can be interacted by gestures
* @param content content of the rest of the UI
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DismissibleNavigationDrawer(
drawerContent: @Composable () -> Unit,
@@ -510,6 +512,10 @@
/**
* Content inside of a modal navigation drawer.
+
+ * Note: This version of [ModalDrawerSheet] does not handle back by default. For automatic back
+ * handling and predictive back animations on Android 14+, use the [ModalDrawerSheet] that accepts
+ * `drawerState` as a param.
*
* @param modifier the [Modifier] to be applied to this drawer's content
* @param drawerShape defines the shape of this drawer's container
@@ -535,6 +541,7 @@
content: @Composable ColumnScope.() -> Unit
) {
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier,
drawerShape,
@@ -546,8 +553,58 @@
}
/**
+ * Content inside of a modal navigation drawer.
+ *
+ * Note: This version of [ModalDrawerSheet] requires a [drawerState] to be provided and will handle
+ * back by default for all Android versions, as well as animate during predictive back on Android
+ * 14+.
+ *
+ * @param drawerState state of the drawer
+ * @param modifier the [Modifier] to be applied to this drawer's content
+ * @param drawerShape defines the shape of this drawer's container
+ * @param drawerContainerColor the color used for the background of this drawer. Use
+ * [Color.Transparent] to have no color.
+ * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
+ * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
+ * [drawerContainerColor] is not a color from the theme.
+ * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
+ * primary color overlay is applied on top of the container. A higher tonal elevation value will
+ * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param windowInsets a window insets for the sheet.
+ * @param content content inside of a modal navigation drawer
+ */
+@Composable
+fun ModalDrawerSheet(
+ drawerState: DrawerState,
+ modifier: Modifier = Modifier,
+ drawerShape: Shape = DrawerDefaults.shape,
+ drawerContainerColor: Color = DrawerDefaults.modalContainerColor,
+ drawerContentColor: Color = contentColorFor(drawerContainerColor),
+ drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
+ windowInsets: WindowInsets = DrawerDefaults.windowInsets,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
+ DrawerSheet(
+ drawerPredictiveBackState,
+ windowInsets,
+ modifier,
+ drawerShape,
+ drawerContainerColor,
+ drawerContentColor,
+ drawerTonalElevation,
+ content
+ )
+ }
+}
+
+/**
* Content inside of a dismissible navigation drawer.
*
+ * Note: This version of [DismissibleDrawerSheet] does not handle back by default. For automatic
+ * back handling and predictive back animations on Android 14+, use the [DismissibleDrawerSheet]
+ * that accepts `drawerState` as a param.
+ *
* @param modifier the [Modifier] to be applied to this drawer's content
* @param drawerShape defines the shape of this drawer's container
* @param drawerContainerColor the color used for the background of this drawer. Use
@@ -572,6 +629,7 @@
content: @Composable ColumnScope.() -> Unit
) {
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier,
drawerShape,
@@ -583,6 +641,52 @@
}
/**
+ * Content inside of a dismissible navigation drawer.
+
+ * Note: This version of [DismissibleDrawerSheet] requires a [drawerState] to be provided and will
+ * handle back by default for all Android versions, as well as animate during predictive back on
+ * Android 14+.
+ *
+ * @param drawerState state of the drawer
+ * @param modifier the [Modifier] to be applied to this drawer's content
+ * @param drawerShape defines the shape of this drawer's container
+ * @param drawerContainerColor the color used for the background of this drawer. Use
+ * [Color.Transparent] to have no color.
+ * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
+ * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
+ * [drawerContainerColor] is not a color from the theme.
+ * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
+ * primary color overlay is applied on top of the container. A higher tonal elevation value will
+ * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param windowInsets a window insets for the sheet.
+ * @param content content inside of a dismissible navigation drawer
+ */
+@Composable
+fun DismissibleDrawerSheet(
+ drawerState: DrawerState,
+ modifier: Modifier = Modifier,
+ drawerShape: Shape = RectangleShape,
+ drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
+ drawerContentColor: Color = contentColorFor(drawerContainerColor),
+ drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation,
+ windowInsets: WindowInsets = DrawerDefaults.windowInsets,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
+ DrawerSheet(
+ drawerPredictiveBackState,
+ windowInsets,
+ modifier,
+ drawerShape,
+ drawerContainerColor,
+ drawerContentColor,
+ drawerTonalElevation,
+ content
+ )
+ }
+}
+
+/**
* Content inside of a permanent navigation drawer.
*
* @param modifier the [Modifier] to be applied to this drawer's content
@@ -610,6 +714,7 @@
) {
val navigationMenu = getString(Strings.NavigationMenu)
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier.semantics {
paneTitle = navigationMenu
@@ -623,7 +728,8 @@
}
@Composable
-private fun DrawerSheet(
+internal fun DrawerSheet(
+ drawerPredictiveBackState: DrawerPredictiveBackState?,
windowInsets: WindowInsets,
modifier: Modifier = Modifier,
drawerShape: Shape = RectangleShape,
@@ -632,30 +738,93 @@
drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation,
content: @Composable ColumnScope.() -> Unit
) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val predictiveBackDrawerContainerModifier =
+ if (drawerPredictiveBackState != null) Modifier.predictiveBackDrawerContainer(
+ drawerPredictiveBackState,
+ isRtl
+ ) else Modifier
Surface(
modifier = modifier
.sizeIn(
minWidth = MinimumDrawerWidth,
maxWidth = DrawerDefaults.MaximumDrawerWidth
)
+ .then(predictiveBackDrawerContainerModifier)
.fillMaxHeight(),
shape = drawerShape,
color = drawerContainerColor,
contentColor = drawerContentColor,
tonalElevation = drawerTonalElevation
) {
+ val predictiveBackDrawerChildModifier =
+ if (drawerPredictiveBackState != null) Modifier.predictiveBackDrawerChild(
+ drawerPredictiveBackState,
+ isRtl
+ ) else Modifier
Column(
Modifier
.sizeIn(
minWidth = MinimumDrawerWidth,
maxWidth = DrawerDefaults.MaximumDrawerWidth
)
+ .then(predictiveBackDrawerChildModifier)
.windowInsetsPadding(windowInsets),
content = content
)
}
}
+private fun Modifier.predictiveBackDrawerContainer(
+ drawerPredictiveBackState: DrawerPredictiveBackState,
+ isRtl: Boolean
+) = graphicsLayer {
+ scaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
+ scaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
+ transformOrigin = TransformOrigin(if (isRtl) 1f else 0f, 0.5f)
+}
+
+private fun Modifier.predictiveBackDrawerChild(
+ drawerPredictiveBackState: DrawerPredictiveBackState,
+ isRtl: Boolean
+) = graphicsLayer {
+ // Preserve the original aspect ratio and container alignment of the child
+ // content, and add content margins.
+ val containerScaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
+ val containerScaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
+ scaleX = if (containerScaleX != 0f) containerScaleY / containerScaleX else 1f
+ transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f)
+}
+
+private fun GraphicsLayerScope.calculatePredictiveBackScaleX(
+ drawerPredictiveBackState: DrawerPredictiveBackState
+): Float {
+ val width = size.width
+ return if (width.isNaN() || width == 0f) {
+ 1f
+ } else {
+ val scaleXDirection = if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) 1 else -1
+ 1f + drawerPredictiveBackState.scaleXDistance * scaleXDirection / width
+ }
+}
+
+private fun GraphicsLayerScope.calculatePredictiveBackScaleY(
+ drawerPredictiveBackState: DrawerPredictiveBackState
+): Float {
+ val height = size.height
+ return if (height.isNaN() || height == 0f) {
+ 1f
+ } else {
+ 1f - drawerPredictiveBackState.scaleYDistance / height
+ }
+}
+
+@Composable
+internal expect fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+)
+
/**
* Object to hold default values for [ModalNavigationDrawer]
*/
@@ -861,6 +1030,36 @@
val ItemPadding = PaddingValues(horizontal = 12.dp)
}
+@Stable
+internal class DrawerPredictiveBackState {
+
+ var swipeEdgeMatchesDrawer by mutableStateOf(true)
+
+ var scaleXDistance by mutableFloatStateOf(0f)
+
+ var scaleYDistance by mutableFloatStateOf(0f)
+
+ fun update(
+ progress: Float,
+ swipeEdgeLeft: Boolean,
+ isRtl: Boolean,
+ maxScaleXDistanceGrow: Float,
+ maxScaleXDistanceShrink: Float,
+ maxScaleYDistance: Float
+ ) {
+ swipeEdgeMatchesDrawer = swipeEdgeLeft != isRtl
+ val maxScaleXDistance =
+ if (swipeEdgeMatchesDrawer) maxScaleXDistanceGrow else maxScaleXDistanceShrink
+ scaleXDistance = lerp(0f, maxScaleXDistance, progress)
+ scaleYDistance = lerp(0f, maxScaleYDistance, progress)
+ }
+ fun clear() {
+ swipeEdgeMatchesDrawer = true
+ scaleXDistance = 0f
+ scaleYDistance = 0f
+ }
+}
+
private class DefaultDrawerItemsColor(
val selectedIconColor: Color,
val unselectedIconColor: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 935dc9e..05f80c2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -53,7 +53,6 @@
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@@ -269,7 +268,6 @@
},
valueRange: ClosedFloatingPointRange<Float> = 0f..1f
) {
- val onValueChangeFinishedState = rememberUpdatedState(onValueChangeFinished)
val state = remember(
steps,
valueRange
@@ -277,11 +275,12 @@
SliderState(
value,
steps,
- { onValueChangeFinishedState.value?.invoke() },
+ onValueChangeFinished,
valueRange
)
}
+ state.onValueChangeFinished = onValueChangeFinished
state.onValueChange = onValueChange
state.value = value
@@ -546,7 +545,6 @@
@IntRange(from = 0)
steps: Int = 0
) {
- val onValueChangeFinishedState = rememberUpdatedState(onValueChangeFinished)
val state = remember(
steps,
valueRange
@@ -555,11 +553,12 @@
value.start,
value.endInclusive,
steps,
- { onValueChangeFinishedState.value?.invoke() },
+ onValueChangeFinished,
valueRange
)
}
+ state.onValueChangeFinished = onValueChangeFinished
state.onValueChange = { onValueChange(it.start..it.endInclusive) }
state.activeRangeStart = value.start
state.activeRangeEnd = value.endInclusive
@@ -1332,6 +1331,7 @@
val gap =
if (thumbTrackGapSize > 0.dp) thumbWidth.toPx() / 2 + thumbTrackGapSize.toPx() else 0f
+ // inactive track (range slider)
if (isRangeSlider && sliderValueStart.x > sliderStart.x + gap + cornerSize) {
val start = sliderStart.x
val end = sliderValueStart.x - gap
@@ -1344,6 +1344,7 @@
)
drawStopIndicator?.invoke(this, Offset(start + cornerSize, center.y))
}
+ // inactive track
if (sliderValueEnd.x < sliderEnd.x - gap - cornerSize) {
val start = sliderValueEnd.x + gap
val end = sliderEnd.x
@@ -1356,11 +1357,12 @@
)
drawStopIndicator?.invoke(this, Offset(end - cornerSize, center.y))
}
+ // active track
val activeTrackStart =
if (isRangeSlider) sliderValueStart.x + gap else 0f
val activeTrackEnd = sliderValueEnd.x - gap
val startCornerRadius = if (isRangeSlider) insideCornerSize else cornerSize
- if (activeTrackEnd - activeTrackStart > startCornerRadius + gap) {
+ if (activeTrackEnd - activeTrackStart > startCornerRadius) {
drawTrackPath(
Offset(activeTrackStart, 0f),
Size(activeTrackEnd - activeTrackStart, trackStrokeWidth),
@@ -1396,29 +1398,51 @@
startCornerRadius: Float,
endCornerRadius: Float
) {
- val startCorner = RoundRect(
- rect = Rect(
- offset,
- size = Size(startCornerRadius * 2, size.height)
- ), cornerRadius = CornerRadius(startCornerRadius)
- )
+ trackPath.rewind()
+
val track =
Rect(
Offset(offset.x + startCornerRadius, 0f),
size = Size(size.width - startCornerRadius - endCornerRadius, size.height)
)
- val endCorner = RoundRect(
- rect = Rect(
- Offset(offset.x + startCornerRadius + track.width - endCornerRadius, 0f),
- size = Size(endCornerRadius * 2, size.height)
- ), cornerRadius = CornerRadius(endCornerRadius)
- )
+ trackPath.addRect(track)
- val path = Path()
- path.addRoundRect(startCorner)
- path.addRect(track)
- path.addRoundRect(endCorner)
- drawPath(path, color)
+ buildCorner(offset, size, startCornerRadius, isStart = true) // start
+ buildCorner(Offset(track.right - endCornerRadius, 0f), size, endCornerRadius) // end
+
+ drawPath(trackPath, color)
+
+ trackPath.rewind()
+ }
+
+ private fun buildCorner(
+ offset: Offset,
+ size: Size,
+ cornerRadius: Float,
+ isStart: Boolean = false
+ ) {
+ cornerPath.rewind()
+ halfRectPath.rewind()
+
+ val corner = RoundRect(
+ rect = Rect(
+ offset,
+ size = Size(cornerRadius * 2, size.height)
+ ), cornerRadius = CornerRadius(cornerRadius)
+ )
+ cornerPath.addRoundRect(corner)
+
+ // delete the unnecessary half of the RoundRect
+ halfRectPath.addRect(
+ Rect(
+ Offset(corner.left + if (isStart) cornerRadius else 0f, 0f),
+ size = Size(cornerRadius, size.height)
+ )
+ )
+ trackPath.addPath(cornerPath - halfRectPath)
+
+ cornerPath.rewind()
+ halfRectPath.rewind()
}
private fun DrawScope.drawStopIndicator(
@@ -1432,6 +1456,10 @@
radius = size.toPx() / 2f
)
}
+
+ private val trackPath = Path()
+ private val cornerPath = Path()
+ private val halfRectPath = Path()
}
private fun snapValueToTick(
@@ -1980,13 +2008,12 @@
* @param valueRange range of values that Slider values can take. [value] will be
* coerced to this range.
*/
-@Stable
@ExperimentalMaterial3Api
class SliderState(
value: Float = 0f,
@IntRange(from = 0)
val steps: Int = 0,
- val onValueChangeFinished: (() -> Unit)? = null,
+ var onValueChangeFinished: (() -> Unit)? = null,
val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
) : DraggableState {
@@ -2110,14 +2137,13 @@
* @param valueRange range of values that Range Slider values can take. [activeRangeStart]
* and [activeRangeEnd] will be coerced to this range.
*/
-@Stable
@ExperimentalMaterial3Api
class RangeSliderState(
activeRangeStart: Float = 0f,
activeRangeEnd: Float = 1f,
@IntRange(from = 0)
val steps: Int = 0,
- val onValueChangeFinished: (() -> Unit)? = null,
+ var onValueChangeFinished: (() -> Unit)? = null,
val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
) {
private var activeRangeStartState by mutableFloatStateOf(activeRangeStart)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
index c55ac5116..05b7a1b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
@@ -122,25 +122,6 @@
}
/**
- * Whether the component has been dismissed in the given [direction].
- *
- * @param direction The dismiss direction.
- */
- @Deprecated(
- message = "DismissDirection is no longer used by SwipeToDismissBoxState. Please compare " +
- "currentValue against SwipeToDismissValue instead.",
- level = DeprecationLevel.HIDDEN
- )
- @Suppress("DEPRECATION")
- fun isDismissed(direction: DismissDirection): Boolean {
- val directionalDismissValue = when (direction) {
- DismissDirection.StartToEnd -> SwipeToDismissBoxValue.StartToEnd
- DismissDirection.EndToStart -> SwipeToDismissBoxValue.EndToStart
- }
- return currentValue == directionalDismissValue
- }
-
- /**
* Set the state without any animation and suspend until it's set
*
* @param targetValue The new target value
@@ -226,47 +207,6 @@
* @sample androidx.compose.material3.samples.SwipeToDismissListItems
*
* @param state The state of this component.
- * @param background A composable that is stacked behind the content and is exposed when the
- * content is swiped. You can/should use the [state] to have different backgrounds on each side.
- * @param dismissContent The content that can be dismissed.
- * @param modifier Optional [Modifier] for this component.
- * @param directions The set of directions in which the component can be dismissed.
- */
-@Composable
-@Deprecated(
- level = DeprecationLevel.WARNING,
- message = "Use SwipeToDismissBox instead",
- replaceWith =
- ReplaceWith(
- "SwipeToDismissBox(state, background, modifier, " +
- "enableDismissFromStartToEnd, enableDismissFromEndToStart, dismissContent)"
- )
-)
-@ExperimentalMaterial3Api
-fun SwipeToDismiss(
- state: SwipeToDismissBoxState,
- background: @Composable RowScope.() -> Unit,
- dismissContent: @Composable RowScope.() -> Unit,
- modifier: Modifier = Modifier,
- directions: Set<SwipeToDismissBoxValue> = setOf(
- SwipeToDismissBoxValue.EndToStart,
- SwipeToDismissBoxValue.StartToEnd
- ),
-) = SwipeToDismissBox(
- state = state,
- backgroundContent = background,
- modifier = modifier,
- enableDismissFromStartToEnd = SwipeToDismissBoxValue.StartToEnd in directions,
- enableDismissFromEndToStart = SwipeToDismissBoxValue.EndToStart in directions,
- content = dismissContent
-)
-
-/**
- * A composable that can be dismissed by swiping left or right.
- *
- * @sample androidx.compose.material3.samples.SwipeToDismissListItems
- *
- * @param state The state of this component.
* @param backgroundContent A composable that is stacked behind the [content] and is exposed when
* the content is swiped. You can/should use the [state] to have different backgrounds on each side.
* @param modifier Optional [Modifier] for this component.
@@ -332,51 +272,4 @@
}
}
-/**
- * The directions in which a [SwipeToDismissBox] can be dismissed.
- */
-@ExperimentalMaterial3Api
-@Deprecated(
- message = "Dismiss direction is no longer used by SwipeToDismissBoxState. Please use " +
- "SwipeToDismissBoxValue instead.",
- level = DeprecationLevel.WARNING
-)
-enum class DismissDirection {
- /**
- * Can be dismissed by swiping in the reading direction.
- */
- StartToEnd,
-
- /**
- * Can be dismissed by swiping in the reverse of the reading direction.
- */
- EndToStart,
-}
-
-/**
- * Possible values of [SwipeToDismissBoxState].
- */
-@ExperimentalMaterial3Api
-@Deprecated(
- message = "DismissValue is no longer used by SwipeToDismissBoxState. Please use " +
- "SwipeToDismissBoxValue instead.",
- level = DeprecationLevel.WARNING
-)
-enum class DismissValue {
- /**
- * Indicates the component has not been dismissed yet.
- */
- Default,
-
- /**
- * Indicates the component has been dismissed in the reading direction.
- */
- DismissedToEnd,
-
- /**
- * Indicates the component has been dismissed in the reverse of the reading direction.
- */
- DismissedToStart
-}
-
private val DismissVelocityThreshold = 125.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index 1a41903..b81a1fe 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -29,6 +29,9 @@
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerDefaults
@@ -65,18 +68,24 @@
* A horizontal carousel meant to display many items at once for quick browsing of smaller content
* like album art or photo thumbnails.
*
- * Note that this carousel may adjust the size of large items. In order to ensure a mix of large,
+ * Note that this carousel may adjust the size of items in order to ensure a mix of large,
* medium, and small items fit perfectly into the available space and are arranged in a
- * visually pleasing way, this carousel finds the nearest number of large items that
- * will fit the container and adjusts their size to fit, if necessary.
+ * visually pleasing way. Carousel then lays out items using the large item size and clips
+ * (or masks) items depending on their scroll offset to create items which smoothly expand
+ * and collapse between the large, medium, and small sizes.
*
* For more information, see <a href="https://material.io/components/carousel/overview">design
* guidelines</a>.
*
+ * Example of a multi-browse carousel:
+ * @sample androidx.compose.material3.samples.HorizontalMultiBrowseCarouselSample
+ *
* @param state The state object to be used to control the carousel's state
- * @param preferredItemWidth The width the fully visible items would like to be in the main axis.
- * This width is a target and will likely be adjusted by carousel in order to fit a whole number of
- * items within the container
+ * @param preferredItemWidth The width that large, fully visible items would like to be in the
+ * horizontal axis. This width is a target and will likely be adjusted by carousel in order to fit
+ * a whole number of items within the container. Carousel adjusts small items first (between the
+ * [minSmallItemWidth] and [maxSmallItemWidth]) then medium items when present, and finally large
+ * items if necessary.
* @param modifier A modifier instance to be applied to this carousel container
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
@@ -86,13 +95,14 @@
* @param maxSmallItemWidth The maximum allowable width of small items in dp. Depending on the
* [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
* range of [minSmallItemWidth] and [maxSmallItemWidth]
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param content The carousel's content Composable
- *
- * TODO: Add sample link
*/
@ExperimentalMaterial3Api
@Composable
-internal fun HorizontalMultiBrowseCarousel(
+fun HorizontalMultiBrowseCarousel(
state: CarouselState,
preferredItemWidth: Dp,
modifier: Modifier = Modifier,
@@ -101,6 +111,7 @@
CarouselDefaults.singleAdvanceFlingBehavior(state = state),
minSmallItemWidth: Dp = CarouselDefaults.MinSmallItemSize,
maxSmallItemWidth: Dp = CarouselDefaults.MaxSmallItemSize,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
@@ -120,6 +131,7 @@
)
}
},
+ contentPadding = contentPadding,
modifier = modifier,
itemSpacing = itemSpacing,
flingBehavior = flingBehavior,
@@ -140,23 +152,28 @@
* For more information, see <a href="https://material.io/components/carousel/overview">design
* guidelines</a>.
*
+ * Example of an uncontained carousel:
+ * @sample androidx.compose.material3.samples.HorizontalUncontainedCarouselSample
+ *
* @param state The state object to be used to control the carousel's state
* @param itemWidth The width of items in the carousel
* @param modifier A modifier instance to be applied to this carousel container
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param content The carousel's content Composable
- *
- * TODO: Add sample link
*/
@ExperimentalMaterial3Api
@Composable
-internal fun HorizontalUncontainedCarousel(
+fun HorizontalUncontainedCarousel(
state: CarouselState,
itemWidth: Dp,
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior = CarouselDefaults.noSnapFlingBehavior(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
@@ -173,6 +190,7 @@
)
}
},
+ contentPadding = contentPadding,
modifier = modifier,
itemSpacing = itemSpacing,
flingBehavior = flingBehavior,
@@ -190,19 +208,22 @@
* @param orientation The layout orientation of the carousel
* @param keylineList The list of keylines that are fixed positions along the scrolling axis which
* define the state an item should be in when its center is co-located with the keyline's position.
+ * @param contentPadding a padding around the whole content. This will add padding for the
* @param modifier A modifier instance to be applied to this carousel outer layout
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
* @param content The carousel's content Composable where each call is passed the index, from the
* total item count, of the item being composed
- * TODO: Add sample link
*/
-@ExperimentalMaterial3Api
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun Carousel(
state: CarouselState,
orientation: Orientation,
keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior =
@@ -210,7 +231,11 @@
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- val pageSize = remember(keylineList) { CarouselPageSize(keylineList) }
+ val beforeContentPadding = contentPadding.calculateBeforeContentPadding(orientation)
+ val afterContentPadding = contentPadding.calculateAfterContentPadding(orientation)
+ val pageSize = remember(keylineList) {
+ CarouselPageSize(keylineList, beforeContentPadding, afterContentPadding)
+ }
val outOfBoundsPageCount = remember(pageSize.strategy.itemMainAxisSize) {
calculateOutOfBounds(pageSize.strategy)
@@ -228,6 +253,11 @@
if (orientation == Orientation.Horizontal) {
HorizontalPager(
state = state.pagerState,
+ // Only pass cross axis padding as main axis padding will be handled by the strategy
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding(),
+ bottom = contentPadding.calculateBottomPadding()
+ ),
pageSize = pageSize,
pageSpacing = itemSpacing,
outOfBoundsPageCount = outOfBoundsPageCount,
@@ -250,6 +280,11 @@
} else if (orientation == Orientation.Vertical) {
VerticalPager(
state = state.pagerState,
+ // Only pass cross axis padding as main axis padding will be handled by the strategy
+ contentPadding = PaddingValues(
+ start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),
+ end = contentPadding.calculateEndPadding(LocalLayoutDirection.current)
+ ),
pageSize = pageSize,
pageSpacing = itemSpacing,
outOfBoundsPageCount = outOfBoundsPageCount,
@@ -272,6 +307,28 @@
}
}
+@Composable
+private fun PaddingValues.calculateBeforeContentPadding(orientation: Orientation): Float {
+ val dpValue = if (orientation == Orientation.Vertical) {
+ calculateTopPadding()
+ } else {
+ calculateStartPadding(LocalLayoutDirection.current)
+ }
+
+ return with(LocalDensity.current) { dpValue.toPx() }
+}
+
+@Composable
+private fun PaddingValues.calculateAfterContentPadding(orientation: Orientation): Float {
+ val dpValue = if (orientation == Orientation.Vertical) {
+ calculateBottomPadding()
+ } else {
+ calculateEndPadding(LocalLayoutDirection.current)
+ }
+
+ return with(LocalDensity.current) { dpValue.toPx() }
+}
+
internal fun calculateOutOfBounds(strategy: Strategy): Int {
if (!strategy.isValid()) {
return PagerDefaults.OutOfBoundsPageCount
@@ -297,11 +354,18 @@
* define the state an item should be in when its center is co-located with the keyline's position.
*/
private class CarouselPageSize(
- keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?
+ keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+ private val beforeContentPadding: Float,
+ private val afterContentPadding: Float
) : PageSize {
val strategy = Strategy(keylineList)
override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
- strategy.apply(availableSpace.toFloat(), pageSpacing.toFloat())
+ strategy.apply(
+ availableSpace.toFloat(),
+ pageSpacing.toFloat(),
+ beforeContentPadding,
+ afterContentPadding
+ )
return if (strategy.isValid()) {
strategy.itemMainAxisSize.roundToInt()
} else {
@@ -337,7 +401,7 @@
* @param state the carousel state
* @param strategy the strategy used to mask and translate items in the carousel
* @param itemPositionMap the position of each index when it is the current item
- * @param isRtl whether or not the carousel is rtl
+ * @param isRtl true if the layout direction is right-to-left
*/
@OptIn(ExperimentalMaterial3Api::class)
internal fun Modifier.carouselItem(
@@ -503,12 +567,7 @@
* Contains the default values used by [Carousel].
*/
@ExperimentalMaterial3Api
-internal object CarouselDefaults {
- /** The minimum size that a carousel strategy can choose its small items to be. **/
- val MinSmallItemSize = 40.dp
-
- /** The maximum size that a carousel strategy can choose its small items to be. **/
- val MaxSmallItemSize = 56.dp
+object CarouselDefaults {
/**
* A [TargetedFlingBehavior] that limits a fling to one item at a time. [snapAnimationSpec] can
@@ -602,6 +661,12 @@
return rememberSnapFlingBehavior(snapLayoutInfoProvider = decayLayoutInfoProvider)
}
+ /** The minimum size that a carousel strategy can choose its small items to be. **/
+ internal val MinSmallItemSize = 40.dp
+
+ /** The maximum size that a carousel strategy can choose its small items to be. **/
+ internal val MaxSmallItemSize = 56.dp
+
internal val AnchorSize = 10.dp
internal const val MediumLargeItemDiffThreshold = 0.85f
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
index 94dbda2..467d8a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
@@ -22,7 +22,7 @@
* Receiver scope for [Carousel].
*/
@ExperimentalMaterial3Api
-internal sealed interface CarouselScope
+sealed interface CarouselScope
@ExperimentalMaterial3Api
internal object CarouselScopeImpl : CarouselScope
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
index b6d989a..1046082 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
@@ -16,7 +16,7 @@
package androidx.compose.material3.carousel
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.annotation.FloatRange
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
@@ -32,14 +32,15 @@
* The state that can be used to control all types of carousels.
*
* @param currentItem the current item to be scrolled to.
- * @param currentItemOffsetFraction the current item offset as a fraction of the item size.
+ * @param currentItemOffsetFraction the offset of the current item as a fraction of the item's size.
+ * This should vary between -0.5 and 0.5 and indicates how to offset the current item from the
+ * snapped position.
* @param itemCount the number of items this Carousel will have.
*/
-@OptIn(ExperimentalFoundationApi::class)
@ExperimentalMaterial3Api
-internal class CarouselState(
+class CarouselState(
currentItem: Int = 0,
- currentItemOffsetFraction: Float = 0F,
+ @FloatRange(from = -0.5, to = 0.5) currentItemOffsetFraction: Float = 0f,
itemCount: () -> Int
) : ScrollableState {
var itemCountState = mutableStateOf(itemCount)
@@ -92,7 +93,7 @@
*/
@ExperimentalMaterial3Api
@Composable
-internal fun rememberCarouselState(
+fun rememberCarouselState(
initialItem: Int = 0,
itemCount: () -> Int,
): CarouselState {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
index c8a7af0..9caf196 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
@@ -44,21 +44,21 @@
val endStepsSize = endKeylineSteps.size + numOfFocalKeylines
for (itemIndex in 0 until itemCount) {
- map[itemIndex] = (defaultKeylines.firstFocal.offset -
- defaultKeylines.firstFocal.size / 2F).roundToInt()
+ map[itemIndex] = (defaultKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2F).roundToInt()
if (itemIndex < startStepsSize) {
var startIndex = max(0, startStepsSize - 1 - itemIndex)
startIndex = min(startKeylineSteps.size - 1, startIndex)
val startKeylines = startKeylineSteps[startIndex]
- map[itemIndex] = (startKeylines.firstFocal.offset -
- startKeylines.firstFocal.size / 2f).roundToInt()
+ map[itemIndex] = (startKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2f).roundToInt()
}
if (itemCount > numOfFocalKeylines + 1 && itemIndex >= itemCount - endStepsSize) {
var endIndex = max(0, itemIndex - itemCount + endStepsSize)
endIndex = min(endKeylineSteps.size - 1, endIndex)
val endKeylines = endKeylineSteps[endIndex]
- map[itemIndex] = (endKeylines.firstFocal.offset -
- endKeylines.firstFocal.size / 2f).roundToInt()
+ map[itemIndex] = (endKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2f).roundToInt()
}
}
return map
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
index 9c654b2..6b47ff5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
@@ -65,10 +65,7 @@
// of the large item and medium items are sized between large and small items. Clamp the
// small target size within our min-max range and as close to 1/3 of the target large item
// size as possible.
- val targetSmallSize: Float = (targetLargeSize / 3f + itemSpacing).coerceIn(
- minSmallItemSize + itemSpacing,
- maxSmallItemSize + itemSpacing
- )
+ val targetSmallSize: Float = (targetLargeSize / 3f).coerceIn(minSmallItemSize, maxSmallItemSize)
val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
if (carouselMainAxisSize < minSmallItemSize * 2) {
@@ -108,15 +105,15 @@
var mediumCount = arrangement.mediumCount
while (keylineSurplus > 0) {
if (smallCount > 0) {
- smallCount -= 1;
+ smallCount -= 1
} else if (mediumCount > 1) {
// Keep at least 1 medium so the large items don't fill the entire carousel in new
// strategy.
- mediumCount -= 1;
+ mediumCount -= 1
}
// large items don't need to be removed even if they are a surplus because large items
// are already fully unmasked.
- keylineSurplus -= 1;
+ keylineSurplus -= 1
}
arrangement = Arrangement.findLowestCostArrangement(
availableSpace = carouselMainAxisSize,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 5641be1..916efe0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -22,8 +22,11 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMapIndexed
import androidx.compose.ui.util.lerp
+import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
@@ -84,6 +87,8 @@
internal var availableSpace: Float = 0f
/** The spacing between each item. */
internal var itemSpacing: Float = 0f
+ internal var beforeContentPadding: Float = 0f
+ internal var afterContentPadding: Float = 0f
/** The size of items when in focus and fully unmasked. */
internal var itemMainAxisSize by mutableFloatStateOf(0f)
@@ -101,8 +106,15 @@
* This method must be called before a strategy can be used by carousel.
*
* @param availableSpace the size of the carousel container in scrolling axis
+ * @param beforeContentPadding the padding to add before the list content
+ * @param afterContentPadding the padding to add after the list content
*/
- internal fun apply(availableSpace: Float, itemSpacing: Float): Strategy {
+ internal fun apply(
+ availableSpace: Float,
+ itemSpacing: Float,
+ beforeContentPadding: Float,
+ afterContentPadding: Float
+ ): Strategy {
// Skip computing new keylines and updating this strategy if
// available space has not changed.
if (this.availableSpace == availableSpace && this.itemSpacing == itemSpacing) {
@@ -110,15 +122,16 @@
}
val keylineList = keylineList.invoke(availableSpace, itemSpacing) ?: return this
- val startKeylineSteps = getStartKeylineSteps(keylineList, availableSpace, itemSpacing)
+ val startKeylineSteps =
+ getStartKeylineSteps(keylineList, availableSpace, itemSpacing, beforeContentPadding)
val endKeylineSteps =
- getEndKeylineSteps(keylineList, availableSpace, itemSpacing)
+ getEndKeylineSteps(keylineList, availableSpace, itemSpacing, afterContentPadding)
// TODO: Update this to use the first/last focal keylines to calculate shift?
- val startShiftDistance = startKeylineSteps.last().first().unadjustedOffset -
- keylineList.first().unadjustedOffset
- val endShiftDistance = keylineList.last().unadjustedOffset -
- endKeylineSteps.last().last().unadjustedOffset
+ val startShiftDistance = max(startKeylineSteps.last().first().unadjustedOffset -
+ keylineList.first().unadjustedOffset, beforeContentPadding)
+ val endShiftDistance = max(keylineList.last().unadjustedOffset -
+ endKeylineSteps.last().last().unadjustedOffset, afterContentPadding)
this.defaultKeylines = keylineList
this.defaultKeylines = keylineList
@@ -138,6 +151,8 @@
)
this.availableSpace = availableSpace
this.itemSpacing = itemSpacing
+ this.beforeContentPadding = beforeContentPadding
+ this.afterContentPadding = afterContentPadding
this.itemMainAxisSize = defaultKeylines.firstFocal.size
return this
@@ -229,6 +244,8 @@
if (isValid() != other.isValid()) return false
if (availableSpace != other.availableSpace) return false
if (itemSpacing != other.itemSpacing) return false
+ if (beforeContentPadding != other.beforeContentPadding) return false
+ if (afterContentPadding != other.afterContentPadding) return false
if (itemMainAxisSize != other.itemMainAxisSize) return false
if (startShiftDistance != other.startShiftDistance) return false
if (endShiftDistance != other.endShiftDistance) return false
@@ -247,6 +264,8 @@
var result = isValid().hashCode()
result = 31 * result + availableSpace.hashCode()
result = 31 * result + itemSpacing.hashCode()
+ result = 31 * result + beforeContentPadding.hashCode()
+ result = 31 * result + afterContentPadding.hashCode()
result = 31 * result + itemMainAxisSize.hashCode()
result = 31 * result + startShiftDistance.hashCode()
result = 31 * result + endShiftDistance.hashCode()
@@ -277,12 +296,23 @@
private fun getStartKeylineSteps(
defaultKeylines: KeylineList,
carouselMainAxisSize: Float,
- itemSpacing: Float
+ itemSpacing: Float,
+ beforeContentPadding: Float
): List<KeylineList> {
val steps: MutableList<KeylineList> = mutableListOf()
steps.add(defaultKeylines)
if (defaultKeylines.isFirstFocalItemAtStartOfContainer()) {
+ if (beforeContentPadding != 0f) {
+ steps.add(
+ createShiftedKeylineListForContentPadding(
+ defaultKeylines,
+ carouselMainAxisSize,
+ itemSpacing,
+ beforeContentPadding
+ )
+ )
+ }
return steps
}
@@ -329,6 +359,15 @@
i++
}
+ if (beforeContentPadding != 0f) {
+ steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+ steps.last(),
+ carouselMainAxisSize,
+ itemSpacing,
+ beforeContentPadding
+ )
+ }
+
return steps
}
@@ -351,12 +390,21 @@
private fun getEndKeylineSteps(
defaultKeylines: KeylineList,
carouselMainAxisSize: Float,
- itemSpacing: Float
+ itemSpacing: Float,
+ afterContentPadding: Float
): List<KeylineList> {
val steps: MutableList<KeylineList> = mutableListOf()
steps.add(defaultKeylines)
if (defaultKeylines.isLastFocalItemAtEndOfContainer(carouselMainAxisSize)) {
+ if (afterContentPadding != 0f) {
+ steps.add(createShiftedKeylineListForContentPadding(
+ defaultKeylines,
+ carouselMainAxisSize,
+ itemSpacing,
+ -afterContentPadding
+ ))
+ }
return steps
}
@@ -403,10 +451,54 @@
i++
}
+ if (afterContentPadding != 0f) {
+ steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+ steps.last(),
+ carouselMainAxisSize,
+ itemSpacing,
+ -afterContentPadding
+ )
+ }
+
return steps
}
/**
+ * Returns a new [KeylineList] identical to [from] but with each keyline's offset shifted
+ * by [contentPadding].
+ */
+ private fun createShiftedKeylineListForContentPadding(
+ from: KeylineList,
+ carouselMainAxisSize: Float,
+ itemSpacing: Float,
+ contentPadding: Float
+ ): KeylineList {
+ val numberOfNonAnchorKeylines = from.fastFilter { !it.isAnchor }.count()
+ val sizeReduction = contentPadding / numberOfNonAnchorKeylines
+ // Let keylineListOf create a new keyline list with offsets adjusted for each item's
+ // reduction in size
+ val newKeylines = keylineListOf(
+ carouselMainAxisSize = carouselMainAxisSize,
+ itemSpacing = itemSpacing,
+ pivotIndex = from.pivotIndex,
+ pivotOffset = from.pivot.offset + contentPadding - (sizeReduction / 2f)
+ ) {
+ from.fastForEach { k -> add(k.size - abs(sizeReduction), k.isAnchor) }
+ }
+
+ // Then reset each item's unadjusted offset back to their original value from the
+ // incoming keyline list. This is necessary because Pager will still be laying out items
+ // end-to-end with the original page size and not the new reduced size.
+ return KeylineList(
+ newKeylines.fastMapIndexed { i, k ->
+ k.copy(
+ unadjustedOffset = from[i].unadjustedOffset
+ )
+ }
+ )
+ }
+
+ /**
* Returns a new [KeylineList] where the keyline at [srcIndex] is moved to [dstIndex] and
* with updated pivot and offsets that reflect any change in focal shift.
*/
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
new file mode 100644
index 0000000..94baaa8
--- /dev/null
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+
+/**
+ * A predictive back handler that does nothing when running in a desktop context, since predictive
+ * back is only supported on Android.
+ *
+ * @param drawerState state of the drawer
+ * @param content content of the rest of the UI
+ */
+@Composable
+internal actual fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+) {
+ content(remember { DrawerPredictiveBackState() })
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 02f6601..d2c2079 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
* IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
* `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
*/
- const val version: Int = 12400
+ const val version: Int = 12500
}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index bd8f832..f65a14b 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -733,11 +733,11 @@
public final class PathHitTester {
ctor public PathHitTester();
method public operator boolean contains(long position);
- method public void updatePath(androidx.compose.ui.graphics.Path path, optional float tolerance);
+ method public void updatePath(androidx.compose.ui.graphics.Path path, optional @FloatRange(from=0.0) float tolerance);
}
public final class PathHitTesterKt {
- method public static androidx.compose.ui.graphics.PathHitTester PathHitTester(androidx.compose.ui.graphics.Path path, optional float tolerance);
+ method public static androidx.compose.ui.graphics.PathHitTester PathHitTester(androidx.compose.ui.graphics.Path path, optional @FloatRange(from=0.0) float tolerance);
}
public interface PathIterator extends java.util.Iterator<androidx.compose.ui.graphics.PathSegment> kotlin.jvm.internal.markers.KMappedMarker {
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index fb5ad0c..6d5ec8b 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -804,11 +804,11 @@
public final class PathHitTester {
ctor public PathHitTester();
method public operator boolean contains(long position);
- method public void updatePath(androidx.compose.ui.graphics.Path path, optional float tolerance);
+ method public void updatePath(androidx.compose.ui.graphics.Path path, optional @FloatRange(from=0.0) float tolerance);
}
public final class PathHitTesterKt {
- method public static androidx.compose.ui.graphics.PathHitTester PathHitTester(androidx.compose.ui.graphics.Path path, optional float tolerance);
+ method public static androidx.compose.ui.graphics.PathHitTester PathHitTester(androidx.compose.ui.graphics.Path path, optional @FloatRange(from=0.0) float tolerance);
}
public interface PathIterator extends java.util.Iterator<androidx.compose.ui.graphics.PathSegment> kotlin.jvm.internal.markers.KMappedMarker {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 29a7c76..873cedd 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -84,10 +84,10 @@
* | A | Alpha | 10 bits | `[0..1023]` |
* | | Color space | 6 bits | `[0..63]` |
* | [SRGB][ColorSpaces.Srgb] color space |
+ * | A | Alpha | 8 bits | `[0..255]` |
* | R | Red | 8 bits | `[0..255]` |
* | G | Green | 8 bits | `[0..255]` |
* | B | Blue | 8 bits | `[0..255]` |
- * | A | Alpha | 8 bits | `[0..255]` |
* | X | Unused | 32 bits | `[0]` |
* | [XYZ][ColorSpace.Model.Xyz] color model |
* | X | X | 16 bits | `[-65504.0, 65504.0]` |
@@ -102,7 +102,7 @@
* | A | Alpha | 10 bits | `[0..1023]` |
* | | Color space | 6 bits | `[0..63]` |
* ```
- * The components in this table are listed in encoding order (see below),
+ * The components in this table are listed in encoding order,
* which is why color longs in the RGB model are called RGBA colors (even if
* this doesn't quite hold for the special case of sRGB colors).
*
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathHitTester.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathHitTester.kt
index a380113..96df70f 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathHitTester.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathHitTester.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.graphics
+import androidx.annotation.FloatRange
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -36,13 +37,16 @@
* instance if the path is defined in pixels, 0.5 (half a pixel) or 1.0 (a pixel) are
* appropriate tolerances. If the path is normalized and defined in the domain 0..1,
* the caller should choose a more appropriate tolerance close to or equal to one
- * "query unit".
+ * "query unit". The tolerance must be >= 0.
*
* @param path The [Path] to run queries against.
* @param tolerance When [path] contains conic curves, defines the maximum distance between
* the original conic curve and its quadratic approximations. Set to 0.5 by default.
*/
-fun PathHitTester(path: Path, tolerance: Float = 0.5f) = PathHitTester().apply {
+fun PathHitTester(
+ path: Path,
+ @FloatRange(from = 0.0) tolerance: Float = 0.5f
+) = PathHitTester().apply {
updatePath(path, tolerance)
}
@@ -79,13 +83,13 @@
* For instance if the path is defined in pixels, 0.5 (half a pixel) or 1.0 (a pixel)
* are appropriate tolerances. If the path is normalized and defined in the domain 0..1,
* the caller should choose a more appropriate tolerance close to or equal to one
- * "query unit".
+ * "query unit". The tolerance must be >= 0.
*
* @param path The [Path] to run queries against.
* @param tolerance When [path] contains conic curves, defines the maximum distance between
* the original conic curve and its quadratic approximations. Set to 0.5 by default.
*/
- fun updatePath(path: Path, tolerance: Float = 0.5f) {
+ fun updatePath(path: Path, @FloatRange(from = 0.0) tolerance: Float = 0.5f) {
this.path = path
this.tolerance = tolerance
bounds = path.getBounds()
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 355ba52..d7a30f9 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -3201,6 +3201,16 @@
}
+package androidx.compose.ui.scrollcapture {
+
+ public final class ScrollCapture_androidKt {
+ method @Deprecated public static boolean getComposeFeatureFlag_LongScreenshotsEnabled();
+ method @Deprecated public static void setComposeFeatureFlag_LongScreenshotsEnabled(boolean);
+ property @Deprecated public static final boolean ComposeFeatureFlag_LongScreenshotsEnabled;
+ }
+
+}
+
package androidx.compose.ui.semantics {
public final class AccessibilityAction<T extends kotlin.Function<? extends java.lang.Boolean>> {
@@ -3319,6 +3329,7 @@
method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPerformImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getRequestFocus();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> getScrollBy();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function2<androidx.compose.ui.geometry.Offset,kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,java.lang.Object?>> getScrollByOffset();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> getScrollToIndex();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Boolean>>> getSetProgress();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function3<java.lang.Integer,java.lang.Integer,java.lang.Boolean,java.lang.Boolean>>> getSetSelection();
@@ -3346,6 +3357,7 @@
property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PerformImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> RequestFocus;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> ScrollBy;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function2<androidx.compose.ui.geometry.Offset,kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,java.lang.Object?>> ScrollByOffset;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> ScrollToIndex;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Boolean>>> SetProgress;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function3<java.lang.Integer,java.lang.Integer,java.lang.Boolean,java.lang.Boolean>>> SetSelection;
@@ -3571,6 +3583,7 @@
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void requestFocus(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean>? action);
+ method public static void scrollByOffset(androidx.compose.ui.semantics.SemanticsPropertyReceiver, kotlin.jvm.functions.Function2<? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,?> action);
method public static void scrollToIndex(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> action);
method public static void selectableGroup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void setCollectionInfo(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.CollectionInfo);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index ab80884..8abe50e 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -3261,6 +3261,16 @@
}
+package androidx.compose.ui.scrollcapture {
+
+ public final class ScrollCapture_androidKt {
+ method @Deprecated public static boolean getComposeFeatureFlag_LongScreenshotsEnabled();
+ method @Deprecated public static void setComposeFeatureFlag_LongScreenshotsEnabled(boolean);
+ property @Deprecated public static final boolean ComposeFeatureFlag_LongScreenshotsEnabled;
+ }
+
+}
+
package androidx.compose.ui.semantics {
public final class AccessibilityAction<T extends kotlin.Function<? extends java.lang.Boolean>> {
@@ -3379,6 +3389,7 @@
method @Deprecated public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPerformImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getRequestFocus();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> getScrollBy();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function2<androidx.compose.ui.geometry.Offset,kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,java.lang.Object?>> getScrollByOffset();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> getScrollToIndex();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Boolean>>> getSetProgress();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function3<java.lang.Integer,java.lang.Integer,java.lang.Boolean,java.lang.Boolean>>> getSetSelection();
@@ -3406,6 +3417,7 @@
property @Deprecated public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PerformImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> RequestFocus;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> ScrollBy;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.jvm.functions.Function2<androidx.compose.ui.geometry.Offset,kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,java.lang.Object?>> ScrollByOffset;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> ScrollToIndex;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Boolean>>> SetProgress;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function3<java.lang.Integer,java.lang.Integer,java.lang.Boolean,java.lang.Boolean>>> SetSelection;
@@ -3631,6 +3643,7 @@
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void requestFocus(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean>? action);
+ method public static void scrollByOffset(androidx.compose.ui.semantics.SemanticsPropertyReceiver, kotlin.jvm.functions.Function2<? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super androidx.compose.ui.geometry.Offset>,?> action);
method public static void scrollToIndex(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> action);
method public static void selectableGroup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void setCollectionInfo(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.CollectionInfo);
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index 1dbcbf7..7aa50f4 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -17,6 +17,7 @@
# R8 to complain about them not being there during optimization.
-dontwarn android.view.RenderNode
-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
-keepclassmembers class androidx.compose.ui.platform.ViewLayerContainer {
protected void dispatchGetDisplayList();
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 94119f3..b20ed84 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -58,6 +58,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.OpenComposeView
+import androidx.compose.ui.background
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.scale
@@ -65,6 +66,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.PointerCoords
import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
@@ -2267,6 +2269,1050 @@
}
/*
+ * Tests TOUCH events are triggered correctly when dynamically adding a NON-pointer input
+ * modifier above an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier is not re-executed after the
+ * dynamic one is added.
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically adds NON-pointer input modifier (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicNonInputModifier_addsAboveExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicModifierExecuted by mutableStateOf(false)
+
+ // Non-Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledModifier(enable: Boolean) = if (enable) {
+ dynamicModifierExecuted = true
+ background(Color.Green)
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledModifier(enableDynamicPointerInput)
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // DOWN (original + dynamically added modifiers)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // There are no pointer input modifiers added above this pointer modifier, so the
+ // same one is used.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(2)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered correctly when dynamically adding a pointer input modifier
+ * ABOVE an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier **IS** re-executed after the
+ * dynamic pointer input modifier is added above it.
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier above existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithKey_addsAboveExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(pointerEventLambda) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // Important Note: Even though we reset all the pointer input blocks, the initial lambda is
+ // lazily executed, meaning it won't reset the values until the first event comes in, so
+ // the previously set values are still the same until an event comes in.
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+ }
+
+ // DOWN (original + dynamically added modifiers)
+ // Now an event comes in, so the lambdas are both executed completely (dynamic one for the
+ // first time and the existing one for a second time [since it was moved]).
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // While the original pointer input block is being reused after a new one is added, it
+ // is reset (since things have changed with the Modifiers), so the entire block is
+ // executed again to allow devs to reset their gesture detectors for the new Modifier
+ // chain changes.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered incorrectly when dynamically adding a pointer input modifier
+ * (which uses Unit for its key [bad]) ABOVE an existing pointer input modifier. This is more
+ * of an "education test" for developers to see how things can go wrong if you use "Unit" for
+ * your key in pointer input and pointer input modifiers are later added dynamically.
+ *
+ * Note: Even though we are dynamically adding a new pointer input modifier above the existing
+ * pointer input modifier, Compose actually reuses the existing pointer input modifier to
+ * contain the new pointer input modifier. It then adds a new pointer input modifier below that
+ * one and copies in the original (non-dynamic) pointer input modifier into that. However, in
+ * this case, because we are using the "Unit" for both keys, Compose thinks they are the same
+ * pointer input modifier, so it never replaces the existing lambda with the dynamic pointer
+ * input modifier node's lambda. This is why you should not use Unit for your key.
+ *
+ * Why can't the lambdas passed into pointer input be compared? We can't memoize them because
+ * they are outside of a Compose scope (defined in a Modifier extension function), so
+ * developers need to pass a unique key(s) as a way to let us know when to update the lambda.
+ * You can do that with a unique key for each pointer input modifier and/or take it a step
+ * further and use captured values in the lambda as keys (ones that change lambda
+ * behavior).
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier above existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithUnitKey_addsAboveExistingModifier_failsToTriggerNewModifier() {
+ // --> Arrange
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(Unit) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ .pointerInput(Unit) {
+ ++originalPointerInputScopeExecutionCount
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // Important Note: I'm not resetting the variable counters in this test.
+
+ // DOWN (original + dynamically added modifiers)
+ // Now an event comes in, so the lambdas are both executed completely for the first time.
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // While the original pointer input block is being reused after a new one is added, it
+ // is reset (since things have changed with the Modifiers), so the entire block is
+ // executed again to allow devs to reset their gesture detectors for the new Modifier
+ // chain changes.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ // This is 2 because the dynamic modifier added before the existing one, is using Unit
+ // for the key, so the comparison shows that it doesn't need to update the lambda...
+ // Thus, it uses the old lambda (why it is very important you don't use Unit for your
+ // key.
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(3)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(3)
+ assertThat(preexistingModifierRelease).isEqualTo(3)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered correctly when dynamically adding a pointer input
+ * modifier BELOW an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier is NOT re-executed after the
+ * dynamic one is added below it (since it doesn't impact it).
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier below existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithKey_addsBelowExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(pointerEventLambda) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // DOWN (original + dynamically added modifiers)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // Because the new pointer input modifier is added below the existing one, the existing
+ // one doesn't change.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(2)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
* Tests a full mouse event cycle from a press and release.
*
* Important Note: The pointer id should stay the same throughout all these events (part of the
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt
new file mode 100644
index 0000000..88b2c1c
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.scrollcapture
+
+import android.graphics.Bitmap
+import android.graphics.Rect
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.assertColor
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.core.graphics.applyCanvas
+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.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 31)
+class ScrollCaptureDrawTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val captureTester = ScrollCaptureTester(rule)
+
+ @Before
+ fun setUp() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = true
+ }
+
+ @After
+ fun tearDown() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+ }
+
+ @Test
+ fun capture_drawsScrollContents_withCaptureHeight1px() {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 1)
+ assertThat(bitmaps).hasSize(27)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_drawsScrollContents_withCaptureHeightFullViewport() {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ assertThat(bitmaps).hasSize(3)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_resetsScrollPosition_from0() {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ bitmaps.forEach { it.recycle() }
+ rule.runOnIdle {
+ assertThat(scrollState.value).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun capture_resetsScrollPosition_fromNonZero() {
+ val scrollState = ScrollState(5)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ bitmaps.forEach { it.recycle() }
+ rule.runOnIdle {
+ assertThat(scrollState.value).isEqualTo(5)
+ }
+ }
+
+ @Composable
+ private fun TestContent(scrollState: ScrollState) {
+ with(LocalDensity.current) {
+ Column(
+ Modifier
+ .size(10.toDp())
+ .verticalScroll(scrollState)
+ ) {
+ Box(
+ Modifier
+ .background(Color.Red)
+ .height(9.toDp())
+ .fillMaxWidth()
+ )
+ Box(
+ Modifier
+ .background(Color.Blue)
+ .height(9.toDp())
+ .fillMaxWidth()
+ )
+ Box(
+ Modifier
+ .background(Color.Green)
+ .height(9.toDp())
+ .fillMaxWidth()
+ )
+ }
+ }
+ }
+
+ private inline fun Bitmap.use(block: (Bitmap) -> Unit) {
+ try {
+ block(this)
+ } finally {
+ recycle()
+ }
+ }
+
+ private fun Iterable<Bitmap>.joinVerticallyToBitmap(): Bitmap {
+ val width = maxOf { it.width }
+ val height = sumOf { it.height }
+ val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ var y = 0
+ try {
+ result.applyCanvas {
+ forEach {
+ drawBitmap(it, 0f, y.toFloat(), null)
+ y += it.height
+ }
+ }
+ } finally {
+ forEach { it.recycle() }
+ }
+ return result
+ }
+
+ private fun Bitmap.assertRect(rect: Rect, color: Color) {
+ for (x in rect.left until rect.right) {
+ for (y in rect.top until rect.bottom) {
+ assertColor(color, x, y)
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt
new file mode 100644
index 0000000..c29828b
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.scrollcapture
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createComposeRule
+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.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests real Foundation scrollable components' integration with scroll capture.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 31)
+class ScrollCaptureIntegrationTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val captureTester = ScrollCaptureTester(rule)
+
+ @Before
+ fun setUp() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = true
+ }
+
+ @After
+ fun tearDown() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+ }
+
+ @Test
+ fun search_findsVerticalScrollModifier() {
+ captureTester.setContent {
+ with(LocalDensity.current) {
+ Box(
+ Modifier
+ .size(10.toDp())
+ .verticalScroll(rememberScrollState())
+ ) {
+ Box(Modifier.size(100.toDp()))
+ }
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect.width()).isEqualTo(10)
+ assertThat(target.localVisibleRect.height()).isEqualTo(10)
+ }
+
+ @Test
+ fun search_findsLazyColumn() {
+ captureTester.setContent {
+ with(LocalDensity.current) {
+ LazyColumn(Modifier.size(10.toDp())) {
+ item {
+ Box(Modifier.size(100.toDp()))
+ }
+ }
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect.width()).isEqualTo(10)
+ assertThat(target.localVisibleRect.height()).isEqualTo(10)
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
new file mode 100644
index 0000000..bb4d86f
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.scrollcapture
+
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.view.ScrollCaptureSession
+import android.view.Surface
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.scrollByOffset
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+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 kotlin.math.roundToInt
+import kotlin.test.fail
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.selects.onTimeout
+import kotlinx.coroutines.selects.select
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Tests the scroll capture implementation's integration with semantics. Tests in this class should
+ * not use any scrollable components from Foundation.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 31)
+class ScrollCaptureTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val captureTester = ScrollCaptureTester(rule)
+
+ @Before
+ fun setUp() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = true
+ }
+
+ @After
+ fun tearDown() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+ }
+
+ @Test
+ fun search_findsScrollableTarget() {
+ lateinit var coordinates: LayoutCoordinates
+ captureTester.setContent {
+ TestVerticalScrollable(
+ size = 10,
+ maxValue = 1f,
+ modifier = Modifier.onPlaced { coordinates = it }
+ )
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.hint).isEqualTo(0)
+ assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 10, 10))
+ assertThat(target.positionInWindow)
+ .isEqualTo(coordinates.positionInWindow().roundToPoint())
+ assertThat(target.scrollBounds).isEqualTo(Rect(0, 0, 10, 10))
+ }
+
+ @Test
+ fun search_usesTargetsCoordinates() {
+ lateinit var coordinates: LayoutCoordinates
+ val padding = 15
+ captureTester.setContent {
+ Box(Modifier
+ .onPlaced { coordinates = it }
+ .padding(with(LocalDensity.current) { padding.toDp() })
+ ) {
+ TestVerticalScrollable(
+ size = 10,
+ maxValue = 1f,
+ )
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ // Relative to the View, i.e. the root of the composition.
+ assertThat(target.localVisibleRect)
+ .isEqualTo(Rect(padding, padding, padding + 10, padding + 10))
+ assertThat(target.positionInWindow)
+ .isEqualTo(
+ (coordinates.positionInWindow() +
+ Offset(padding.toFloat(), padding.toFloat())
+ ).roundToPoint()
+ )
+ assertThat(target.scrollBounds)
+ .isEqualTo(Rect(padding, padding, padding + 10, padding + 10))
+ }
+
+ @Test
+ fun search_findsLargestTarget_whenMultipleMatches() {
+ val smallerSize = 10
+ val largerSize = 11
+ captureTester.setContent {
+ Column {
+ TestVerticalScrollable(size = smallerSize)
+ TestVerticalScrollable(size = largerSize)
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect)
+ .isEqualTo(Rect(0, smallerSize, largerSize, smallerSize + largerSize))
+ }
+
+ @Test
+ fun search_findsDeepestTarget() {
+ captureTester.setContent {
+ TestVerticalScrollable(size = 11) {
+ TestVerticalScrollable(size = 10)
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 10, 10))
+ }
+
+ @Test
+ fun search_findsDeepestTarget_whenLargerParentSibling() {
+ captureTester.setContent {
+ Column {
+ TestVerticalScrollable(size = 10) {
+ TestVerticalScrollable(size = 9)
+ }
+ TestVerticalScrollable(size = 11)
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 9, 9))
+ }
+
+ @Test
+ fun search_findsDeepestLargestTarget_whenMultipleMatches() {
+ captureTester.setContent {
+ Column {
+ TestVerticalScrollable(size = 10) {
+ TestVerticalScrollable(size = 9)
+ }
+ TestVerticalScrollable(size = 10) {
+ TestVerticalScrollable(size = 8)
+ }
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 9, 9))
+ }
+
+ @Test
+ fun search_usesClippedSize() {
+ captureTester.setContent {
+ TestVerticalScrollable(size = 10) {
+ TestVerticalScrollable(size = 100)
+ }
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).hasSize(1)
+ val target = targets.single()
+ assertThat(target.localVisibleRect).isEqualTo(Rect(0, 0, 10, 10))
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenFeatureFlagDisabled() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+
+ captureTester.setContent {
+ TestVerticalScrollable()
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenInvisibleToUser() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+
+ captureTester.setContent {
+ TestVerticalScrollable(Modifier.semantics {
+ invisibleToUser()
+ })
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenZeroSize() {
+ @Suppress("DEPRECATION")
+ ComposeFeatureFlag_LongScreenshotsEnabled = false
+
+ captureTester.setContent {
+ TestVerticalScrollable(Modifier.size(0.dp))
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenZeroMaxValue() {
+ captureTester.setContent {
+ TestVerticalScrollable(maxValue = 0f)
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenNoScrollAxisRange() {
+ captureTester.setContent {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ scrollByOffset { Offset.Zero }
+ }
+ )
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenNoVerticalScrollAxisRange() {
+ captureTester.setContent {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ scrollByOffset { Offset.Zero }
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ )
+ }
+ )
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun search_doesNotFindTarget_whenNoScrollByImmediately() {
+ captureTester.setContent {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ verticalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ )
+ }
+ )
+ }
+
+ val targets = captureTester.findCaptureTargets()
+ assertThat(targets).isEmpty()
+ }
+
+ @Test
+ fun callbackOnSearch_returnsViewportBounds() = runTest {
+ lateinit var coordinates: LayoutCoordinates
+ val padding = 15
+ captureTester.setContent {
+ Box(Modifier
+ .onPlaced { coordinates = it }
+ .padding(with(LocalDensity.current) { padding.toDp() })
+ ) {
+ TestVerticalScrollable(
+ size = 10,
+ maxValue = 1f,
+ )
+ }
+ }
+
+ val callback = captureTester.findCaptureTargets().single().callback
+
+ launch {
+ val result = callback.onScrollCaptureSearch()
+
+ // Search result is in window coordinates.
+ assertThat(result).isEqualTo(
+ Rect(
+ coordinates.positionInWindow().x.roundToInt() + padding,
+ coordinates.positionInWindow().y.roundToInt() + padding,
+ coordinates.positionInWindow().x.roundToInt() + padding + 10,
+ coordinates.positionInWindow().y.roundToInt() + padding + 10
+ )
+ )
+ }
+ }
+
+ // TODO this is flaky, figure out why
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun callbackOnImageCapture_scrollsBackwardsThenForwards() = runTest {
+ data class ScrollRequest(
+ val requestedOffset: Offset,
+ val consumeScroll: (Offset) -> Unit
+ )
+
+ val scrollRequests = Channel<ScrollRequest>(capacity = Channel.RENDEZVOUS)
+ suspend fun expectScrollRequest(expectedOffset: Offset, consume: Offset = expectedOffset) {
+ val request = select {
+ scrollRequests.onReceive { it }
+ onTimeout(1000) { fail("No scroll request received after 1000ms") }
+ }
+ assertThat(request.requestedOffset).isEqualTo(expectedOffset)
+ request.consumeScroll(consume)
+ // Allow the scroll request to be consumed.
+ rule.awaitIdle()
+ }
+
+ suspend fun expectNoScrollRequests() {
+ rule.awaitIdle()
+ if (!scrollRequests.isEmpty) {
+ val requests = buildList {
+ do {
+ val request = scrollRequests.tryReceive()
+ request.getOrNull()?.let(::add)
+ } while (request.isSuccess)
+ }
+ fail("Expected no scroll requests, but had ${requests.size}: " +
+ requests.joinToString { it.requestedOffset.toString() })
+ }
+ }
+
+ val size = 10
+ captureTester.setContent {
+ TestVerticalScrollable(
+ size = size,
+ onScrollByImmediately = { offset ->
+ val result = CompletableDeferred<Offset>(parent = currentCoroutineContext().job)
+ scrollRequests.send(ScrollRequest(offset, consumeScroll = result::complete))
+ result.await()
+ }
+ )
+ }
+
+ val callback = captureTester.findCaptureTargets().single().callback
+ val canvas = mock<Canvas>()
+ val surface = mock<Surface> {
+ on(it.lockHardwareCanvas()).thenReturn(canvas)
+ }
+ val session = mock<ScrollCaptureSession> {
+ on(it.surface).thenReturn(surface)
+ }
+
+ launch {
+ callback.onScrollCaptureStart(session)
+
+ // First request is at origin, no scrolling required.
+ async { callback.onScrollCaptureImageRequest(session, Rect(0, 0, 10, 10)) }
+ .let { captureResult ->
+ expectNoScrollRequests()
+ assertThat(captureResult.await()).isEqualTo(Rect(0, 0, 10, 10))
+ }
+
+ // Back one half-page, but only respond to part of it.
+ async { callback.onScrollCaptureImageRequest(session, Rect(0, -5, 10, 0)) }
+ .let { captureResult ->
+ expectScrollRequest(Offset(0f, -5f), consume = Offset(0f, -4f))
+ assertThat(captureResult.await()).isEqualTo(Rect(0, -4, 10, 0))
+ }
+
+ // Forward one half-page – already in viewport, no scrolling required.
+ async { callback.onScrollCaptureImageRequest(session, Rect(0, 0, 10, 5)) }
+ .let { captureResult ->
+ expectNoScrollRequests()
+ assertThat(captureResult.await()).isEqualTo(Rect(0, 0, 10, 5))
+ }
+
+ // Forward another half-page. This time we need to scroll.
+ async { callback.onScrollCaptureImageRequest(session, Rect(0, 5, 10, 10)) }
+ .let { captureResult ->
+ expectScrollRequest(Offset(0f, 4f))
+ assertThat(captureResult.await()).isEqualTo(Rect(0, 5, 10, 10))
+ }
+
+ // Forward another half-page, scroll again so now we're past the original viewport.
+ async { callback.onScrollCaptureImageRequest(session, Rect(0, 10, 10, 15)) }
+ .let { captureResult ->
+ expectScrollRequest(Offset(0f, 5f))
+ assertThat(captureResult.await()).isEqualTo(Rect(0, 10, 10, 15))
+ }
+
+ launch { callback.onScrollCaptureEnd() }
+ // One last scroll request to reset to original offset.
+ expectScrollRequest(Offset(0f, -5f))
+ expectNoScrollRequests()
+ }
+ }
+
+ /**
+ * A component that publishes all the right semantics to be considered a scrollable.
+ */
+ @Composable
+ private fun TestVerticalScrollable(
+ modifier: Modifier = Modifier,
+ size: Int = 10,
+ maxValue: Float = 1f,
+ onScrollByImmediately: suspend (Offset) -> Offset = { Offset.Zero },
+ content: (@Composable () -> Unit)? = null
+ ) {
+ with(LocalDensity.current) {
+ val updatedMaxValue by rememberUpdatedState(maxValue)
+ val scrollAxisRange = remember {
+ ScrollAxisRange(
+ value = { 0f },
+ maxValue = { updatedMaxValue },
+ )
+ }
+ Box(
+ modifier
+ .size(size.toDp())
+ .semantics {
+ verticalScrollAxisRange = scrollAxisRange
+ scrollByOffset(onScrollByImmediately)
+ },
+ content = { content?.invoke() }
+ )
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt
new file mode 100644
index 0000000..8c75dc6
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.scrollcapture
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.ColorSpace
+import android.graphics.PixelFormat
+import android.graphics.Point
+import android.graphics.Rect
+import android.hardware.HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
+import android.hardware.HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+import android.media.Image
+import android.media.ImageReader
+import android.os.CancellationSignal
+import android.os.Handler
+import android.os.Looper
+import android.view.ScrollCaptureCallback
+import android.view.ScrollCaptureSession
+import android.view.ScrollCaptureTarget
+import android.view.Surface
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.internal.requirePrecondition
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import kotlin.coroutines.resume
+import kotlin.math.roundToInt
+import kotlin.test.fail
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.selects.onTimeout
+import kotlinx.coroutines.selects.select
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Helps tests pretend to be the Android platform performing scroll capture search and image
+ * capture. Tests must call [setContent] on this class instead of on [rule].
+ */
+@RequiresApi(31)
+class ScrollCaptureTester(private val rule: ComposeContentTestRule) {
+ private var view: View? = null
+ private var coroutineScope: CoroutineScope? = null
+
+ fun setContent(content: @Composable () -> Unit) {
+ rule.setContent {
+ this.view = LocalView.current
+ this.coroutineScope = rememberCoroutineScope()
+ content()
+ }
+ }
+
+ /**
+ * Calls [View.onScrollCaptureSearch] on the Compose host view, which searches the composition
+ * from [setContent] for scroll containers, and returns all the [ScrollCaptureTarget]s produced
+ * that would be given to the platform in production.
+ */
+ fun findCaptureTargets(): List<ScrollCaptureTarget> = rule.runOnIdle {
+ val view = checkNotNull(view as? AndroidComposeView) {
+ "Must call setContent on ScrollCaptureTester before capturing."
+ }
+ val localVisibleRect = Rect().also(view::getLocalVisibleRect)
+ val windowOffset = view.calculatePositionInWindow(Offset.Zero).roundToPoint()
+ val targets = mutableListOf<ScrollCaptureTarget>()
+ view.onScrollCaptureSearch(localVisibleRect, windowOffset, targets::add)
+ targets
+ }
+
+ /**
+ * Emulates (roughly) how the platform interacts with [ScrollCaptureCallback] to iteratively
+ * assemble a screenshot of the entire contents of the [target]. Unlike the platform, this
+ * method will not limit itself to a certain size, it always captures the entire scroll
+ * contents, so tests should make sure to use small enough scroll contents or the test might
+ * run out of memory.
+ *
+ * @param captureHeight The height of the capture window. Must not be greater than viewport
+ * height.
+ */
+ fun captureBitmapsVertically(target: ScrollCaptureTarget, captureHeight: Int): List<Bitmap> {
+ val scope = rule.runOnIdle {
+ checkNotNull(coroutineScope) {
+ "Must call setContent on ScrollCaptureTest before capturing."
+ }
+ }
+ val bitmapsFromTop = mutableListOf<Bitmap>()
+
+ // This coroutine will run on the main thread, no need to use runOnUiThread.
+ val captureJob = scope.launch {
+ runCaptureSession(target, captureHeight, onBitmap = bitmapsFromTop::add)
+ }
+
+ rule.waitUntil(3_000) { captureJob.isCompleted }
+ return bitmapsFromTop
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend fun runCaptureSession(
+ target: ScrollCaptureTarget,
+ captureHeight: Int,
+ onBitmap: (Bitmap) -> Unit
+ ) {
+ val callback = target.callback
+ // Use the bounds returned from the callback, not the ones from the target, because that's
+ // what the system does.
+ val scrollBounds = callback.onScrollCaptureSearch()
+ val captureWidth = scrollBounds.width()
+ requirePrecondition(captureHeight <= scrollBounds.height()) {
+ "Expected windowSize ($captureHeight) ≤ viewport height (${scrollBounds.height()})"
+ }
+
+ withSurfaceBitmaps(captureWidth, captureHeight) { surface, bitmapsFromSurface ->
+ val session = ScrollCaptureSession(
+ surface,
+ scrollBounds,
+ target.positionInWindow
+ )
+ callback.onScrollCaptureStart(session)
+
+ var captureOffset = Point(0, 0)
+ var goingUp = true
+ // Starting with the original viewport, scrolls all the way to the top, then all the way
+ // back down, capturing images on the way down until it hits the bottom.
+ while (true) {
+ val requestedCaptureArea = Rect(
+ captureOffset.x,
+ captureOffset.y,
+ captureOffset.x + captureWidth,
+ captureOffset.y + captureHeight
+ )
+ val resultCaptureArea =
+ callback.onScrollCaptureImageRequest(session, requestedCaptureArea)
+
+ // Empty results shouldn't produce an image.
+ if (!resultCaptureArea.isEmpty) {
+ val bitmap = bitmapsFromSurface.receiveWithTimeout(1_000) {
+ "No bitmap received after 1 second for capture area $resultCaptureArea"
+ }
+
+ // Only collect the returned images on the way down.
+ if (!goingUp) {
+ onBitmap(bitmap)
+ } else {
+ bitmap.recycle()
+ }
+ }
+
+ if (resultCaptureArea != requestedCaptureArea) {
+ // We found the top or bottom.
+ if (goingUp) {
+ // "Bounce" off the top: Change direction and start re-capturing down.
+ goingUp = false
+ captureOffset = Point(0, resultCaptureArea.top)
+ } else {
+ // If we hit the bottom then we're done.
+ break
+ }
+ } else {
+ // We can keep going in the same direction, offset the capture window and loop.
+ captureOffset = if (goingUp) {
+ Point(0, resultCaptureArea.top - captureHeight)
+ } else {
+ Point(0, resultCaptureArea.bottom)
+ }
+ }
+ }
+ }
+
+ callback.onScrollCaptureEnd()
+ }
+
+ /**
+ * Creates a [Surface] passes it to [block] along with a channel that will receive all images
+ * written to the [Surface].
+ */
+ private suspend inline fun withSurfaceBitmaps(
+ width: Int,
+ height: Int,
+ crossinline block: suspend (Surface, ReceiveChannel<Bitmap>) -> Unit
+ ) {
+ coroutineScope {
+ // ImageReader gives us the Surface that we'll provide to the session.
+ ImageReader.newInstance(
+ width,
+ height,
+ PixelFormat.RGBA_8888,
+ // Each image is read, processed, and closed before the next request to draw is made,
+ // so we don't need multiple images.
+ /* maxImages= */ 1,
+ USAGE_GPU_SAMPLED_IMAGE or USAGE_GPU_COLOR_OUTPUT
+ ).use { imageReader ->
+ val bitmapsChannel = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
+
+ // Must register the OnImageAvailableListener before any code in block runs to avoid
+ // race conditions.
+ val imageCollectorJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ imageReader.collectImages {
+ val bitmap = it.toSoftwareBitmap()
+ bitmapsChannel.send(bitmap)
+ }
+ }
+
+ try {
+ block(imageReader.surface, bitmapsChannel)
+ // ImageReader has no signal that it's finished, so in the happy path we have to
+ // stop the collector job explicitly.
+ imageCollectorJob.cancel()
+ } finally {
+ bitmapsChannel.close()
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads all images from this [ImageReader] and passes them to [onImage]. The [Image] will
+ * automatically be closed when [onImage] returns.
+ *
+ * Propagates backpressure to the [ImageReader] – only one image will be acquired from the
+ * [ImageReader] at a time, and the next image won't be acquired until [onImage] returns.
+ */
+ private suspend inline fun ImageReader.collectImages(onImage: (Image) -> Unit): Nothing {
+ val imageAvailableChannel = Channel<Unit>(capacity = Channel.CONFLATED)
+ setOnImageAvailableListener(
+ { imageAvailableChannel.trySend(Unit) },
+ Handler(Looper.getMainLooper())
+ )
+ val context = currentCoroutineContext()
+
+ try {
+ // Read all images until cancelled.
+ while (true) {
+ context.ensureActive()
+ // Fast path – if an image is immediately available, don't suspend.
+ var image: Image? = acquireNextImage()
+ // If no image was available, suspend until the callback fires.
+ while (image == null) {
+ imageAvailableChannel.receive()
+ image = acquireNextImage()
+ }
+ image.use { onImage(image) }
+ }
+ } finally {
+ setOnImageAvailableListener(null, null)
+ }
+ }
+
+ /**
+ * Helper function for converting an [Image] to a [Bitmap] by copying the hardware buffer into
+ * a software bitmap.
+ */
+ private fun Image.toSoftwareBitmap(): Bitmap {
+ val hardwareBuffer = checkPreconditionNotNull(hardwareBuffer) { "No hardware buffer" }
+ hardwareBuffer.use {
+ val hardwareBitmap = Bitmap.wrapHardwareBuffer(
+ hardwareBuffer,
+ ColorSpace.get(ColorSpace.Named.SRGB)
+ ) ?: error("wrapHardwareBuffer returned null")
+ try {
+ return hardwareBitmap.copy(ARGB_8888, false)
+ } finally {
+ hardwareBitmap.recycle()
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend inline fun <E> ReceiveChannel<E>.receiveWithTimeout(
+ timeoutMillis: Long,
+ crossinline timeoutMessage: () -> String
+ ): E = select {
+ onReceive { it }
+ onTimeout(timeoutMillis) { fail(timeoutMessage()) }
+ }
+}
+
+/**
+ * Helper for calling [ScrollCaptureCallback.onScrollCaptureSearch] from a suspend function.
+ * The [CancellationSignal] and continuation callback are generated from the coroutine.
+ */
+@RequiresApi(31)
+suspend fun ScrollCaptureCallback.onScrollCaptureSearch(): Rect =
+ suspendCancellableCoroutine { continuation ->
+ onScrollCaptureSearch(continuation.createCancellationSignal()) {
+ continuation.resume(it)
+ }
+ }
+
+/**
+ * Helper for calling [ScrollCaptureCallback.onScrollCaptureStart] from a suspend function.
+ * The [CancellationSignal] and continuation callback are generated from the coroutine.
+ */
+@RequiresApi(31)
+suspend fun ScrollCaptureCallback.onScrollCaptureStart(session: ScrollCaptureSession) {
+ suspendCancellableCoroutine { continuation ->
+ onScrollCaptureStart(session, continuation.createCancellationSignal()) {
+ continuation.resume(Unit)
+ }
+ }
+}
+
+/**
+ * Helper for calling [ScrollCaptureCallback.onScrollCaptureImageRequest] from a suspend function.
+ * The [CancellationSignal] and continuation callback are generated from the coroutine.
+ */
+@RequiresApi(31)
+suspend fun ScrollCaptureCallback.onScrollCaptureImageRequest(
+ session: ScrollCaptureSession,
+ captureArea: Rect
+): Rect = suspendCancellableCoroutine { continuation ->
+ onScrollCaptureImageRequest(
+ session,
+ continuation.createCancellationSignal(),
+ captureArea
+ ) {
+ continuation.resume(it)
+ }
+}
+
+/**
+ * Helper for calling [ScrollCaptureCallback.onScrollCaptureEnd] from a suspend function.
+ * The [CancellationSignal] and continuation callback are generated from the coroutine.
+ */
+@RequiresApi(31)
+suspend fun ScrollCaptureCallback.onScrollCaptureEnd() {
+ suspendCancellableCoroutine { continuation ->
+ onScrollCaptureEnd {
+ continuation.resume(Unit)
+ }
+ }
+}
+
+fun Offset.roundToPoint(): Point = Point(x.roundToInt(), y.roundToInt())
+
+/**
+ * Creates a [CancellationSignal] and wires up cancellation bidirectionally to the coroutine's
+ * job: cancelling either one will automatically cancel the other.
+ */
+private fun CancellableContinuation<*>.createCancellationSignal(): CancellationSignal {
+ val signal = CancellationSignal()
+ signal.setOnCancelListener(this::cancel)
+ invokeOnCancellation { signal.cancel() }
+ return signal
+}
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 17c75d2..b618150 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
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.res.Configuration
+import android.graphics.Point
import android.graphics.Rect
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
@@ -46,6 +47,7 @@
import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.TOOL_TYPE_MOUSE
+import android.view.ScrollCaptureTarget
import android.view.View
import android.view.ViewGroup
import android.view.ViewStructure
@@ -171,6 +173,7 @@
import androidx.compose.ui.platform.MotionEventVerifierApi29.isValidMotionEvent
import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
import androidx.compose.ui.platform.coreshims.ViewCompatShims
+import androidx.compose.ui.scrollcapture.ScrollCapture
import androidx.compose.ui.semantics.EmptySemanticsElement
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.findClosestParentNode
@@ -793,6 +796,21 @@
} ?: super.getFocusedRect(rect)
}
+ override fun onScrollCaptureSearch(
+ localVisibleRect: Rect,
+ windowOffset: Point,
+ targets: Consumer<ScrollCaptureTarget>
+ ) {
+ if (SDK_INT >= 31) {
+ ScrollCapture.onScrollCaptureSearch(
+ view = this,
+ semanticsOwner = semanticsOwner,
+ coroutineContext = coroutineContext,
+ targets = targets
+ )
+ }
+ }
+
override fun onResume(owner: LifecycleOwner) {
// Refresh in onResume in case the value has changed.
showLayoutBounds = getIsShowingLayoutBounds()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ComposeScrollCaptureCallback.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ComposeScrollCaptureCallback.android.kt
new file mode 100644
index 0000000..f7e23b3
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ComposeScrollCaptureCallback.android.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.scrollcapture
+
+import android.graphics.BlendMode
+import android.graphics.Canvas as AndroidCanvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect as AndroidRect
+import android.os.CancellationSignal
+import android.view.ScrollCaptureCallback
+import android.view.ScrollCaptureSession
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toComposeIntRect
+import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.semantics.SemanticsActions.ScrollByOffset
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.unit.IntRect
+import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.math.roundToInt
+import kotlin.random.Random
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.launch
+
+private const val DEBUG = false
+
+/**
+ * Implementation of [ScrollCaptureCallback] that captures Compose scroll containers.
+ *
+ * This callback interacts with the scroll container via semantics, namely [ScrollByOffset],
+ * and supports any container that publishes that action – whether the size of the scroll contents
+ * are known or not (e.g. `LazyColumn`). Pixels are captured by drawing the node directly after each
+ * scroll operation.
+ */
+@RequiresApi(31)
+internal class ComposeScrollCaptureCallback(
+ private val node: SemanticsNode,
+ private val viewportBoundsInWindow: IntRect,
+ private val coroutineScope: CoroutineScope,
+) : ScrollCaptureCallback {
+ private val scrollTracker = RelativeScroller(
+ viewportSize = viewportBoundsInWindow.height,
+ scrollBy = { amount ->
+ val scrollByOffset = checkPreconditionNotNull(node.scrollCaptureScrollByAction)
+ // This action may animate, ensure any calls to this RelativeScroll are done with a
+ // coroutine context that disables animations.
+ val consumed = scrollByOffset(Offset(0f, amount))
+ consumed.y
+ }
+ )
+
+ override fun onScrollCaptureSearch(signal: CancellationSignal, onReady: Consumer<AndroidRect>) {
+ val bounds = viewportBoundsInWindow
+ onReady.accept(bounds.toAndroidRect())
+ }
+
+ override fun onScrollCaptureStart(
+ session: ScrollCaptureSession,
+ signal: CancellationSignal,
+ onReady: Runnable
+ ) {
+ scrollTracker.reset()
+ // TODO(b/329296635) Notify target when capture session starts.
+ onReady.run()
+ }
+
+ override fun onScrollCaptureImageRequest(
+ session: ScrollCaptureSession,
+ signal: CancellationSignal,
+ captureArea: AndroidRect,
+ onComplete: Consumer<AndroidRect>
+ ) {
+ coroutineScope.launchWithCancellationSignal(
+ signal = signal,
+ // Don't animate scrollByOffset calls.
+ context = DisableAnimationMotionDurationScale
+ ) {
+ val result = onScrollCaptureImageRequest(session, captureArea.toComposeIntRect())
+ onComplete.accept(result.toAndroidRect())
+ }
+ }
+
+ private suspend fun onScrollCaptureImageRequest(
+ session: ScrollCaptureSession,
+ captureArea: IntRect,
+ ): IntRect {
+ // Scroll the requested capture area into the viewport so we can draw it.
+ val targetMin = captureArea.top
+ val targetMax = captureArea.bottom
+ scrollTracker.scrollRangeIntoView(targetMin, targetMax)
+
+ // Wait a frame to allow layout to respond to the scroll.
+ withFrameNanos {}
+
+ // Calculate the viewport-relative coordinates of the capture area, clipped to
+ // the viewport.
+ val viewportClippedMin = scrollTracker.mapOffsetToViewport(targetMin)
+ val viewportClippedMax = scrollTracker.mapOffsetToViewport(targetMax)
+ val viewportClippedRect = captureArea.copy(
+ top = viewportClippedMin,
+ bottom = viewportClippedMax
+ )
+
+ if (viewportClippedMin == viewportClippedMax) {
+ // Requested capture area is outside the bounds of scrollable content,
+ // nothing to capture.
+ return IntRect.Zero
+ }
+
+ // Draw a single frame of the content to a buffer that we can stamp out.
+ val coordinator = checkNotNull(node.findCoordinatorToGetBounds()) {
+ "Could not find coordinator for semantics node."
+ }
+
+ val androidCanvas = session.surface.lockHardwareCanvas()
+ try {
+ // Clear any pixels left over from a previous request.
+ androidCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR)
+
+ if (DEBUG) {
+ androidCanvas.drawDebugBackground()
+ }
+
+ val canvas = Canvas(androidCanvas)
+ canvas.translate(
+ dx = -viewportClippedRect.left.toFloat(),
+ dy = -viewportClippedRect.top.toFloat()
+ )
+ coordinator.draw(canvas, graphicsLayer = null)
+
+ if (DEBUG) {
+ canvas.translate(
+ dx = viewportClippedRect.left.toFloat(),
+ dy = viewportClippedRect.top.toFloat(),
+ )
+ androidCanvas.drawDebugOverlay()
+ }
+ } finally {
+ session.surface.unlockCanvasAndPost(androidCanvas)
+ }
+
+ // Translate back to "original" coordinates to report.
+ val resultRect = viewportClippedRect.translate(0, scrollTracker.scrollAmount.roundToInt())
+ return resultRect
+ }
+
+ override fun onScrollCaptureEnd(onReady: Runnable) {
+ coroutineScope.launch(NonCancellable) {
+ scrollTracker.scrollTo(0f)
+ // TODO(b/329296635) Notify target when capture session ends.
+ onReady.run()
+ }
+ }
+
+ private fun AndroidCanvas.drawDebugBackground() {
+ drawColor(
+ androidx.compose.ui.graphics.Color.hsl(
+ hue = Random.nextFloat() * 360f,
+ saturation = 0.75f,
+ lightness = 0.5f,
+ alpha = 1f
+ ).toArgb()
+ )
+ }
+
+ private fun AndroidCanvas.drawDebugOverlay() {
+ val circleRadius = 20f
+ val circlePaint = Paint().apply {
+ color = Color.RED
+ }
+ drawCircle(0f, 0f, circleRadius, circlePaint)
+ drawCircle(width.toFloat(), 0f, circleRadius, circlePaint)
+ drawCircle(
+ width.toFloat(),
+ height.toFloat(),
+ circleRadius,
+ circlePaint
+ )
+ drawCircle(0f, height.toFloat(), circleRadius, circlePaint)
+ }
+}
+
+private fun CoroutineScope.launchWithCancellationSignal(
+ signal: CancellationSignal,
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit
+): Job {
+ val job = launch(context = context, block = block)
+ job.invokeOnCompletion { cause ->
+ if (cause != null) {
+ signal.cancel()
+ }
+ }
+ signal.setOnCancelListener {
+ job.cancel()
+ }
+ return job
+}
+
+/**
+ * Helper class for scrolling to specific offsets relative to an original scroll position and
+ * mapping those offsets to the current viewport coordinates.
+ */
+private class RelativeScroller(
+ private val viewportSize: Int,
+ private val scrollBy: suspend (Float) -> Float
+) {
+ var scrollAmount = 0f
+ private set
+
+ fun reset() {
+ scrollAmount = 0f
+ }
+
+ /**
+ * Scrolls so that the range ([min], [max]) is in the viewport. The range must fit inside the
+ * viewport.
+ */
+ suspend fun scrollRangeIntoView(min: Int, max: Int) {
+ require(min <= max) { "Expected min=$min ≤ max=$max" }
+ require(max - min <= viewportSize) {
+ "Expected range (${max - min}) to be ≤ viewportSize=$viewportSize"
+ }
+
+ if (min >= scrollAmount && max <= scrollAmount + viewportSize) {
+ // Already visible, no need to scroll.
+ return
+ }
+
+ // Scroll to the nearest edge.
+ val target = if (min < scrollAmount) min else max - viewportSize
+ scrollTo(target.toFloat())
+ }
+
+ /**
+ * Given [offset] relative to the original scroll position, maps it to the current offset in the
+ * viewport. Values are clamped to the viewport.
+ *
+ * This is an identity map for values inside the viewport before any scrolling has been done
+ * after calling `scrollTo(0f)`.
+ */
+ fun mapOffsetToViewport(offset: Int): Int {
+ return (offset - scrollAmount.roundToInt()).coerceIn(0, viewportSize)
+ }
+
+ /**
+ * Try to scroll to [offset] pixels past the original scroll position.
+ */
+ suspend fun scrollTo(offset: Float): Float = scrollBy(offset - scrollAmount)
+
+ private suspend fun scrollBy(delta: Float): Float {
+ val consumed = scrollBy.invoke(delta)
+ scrollAmount += consumed
+ return consumed
+ }
+}
+
+private object DisableAnimationMotionDurationScale : MotionDurationScale {
+ override val scaleFactor: Float
+ get() = 0f
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt
new file mode 100644
index 0000000..f4ba501
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.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.ui.scrollcapture
+
+import android.graphics.Point
+import android.view.ScrollCaptureCallback
+import android.view.ScrollCaptureTarget
+import android.view.View
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.platform.isVisible
+import androidx.compose.ui.semantics.SemanticsActions.ScrollByOffset
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsProperties.Disabled
+import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.roundToIntRect
+import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Temporary feature flag for long screenshots support in Compose scrollables. This property will
+ * eventually be removed.
+ *
+ * Long screenshot support is currently off by default. To enable it, set this flag to true.
+ * A future release will set it to true by default.
+ */
+@Deprecated("Temporary feature flag. See b/329128246")
+@get:Deprecated("Temporary feature flag. See b/329128246")
+@set:Deprecated("Temporary feature flag. See b/329128246")
+// TODO(b/329128246) Remove before 1.7
+var ComposeFeatureFlag_LongScreenshotsEnabled by mutableStateOf(false)
+
+/**
+ * Separate class to host the implementation of scroll capture for dex verification.
+ */
+@RequiresApi(31)
+internal object ScrollCapture {
+ /**
+ * Implements scroll capture (long screenshots) support for a composition. Finds a single
+ * [ScrollCaptureTarget] to propose to the platform. Searches over the semantics tree to find
+ * nodes that publish vertical scroll semantics (namely [ScrollByOffset] and
+ * [VerticalScrollAxisRange]) and then uses logic similar to how the platform searches [View]
+ * targets to select the deepest, largest scroll container. If a target is found, an
+ * implementation of [ScrollCaptureCallback] is created for it (see
+ * [ComposeScrollCaptureCallback]) and given to the platform.
+ *
+ * The platform currently only supports scroll capture for containers that scroll vertically.
+ * The API supports horizontal as well, but it's not used. To keep this code simpler and avoid
+ * having dead code, we only implement vertical scroll capture as well.
+ *
+ * See go/compose-long-screenshots for more background.
+ */
+ // Required not to be inlined for class verification.
+ @DoNotInline
+ fun onScrollCaptureSearch(
+ view: View,
+ semanticsOwner: SemanticsOwner,
+ coroutineContext: CoroutineContext,
+ targets: Consumer<ScrollCaptureTarget>
+ ) {
+ @Suppress("DEPRECATION")
+ if (!ComposeFeatureFlag_LongScreenshotsEnabled) return
+
+ // Search the semantics tree for scroll containers.
+ val candidates = mutableVectorOf<ScrollCaptureCandidate>()
+ visitScrollCaptureCandidates(
+ fromNode = semanticsOwner.unmergedRootSemanticsNode,
+ onCandidate = candidates::add
+ )
+
+ // Sort to find the deepest node with the biggest bounds in the dimension(s) that the node
+ // supports scrolling in.
+ candidates.sortWith(compareBy(
+ { it.depth },
+ { it.viewportBoundsInWindow.height },
+ ))
+ val candidate = candidates.lastOrNull() ?: return
+
+ // If we found a candidate, create a capture callback for it and give it to the system.
+ val coroutineScope = CoroutineScope(coroutineContext)
+ val callback = ComposeScrollCaptureCallback(
+ node = candidate.node,
+ viewportBoundsInWindow = candidate.viewportBoundsInWindow,
+ coroutineScope = coroutineScope,
+ )
+ val localVisibleRectOfCandidate = candidate.coordinates.boundsInRoot()
+ val windowOffsetOfCandidate = candidate.viewportBoundsInWindow.topLeft
+ targets.accept(
+ ScrollCaptureTarget(
+ view,
+ localVisibleRectOfCandidate.roundToIntRect().toAndroidRect(),
+ windowOffsetOfCandidate.let { Point(it.x, it.y) },
+ callback
+ ).apply {
+ scrollBounds = candidate.viewportBoundsInWindow.toAndroidRect()
+ }
+ )
+ }
+}
+
+/**
+ * Walks the tree of [SemanticsNode]s rooted at [fromNode] to find nodes that look scrollable and
+ * calculate their nesting depth.
+ */
+private fun visitScrollCaptureCandidates(
+ fromNode: SemanticsNode,
+ depth: Int = 0,
+ onCandidate: (ScrollCaptureCandidate) -> Unit
+) {
+ fromNode.visitDescendants { node ->
+ // Invisible/disabled nodes can't be candidates, nor can any of their descendants.
+ if (!node.isVisible || Disabled in node.config) {
+ return@visitDescendants false
+ }
+
+ val nodeCoordinates = checkPreconditionNotNull(node.findCoordinatorToGetBounds()) {
+ "Expected semantics node to have a coordinator."
+ }.coordinates
+
+ // Zero-sized nodes can't be candidates, and by definition would clip all their children so
+ // they and their descendants can't be candidates either.
+ val viewportBoundsInWindow = nodeCoordinates.boundsInWindow().roundToIntRect()
+ if (viewportBoundsInWindow.isEmpty) {
+ return@visitDescendants false
+ }
+
+ // If the node is visible, we need to check if it's scrollable.
+ // TODO(b/329295945) Support explicit opt-in/-out.
+ // Don't care about horizontal scroll containers.
+ if (!node.canScrollVertically) {
+ // Not a scrollable, so can't be a candidate, but its descendants might be.
+ return@visitDescendants true
+ }
+
+ // We found a node that looks scrollable! Report it, then visit its children with an
+ // incremented depth counter.
+ val candidateDepth = depth + 1
+ onCandidate(
+ ScrollCaptureCandidate(
+ node = node,
+ depth = candidateDepth,
+ viewportBoundsInWindow = viewportBoundsInWindow,
+ coordinates = nodeCoordinates,
+ )
+ )
+ visitScrollCaptureCandidates(
+ fromNode = node,
+ depth = candidateDepth,
+ onCandidate = onCandidate
+ )
+ // We've just visited descendants ourselves, don't need this visit call to do it.
+ return@visitDescendants false
+ }
+}
+
+internal val SemanticsNode.scrollCaptureScrollByAction get() = config.getOrNull(ScrollByOffset)
+
+private val SemanticsNode.canScrollVertically: Boolean
+ get() {
+ val scrollByOffset = scrollCaptureScrollByAction
+ val verticalScrollAxisRange = config.getOrNull(VerticalScrollAxisRange)
+ return scrollByOffset != null &&
+ verticalScrollAxisRange != null &&
+ verticalScrollAxisRange.maxValue() > 0f
+ }
+
+/**
+ * Visits all the descendants of this [SemanticsNode].
+ *
+ * @param onNode Function called for each [SemanticsNode]. Iff this function returns true, the
+ * children of the current node will be visited.
+ */
+private inline fun SemanticsNode.visitDescendants(onNode: (SemanticsNode) -> Boolean) {
+ val nodes = mutableVectorOf<SemanticsNode>()
+ nodes.addAll(getChildrenForSearch())
+ while (nodes.isNotEmpty()) {
+ val node = nodes.removeAt(nodes.lastIndex)
+ val visitChildren = onNode(node)
+ if (visitChildren) {
+ nodes.addAll(node.getChildrenForSearch())
+ }
+ }
+}
+
+private fun SemanticsNode.getChildrenForSearch() = getChildren(
+ includeDeactivatedNodes = false,
+ includeReplacedSemantics = false,
+ includeFakeNodes = false
+)
+
+/**
+ * Information about a potential [ScrollCaptureTarget] needed to both select the final candidate and
+ * create its [ComposeScrollCaptureCallback].
+ */
+private class ScrollCaptureCandidate(
+ val node: SemanticsNode,
+ val depth: Int,
+ val viewportBoundsInWindow: IntRect,
+ val coordinates: LayoutCoordinates,
+) {
+ override fun toString(): String =
+ "ScrollCaptureCandidate(node=$node, " +
+ "depth=$depth, " +
+ "viewportBoundsInWindow=$viewportBoundsInWindow, " +
+ "coordinates=$coordinates)"
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 7f7220d..f1938fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Immutable
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
@@ -302,6 +303,11 @@
val ScrollBy = ActionPropertyKey<(x: Float, y: Float) -> Boolean>("ScrollBy")
/**
+ * @see SemanticsPropertyReceiver.scrollByOffset
+ */
+ val ScrollByOffset = SemanticsPropertyKey<suspend (offset: Offset) -> Offset>("ScrollByOffset")
+
+ /**
* @see SemanticsPropertyReceiver.scrollToIndex
*/
val ScrollToIndex = ActionPropertyKey<(Int) -> Boolean>("ScrollToIndex")
@@ -1244,12 +1250,15 @@
}
/**
- * Action to scroll by a specified amount.
+ * Action to asynchronously scroll by a specified amount.
*
- * Expected to be used in conjunction with verticalScrollAxisRange/horizontalScrollAxisRange.
+ * [scrollByOffset] should be preferred in most cases, since it is synchronous and returns the
+ * amount of scroll that was actually consumed.
+ *
+ * Expected to be used in conjunction with [verticalScrollAxisRange]/[horizontalScrollAxisRange].
*
* @param label Optional label for this action.
- * @param action Action to be performed when the [SemanticsActions.ScrollBy] is called.
+ * @param action Action to be performed when [SemanticsActions.ScrollBy] is called.
*/
fun SemanticsPropertyReceiver.scrollBy(
label: String? = null,
@@ -1259,6 +1268,23 @@
}
/**
+ * Action to scroll by a specified amount and return how much of the offset was actually consumed.
+ * E.g. if the node can't scroll at all in the given direction, [Offset.Zero] should be returned.
+ * The action should not return until the scroll operation has finished.
+ *
+ * Expected to be used in conjunction with [verticalScrollAxisRange]/[horizontalScrollAxisRange].
+ *
+ * Unlike [scrollBy], this action is synchronous, and returns the amount of scroll consumed.
+ *
+ * @param action Action to be performed when [SemanticsActions.ScrollByOffset] is called.
+ */
+fun SemanticsPropertyReceiver.scrollByOffset(
+ action: suspend (offset: Offset) -> Offset
+) {
+ this[SemanticsActions.ScrollByOffset] = action
+}
+
+/**
* Action to scroll a container to the index of one of its items.
*
* The [action] should throw an [IllegalArgumentException] if the index is out of bounds.
diff --git a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
index 244b776..e8e1bbf3 100644
--- a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
@@ -18,11 +18,16 @@
import android.Manifest;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
import android.os.CancellationSignal;
import android.os.Handler;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
@@ -34,53 +39,62 @@
/**
* A class that coordinates access to the fingerprint hardware.
* <p>
- * This class has been deprecated and should no longer be used. On all platform versions, it behaves
- * as though no fingerprint hardware is available.
+ * On platforms before {@link android.os.Build.VERSION_CODES#M}, this class behaves as there would
+ * be no fingerprint hardware available.
*
- * @deprecated {@code FingerprintManager} was removed from the platform SDK in Android V, use
- * {@code androidx.biometrics.BiometricPrompt} instead.
+ * @deprecated Use {@code androidx.biometrics.BiometricPrompt} instead.
*/
@SuppressWarnings({"deprecation", "unused"})
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class FingerprintManagerCompat {
+ private final Context mContext;
+
/** Get a {@link FingerprintManagerCompat} instance for a provided context. */
@NonNull
public static FingerprintManagerCompat from(@NonNull Context context) {
- return new FingerprintManagerCompat();
+ return new FingerprintManagerCompat(context);
}
- private FingerprintManagerCompat() {
+ private FingerprintManagerCompat(Context context) {
+ mContext = context;
}
/**
- * Prior to deprecation, this method would determine if there is at least one fingerprint
- * enrolled.
+ * Determine if there is at least one fingerprint enrolled.
*
- * @return false
+ * @return true if at least one fingerprint is enrolled, false otherwise
*/
@RequiresPermission(Manifest.permission.USE_FINGERPRINT)
public boolean hasEnrolledFingerprints() {
- return false;
+ if (Build.VERSION.SDK_INT >= 23) {
+ final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
+ return (fp != null) && Api23Impl.hasEnrolledFingerprints(fp);
+ } else {
+ return false;
+ }
}
/**
- * Prior to deprecation, this method would determine if fingerprint hardware is present and
- * functional.
+ * Determine if fingerprint hardware is present and functional.
*
- * @return false
+ * @return true if hardware is present and functional, false otherwise.
*/
@RequiresPermission(Manifest.permission.USE_FINGERPRINT)
public boolean isHardwareDetected() {
- return false;
+ if (Build.VERSION.SDK_INT >= 23) {
+ final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
+ return (fp != null) && Api23Impl.isHardwareDetected(fp);
+ } else {
+ return false;
+ }
}
/**
- * Prior to deprecation, this method would request authentication of a crypto object.
- * <p>
- * This call warms up the fingerprint hardware and starts scanning for a fingerprint. It
- * terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+ * Request authentication of a crypto object. This call warms up the fingerprint hardware
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
* {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
* which point the object is no longer valid. The operation can be canceled by using the
* provided cancel object.
@@ -100,14 +114,15 @@
@Nullable androidx.core.os.CancellationSignal cancel,
@NonNull AuthenticationCallback callback,
@Nullable Handler handler) {
- // No-op.
+ authenticate(crypto, flags,
+ cancel != null ? (CancellationSignal) cancel.getCancellationSignalObject() : null,
+ callback, handler);
}
/**
- * Prior to deprecation, this method would request authentication of a crypto object.
- * <p>
- * This call warms up the fingerprint hardware and starts scanning for a fingerprint.
- * It terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+ * Request authentication of a crypto object. This call warms up the fingerprint hardware
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
* {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
* which point the object is no longer valid. The operation can be canceled by using the
* provided cancel object.
@@ -122,7 +137,56 @@
public void authenticate(@Nullable CryptoObject crypto, int flags,
@Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
@Nullable Handler handler) {
- // No-op.
+ if (Build.VERSION.SDK_INT >= 23) {
+ final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
+ if (fp != null) {
+ Api23Impl.authenticate(fp, wrapCryptoObject(crypto), cancel, flags,
+ wrapCallback(callback), handler);
+ }
+ }
+ }
+
+ @Nullable
+ @RequiresApi(23)
+ private static FingerprintManager getFingerprintManagerOrNull(@NonNull Context context) {
+ return Api23Impl.getFingerprintManagerOrNull(context);
+ }
+
+ @RequiresApi(23)
+ private static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
+ return Api23Impl.wrapCryptoObject(cryptoObject);
+ }
+
+ @RequiresApi(23)
+ static CryptoObject unwrapCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
+ return Api23Impl.unwrapCryptoObject(cryptoObject);
+ }
+
+ @RequiresApi(23)
+ private static FingerprintManager.AuthenticationCallback wrapCallback(
+ final AuthenticationCallback callback) {
+ return new FingerprintManager.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(int errMsgId, CharSequence errString) {
+ callback.onAuthenticationError(errMsgId, errString);
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+ callback.onAuthenticationHelp(helpMsgId, helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+ callback.onAuthenticationSucceeded(new AuthenticationResult(
+ unwrapCryptoObject(Api23Impl.getCryptoObject(result))));
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ callback.onAuthenticationFailed();
+ }
+ };
}
/**
@@ -232,4 +296,82 @@
*/
public void onAuthenticationFailed() { }
}
+
+ @RequiresApi(23)
+ static class Api23Impl {
+ private Api23Impl() {
+ // This class is not instantiable.
+ }
+
+ @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
+ @DoNotInline
+ static boolean hasEnrolledFingerprints(Object fingerprintManager) {
+ return ((FingerprintManager) fingerprintManager).hasEnrolledFingerprints();
+ }
+
+ @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
+ @DoNotInline
+ static boolean isHardwareDetected(Object fingerprintManager) {
+ return ((FingerprintManager) fingerprintManager).isHardwareDetected();
+ }
+
+ @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
+ @DoNotInline
+ static void authenticate(Object fingerprintManager, Object crypto,
+ CancellationSignal cancel, int flags, Object callback, Handler handler) {
+ ((FingerprintManager) fingerprintManager).authenticate(
+ (FingerprintManager.CryptoObject) crypto, cancel, flags,
+ (FingerprintManager.AuthenticationCallback) callback, handler);
+ }
+
+ @DoNotInline
+ static FingerprintManager.CryptoObject getCryptoObject(Object authenticationResult) {
+ return ((FingerprintManager.AuthenticationResult) authenticationResult)
+ .getCryptoObject();
+ }
+
+ @DoNotInline
+ public static FingerprintManager getFingerprintManagerOrNull(Context context) {
+ if (Build.VERSION.SDK_INT == 23) {
+ return context.getSystemService(FingerprintManager.class);
+ } else if (Build.VERSION.SDK_INT > 23 && context.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ return context.getSystemService(FingerprintManager.class);
+ } else {
+ return null;
+ }
+ }
+
+ @DoNotInline
+ public static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
+ if (cryptoObject == null) {
+ return null;
+ } else if (cryptoObject.getCipher() != null) {
+ return new FingerprintManager.CryptoObject(cryptoObject.getCipher());
+ } else if (cryptoObject.getSignature() != null) {
+ return new FingerprintManager.CryptoObject(cryptoObject.getSignature());
+ } else if (cryptoObject.getMac() != null) {
+ return new FingerprintManager.CryptoObject(cryptoObject.getMac());
+ } else {
+ return null;
+ }
+ }
+
+ @DoNotInline
+ public static CryptoObject unwrapCryptoObject(Object cryptoObjectObj) {
+ FingerprintManager.CryptoObject cryptoObject =
+ (FingerprintManager.CryptoObject) cryptoObjectObj;
+ if (cryptoObject == null) {
+ return null;
+ } else if (cryptoObject.getCipher() != null) {
+ return new CryptoObject(cryptoObject.getCipher());
+ } else if (cryptoObject.getSignature() != null) {
+ return new CryptoObject(cryptoObject.getSignature());
+ } else if (cryptoObject.getMac() != null) {
+ return new CryptoObject(cryptoObject.getMac());
+ } else {
+ return null;
+ }
+ }
+ }
}
diff --git a/development/bench-flame-diff/bench-flame-diff.sh b/development/bench-flame-diff/bench-flame-diff.sh
index 66a6e40..4f75303 100755
--- a/development/bench-flame-diff/bench-flame-diff.sh
+++ b/development/bench-flame-diff/bench-flame-diff.sh
@@ -1,3 +1,11 @@
#!/usr/bin/env bash
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)"
+pushd "$script_dir" >/dev/null
+
./gradlew --quiet installDist && ./app/build/install/bench-flame-diff/bin/bench-flame-diff "$@"
+exit_code=$?
+
+popd >/dev/null
+
+exit $exit_code
diff --git a/development/build_log_simplifier/build_log_simplifier.py b/development/build_log_simplifier/build_log_simplifier.py
index 974acd6..db1503b 100755
--- a/development/build_log_simplifier/build_log_simplifier.py
+++ b/development/build_log_simplifier/build_log_simplifier.py
@@ -204,6 +204,11 @@
prev_blank = False
return result
+def remove_trailing_blank_lines(lines):
+ while len(lines) > 0 and lines[-1].strip() == "":
+ del lines[-1]
+ return lines
+
def extract_task_name(line):
prefix = "> Task "
if line.startswith(prefix):
@@ -522,6 +527,7 @@
interesting_lines = remove_by_regexes(interesting_lines, exemption_regexes, validate)
interesting_lines = collapse_tasks_having_no_output(interesting_lines)
interesting_lines = collapse_consecutive_blank_lines(interesting_lines)
+ interesting_lines = remove_trailing_blank_lines(interesting_lines)
# process results
if update:
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index 21a9218..7eaea26 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -122,7 +122,15 @@
> Run with \-\-info or \-\-debug option to get more log output\.
> Run with \-\-scan to get full insights\.
# developers already see this message when local builds fail and don't need to also see it in CI
-\* Get more help at https://help\.gradle\.org
+> Get more help at https://help\.gradle\.org.
+# developers already can tell when a build failed
+FAILURE: Build failed with an exception.
+# developers already expect the output to explain what went wrong
+\* What went wrong:
+# the compilation log is already displayed in the output
+.*> Compilation error. See log for more details
+# In practice, these failures are compilation failures
+> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers\$GradleKotlinCompilerWorkAction
# Flaky printout from kapt
WARNING: Illegal reflective access by org\.jetbrains\.kotlin\.kapt3\.util\.ModuleManipulationUtilsKt .* to constructor com\.sun\.tools\.javac\.util\.Context\(\)
WARNING: Please consider reporting this to the maintainers of org\.jetbrains\.kotlin\.kapt3\.util\.ModuleManipulationUtilsKt
diff --git a/gradle.properties b/gradle.properties
index 0204323..0c3fb8f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -45,7 +45,7 @@
androidx.includeOptionalProjects=false
# Keep ComposeCompiler pinned unless performing Kotlin upgrade & ComposeCompiler release
-androidx.unpinComposeCompiler=true
+androidx.unpinComposeCompiler=false
# Disable features we do not use
android.defaults.buildfeatures.aidl=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7e79e78..b56a64b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,7 +28,7 @@
byteBuddy = "1.14.9"
asm = "9.3"
cmake = "3.22.1"
-composeCompilerPlugin = "1.5.9" # Update when pulling in new stable binaries
+composeCompilerPlugin = "1.5.11" # Update when pulling in new stable binaries
dagger = "2.49"
dexmaker = "2.28.3"
dokka = "1.8.20-dev-214"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a85faf4..3dc311e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -339,6 +339,7 @@
<trusted-key id="A4FD709CC4B0515F2E6AF04E218FA0F6A941A037" group="com.github.kevinstern"/>
<trusted-key id="A5B2DDE7843E7CA3E8CAABD02383163BC40844FD" group="org.reactivestreams"/>
<trusted-key id="A5BD02B93E7A40482EB1D66A5F69AD087600B22C" group="org.ow2.asm"/>
+ <trusted-key id="A5F483CD733A4EBAEA378B2AE88979FB9B30ACF2" group="^androidx\..*" regex="true"/>
<trusted-key id="A6D6C97108B8585F91B158748671A8DF71296252" group="^com[.]squareup($|([.].*))" regex="true"/>
<trusted-key id="A7892505CF1A58076453E52D7999BEFBA1039E8B" group="net.bytebuddy"/>
<trusted-key id="AA417737BD805456DB3CBDDE6601E5C08DCCBB96" group="info.picocli" name="picocli"/>
diff --git a/gradlew b/gradlew
index 28d0634..04379b2 100755
--- a/gradlew
+++ b/gradlew
@@ -243,11 +243,6 @@
disableCi=false
fi
-# workaround for https://github.com/gradle/gradle/issues/18386
-if [[ " ${@} " =~ " --profile " ]]; then
- mkdir -p reports
-fi
-
# Expand some arguments
for compact in "--ci" "--strict" "--clean" "--no-ci"; do
expanded=""
@@ -259,7 +254,8 @@
-Pandroidx.enableAffectedModuleDetection\
-Pandroidx.printTimestamps\
--no-watch-fs\
- -Pandroidx.highMemory"
+ -Pandroidx.highMemory\
+ --profile"
fi
fi
if [ "$compact" == "--strict" ]; then
@@ -302,6 +298,11 @@
fi
done
+# workaround for https://github.com/gradle/gradle/issues/18386
+if [[ " ${@} " =~ " --profile " ]]; then
+ mkdir -p reports
+fi
+
raiseMemory=false
if [[ " ${@} " =~ " -Pandroidx.highMemory " ]]; then
raiseMemory=true
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index b13f7bd..57247c7 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -238,6 +238,10 @@
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLOORS_TOTAL;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> GOLF_SHOT_COUNT;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> GOLF_SHOT_COUNT_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> GROUND_CONTACT_BALANCE;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> GROUND_CONTACT_BALANCE_STATS;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> GROUND_CONTACT_TIME;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> GROUND_CONTACT_TIME_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> HEART_RATE_BPM;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> HEART_RATE_BPM_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> INCLINE_DISTANCE;
@@ -260,10 +264,16 @@
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> STEPS_PER_MINUTE;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> STEPS_PER_MINUTE_STATS;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> STEPS_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> STRIDE_LENGTH;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> STRIDE_LENGTH_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT_TOTAL;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_STROKES;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_STROKES_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VERTICAL_OSCILLATION;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VERTICAL_OSCILLATION_STATS;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VERTICAL_RATIO;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VERTICAL_RATIO_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VO2_MAX;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VO2_MAX_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> WALKING_STEPS;
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index d3191b4..359c428 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -238,6 +238,10 @@
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLOORS_TOTAL;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> GOLF_SHOT_COUNT;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> GOLF_SHOT_COUNT_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> GROUND_CONTACT_BALANCE;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> GROUND_CONTACT_BALANCE_STATS;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> GROUND_CONTACT_TIME;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> GROUND_CONTACT_TIME_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> HEART_RATE_BPM;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> HEART_RATE_BPM_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> INCLINE_DISTANCE;
@@ -260,10 +264,16 @@
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> STEPS_PER_MINUTE;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> STEPS_PER_MINUTE_STATS;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> STEPS_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> STRIDE_LENGTH;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> STRIDE_LENGTH_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT_TOTAL;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_STROKES;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_STROKES_TOTAL;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VERTICAL_OSCILLATION;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VERTICAL_OSCILLATION_STATS;
+ field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VERTICAL_RATIO;
+ field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VERTICAL_RATIO_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VO2_MAX;
field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VO2_MAX_STATS;
field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> WALKING_STEPS;
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataType.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataType.kt
index c646fe6..40bde13 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataType.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataType.kt
@@ -597,6 +597,86 @@
createCumulativeDataType("Rep Count")
/**
+ * The amount of time during a single step that the runner's foot was in contact with the
+ * ground in milliseconds in `long` format.
+ */
+ @JvmField
+ val GROUND_CONTACT_TIME: DeltaDataType<Long, SampleDataPoint<Long>> =
+ createSampleDataType("Ground Contact Time")
+
+ /**
+ * Statistics on the amount of time during a single step that the runner's foot was in
+ * contact with the ground in milliseconds in `long` format.
+ */
+ @JvmField
+ val GROUND_CONTACT_TIME_STATS: AggregateDataType<Long, StatisticalDataPoint<Long>> =
+ createStatsDataType("Ground Contact Time")
+
+ /**
+ * Percentage of time the right foot is on the ground in percentage in `double` format.
+ *
+ * Percentage value is from 0 to 100.0. For instance, 52.0 means the right foot is on the
+ * ground 52% of the time.
+ */
+ @JvmField
+ val GROUND_CONTACT_BALANCE: DeltaDataType<Double, SampleDataPoint<Double>> =
+ createSampleDataType("Ground Contact Balance")
+
+ /**
+ * Statistics on percentage of time the right foot is on the ground in percentage in
+ * `double` format.
+ *
+ * Percentage value is from 0 to 100.0. For instance, 52.0 means the right foot is on the
+ * round 52% of the time.
+ */
+ @JvmField
+ val GROUND_CONTACT_BALANCE_STATS:
+ AggregateDataType<Double, StatisticalDataPoint<Double>> =
+ createStatsDataType("Ground Contact Balance")
+
+ /**
+ * Distance the center of mass moves up-and-down with each step in centimeters in `double`
+ * format.
+ */
+ @JvmField
+ val VERTICAL_OSCILLATION: DeltaDataType<Double, SampleDataPoint<Double>> =
+ createSampleDataType("Vertical Oscillation")
+
+ /**
+ * Statistic on distance the center of mass moves up-and-down with each step in centimeters
+ * in `double` format.
+ */
+ @JvmField
+ val VERTICAL_OSCILLATION_STATS: AggregateDataType<Double, StatisticalDataPoint<Double>> =
+ createStatsDataType("Vertical Oscillation")
+
+ /**
+ * Vertical oscillation / stride length. Divide vertical oscillation (converted to meters)
+ * by stride length (in meters) in `double` format.
+ */
+ @JvmField
+ val VERTICAL_RATIO: DeltaDataType<Double, SampleDataPoint<Double>> =
+ createSampleDataType("Vertical Ratio")
+
+ /**
+ * Statistics on vertical oscillation / stride length. Divide vertical oscillation
+ * (converted to meters) by stride length (in meters) in `double` format.
+ */
+ @JvmField
+ val VERTICAL_RATIO_STATS: AggregateDataType<Double, StatisticalDataPoint<Double>> =
+ createStatsDataType("Vertical Ratio")
+
+ /** Distance covered by a single step in meters in `double` format. */
+ @JvmField
+ val STRIDE_LENGTH: DeltaDataType<Double, SampleDataPoint<Double>> =
+ createSampleDataType("Stride Length")
+
+ /** Statistics on distance covered by a single step in meters in `double` format. */
+ @JvmField
+ val STRIDE_LENGTH_STATS: AggregateDataType<Double, StatisticalDataPoint<Double>> =
+ createStatsDataType("Stride Length")
+
+ /**
* The total step count over a day, where the previous day ends and a new day begins at
* 12:00 AM local time. Each [DataPoint] of this type will cover the interval from the start
* of day to now. In the event of time-zone shifts, the interval may be greater than 24hrs.
@@ -662,6 +742,8 @@
FLAT_GROUND_DURATION,
FLOORS,
GOLF_SHOT_COUNT,
+ GROUND_CONTACT_BALANCE,
+ GROUND_CONTACT_TIME,
HEART_RATE_BPM,
INCLINE_DISTANCE,
INCLINE_DURATION,
@@ -673,8 +755,11 @@
SPEED,
STEPS,
STEPS_PER_MINUTE,
+ STRIDE_LENGTH,
SWIMMING_LAP_COUNT,
SWIMMING_STROKES,
+ VERTICAL_OSCILLATION,
+ VERTICAL_RATIO,
VO2_MAX,
WALKING_STEPS,
)
@@ -692,6 +777,8 @@
FLAT_GROUND_DURATION_TOTAL,
FLOORS_TOTAL,
GOLF_SHOT_COUNT_TOTAL,
+ GROUND_CONTACT_BALANCE_STATS,
+ GROUND_CONTACT_TIME_STATS,
HEART_RATE_BPM_STATS,
INCLINE_DISTANCE_TOTAL,
INCLINE_DURATION_TOTAL,
@@ -702,8 +789,11 @@
SPEED_STATS,
STEPS_PER_MINUTE_STATS,
STEPS_TOTAL,
+ STRIDE_LENGTH_STATS,
SWIMMING_LAP_COUNT_TOTAL,
SWIMMING_STROKES_TOTAL,
+ VERTICAL_OSCILLATION_STATS,
+ VERTICAL_RATIO_STATS,
VO2_MAX_STATS,
WALKING_STEPS_TOTAL,
)
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/DataTypeTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/DataTypeTest.kt
index 93fe6bd..8250710 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/DataTypeTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/DataTypeTest.kt
@@ -26,9 +26,19 @@
import androidx.health.services.client.data.DataType.Companion.ELEVATION_GAIN_DAILY
import androidx.health.services.client.data.DataType.Companion.FLOORS_DAILY
import androidx.health.services.client.data.DataType.Companion.FORMAT_BYTE_ARRAY
+import androidx.health.services.client.data.DataType.Companion.GROUND_CONTACT_BALANCE
+import androidx.health.services.client.data.DataType.Companion.GROUND_CONTACT_BALANCE_STATS
+import androidx.health.services.client.data.DataType.Companion.GROUND_CONTACT_TIME
+import androidx.health.services.client.data.DataType.Companion.GROUND_CONTACT_TIME_STATS
import androidx.health.services.client.data.DataType.Companion.LOCATION
import androidx.health.services.client.data.DataType.Companion.STEPS
import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
+import androidx.health.services.client.data.DataType.Companion.STRIDE_LENGTH
+import androidx.health.services.client.data.DataType.Companion.STRIDE_LENGTH_STATS
+import androidx.health.services.client.data.DataType.Companion.VERTICAL_OSCILLATION
+import androidx.health.services.client.data.DataType.Companion.VERTICAL_OSCILLATION_STATS
+import androidx.health.services.client.data.DataType.Companion.VERTICAL_RATIO
+import androidx.health.services.client.data.DataType.Companion.VERTICAL_RATIO_STATS
import androidx.health.services.client.data.DataType.TimeType.Companion.INTERVAL
import androidx.health.services.client.data.DataType.TimeType.Companion.UNKNOWN
import androidx.health.services.client.proto.DataProto
@@ -213,6 +223,31 @@
}
@Test
+ fun aggregatesAndDeltaDataTypeValuesShouldMatch() {
+ val aggregates = DataType.aggregateDataTypes.toMutableSet().apply {
+ // Active duration is special cased and does not have a delta form. Developers get the
+ // Active duration not from a DataPoint, but instead from from a property in the
+ // ExerciseUpdate directly. The DataType is only used to enable setting an ExerciseGoal,
+ // which only operate on aggregates. So, we do not have a delta datatype for this and
+ // instead only have an aggregate.
+ remove(ACTIVE_EXERCISE_DURATION_TOTAL)
+ }.map { it.name to it.valueClass }.toMap()
+ // Certain deltas are expected to not have aggregates
+ val deltas = DataType.deltaDataTypes.toMutableSet().apply {
+ // Aggregate location doesn't make a lot of sense
+ remove(LOCATION)
+ // Dailies are used in passive and passive only deals with deltas
+ remove(CALORIES_DAILY)
+ remove(DISTANCE_DAILY)
+ remove(ELEVATION_GAIN_DAILY)
+ remove(FLOORS_DAILY)
+ remove(STEPS_DAILY)
+ }.map { it.name to it.valueClass }.toMap()
+
+ assertThat(aggregates).isEqualTo(deltas)
+ }
+
+ @Test
fun allDataTypesShouldBeInEitherDeltaOrAggregateDataTypeSets() {
// If this test fails, you haven't added a new DataType to one of the sets below:
val joinedSet = DataType.deltaDataTypes + DataType.aggregateDataTypes
@@ -231,4 +266,22 @@
assertThat(dataTypesThroughReflection).contains(ABSOLUTE_ELEVATION_STATS)
assertThat(joinedSet).containsExactlyElementsIn(dataTypesThroughReflection)
}
+
+ @Test
+ fun sampleDataTypesAreNotAggregates() {
+ assertThat(GROUND_CONTACT_BALANCE.isAggregate).isFalse()
+ assertThat(GROUND_CONTACT_TIME.isAggregate).isFalse()
+ assertThat(VERTICAL_OSCILLATION.isAggregate).isFalse()
+ assertThat(VERTICAL_RATIO.isAggregate).isFalse()
+ assertThat(STRIDE_LENGTH.isAggregate).isFalse()
+ }
+
+ @Test
+ fun statsDataTypesAreAggregates() {
+ assertThat(GROUND_CONTACT_BALANCE_STATS.isAggregate).isTrue()
+ assertThat(GROUND_CONTACT_TIME_STATS.isAggregate).isTrue()
+ assertThat(VERTICAL_OSCILLATION_STATS.isAggregate).isTrue()
+ assertThat(VERTICAL_RATIO_STATS.isAggregate).isTrue()
+ assertThat(STRIDE_LENGTH_STATS.isAggregate).isTrue()
+ }
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 5cf45d7c..c2d38b8 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
[versions]
-ACTIVITY = "1.9.0-beta01"
-ANNOTATION = "1.8.0-alpha02"
+ACTIVITY = "1.9.0-rc01"
+ANNOTATION = "1.8.0-beta01"
ANNOTATION_EXPERIMENTAL = "1.4.0-rc01"
APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
APPACTIONS_INTERACTION = "1.0.0-alpha01"
@@ -14,15 +14,15 @@
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.4.0-alpha04"
+CAMERA = "1.4.0-beta01"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
-CAMERA_VIEWFINDER = "1.4.0-alpha04"
+CAMERA_VIEWFINDER = "1.4.0-alpha05"
CAMERA_VIEWFINDER_COMPOSE = "1.0.0-alpha01"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-alpha01"
COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.7.0-alpha05"
+COMPOSE = "1.7.0-alpha06"
COMPOSE_COMPILER = "1.5.11" # Update when preparing for a release
COMPOSE_MATERIAL3 = "1.3.0-alpha03"
COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha09"
@@ -34,7 +34,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.13.0-beta01"
+CORE = "1.13.0-rc01"
CORE_ANIMATION = "1.0.0-rc01"
CORE_ANIMATION_TESTING = "1.0.0-rc01"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -63,7 +63,7 @@
EMOJI2 = "1.5.0-alpha01"
ENTERPRISE = "1.1.0-rc01"
EXIFINTERFACE = "1.4.0-alpha01"
-FRAGMENT = "1.7.0-beta01"
+FRAGMENT = "1.7.0-rc01"
FUTURES = "1.2.0-alpha03"
GLANCE = "1.1.0-alpha01"
GLANCE_PREVIEW = "1.0.0-alpha06"
@@ -92,14 +92,14 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.8.0-alpha03"
+LIFECYCLE = "2.8.0-alpha04"
LIFECYCLE_EXTENSIONS = "2.2.0"
LINT = "1.0.0-alpha01"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.7.0-rc01"
METRICS = "1.0.0-beta02"
-NAVIGATION = "2.8.0-alpha05"
+NAVIGATION = "2.8.0-alpha06"
PAGING = "3.3.0-alpha05"
PALETTE = "1.1.0-alpha01"
PDF = "1.0.0-alpha01"
@@ -145,7 +145,7 @@
TEXT = "1.0.0-alpha01"
TRACING = "1.3.0-alpha02"
TRACING_PERFETTO = "1.0.0"
-TRANSITION = "1.5.0-beta01"
+TRANSITION = "1.5.0-rc01"
TV = "1.0.0-alpha11"
TVPROVIDER = "1.1.0-alpha02"
VECTORDRAWABLE = "1.2.0-rc01"
@@ -155,7 +155,7 @@
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.1.0-beta03"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.4.0-alpha05"
+WEAR_COMPOSE = "1.4.0-alpha06"
WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha20"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
@@ -166,13 +166,13 @@
WEAR_TILES = "1.4.0-alpha01"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
WEAR_WATCHFACE = "1.3.0-alpha02"
-WEBKIT = "1.11.0-beta01"
+WEBKIT = "1.12.0-alpha01"
# Adding a comment to prevent merge conflicts for Window artifact
WINDOW = "1.3.0-beta01"
WINDOW_EXTENSIONS = "1.3.0-alpha01"
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
-WORK = "2.10.0-alpha01"
+WORK = "2.10.0-alpha02"
XR = "1.0.0-alpha01"
[groups]
@@ -203,7 +203,7 @@
COMPOSE_COMPILER = { group = "androidx.compose.compiler", atomicGroupVersion = "versions.COMPOSE_COMPILER" }
COMPOSE_DESKTOP = { group = "androidx.compose.desktop", atomicGroupVersion = "versions.COMPOSE" }
COMPOSE_FOUNDATION = { group = "androidx.compose.foundation", atomicGroupVersion = "versions.COMPOSE" }
-COMPOSE_MATERIAL = { group = "androidx.compose.material", atomicGroupVersion = "versions.COMPOSE" }
+COMPOSE_MATERIAL = { group = "androidx.compose.material"}
COMPOSE_MATERIAL3 = { group = "androidx.compose.material3", atomicGroupVersion = "versions.COMPOSE_MATERIAL3" }
COMPOSE_MATERIAL3_ADAPTIVE = { group = "androidx.compose.material3.adaptive", atomicGroupVersion = "versions.COMPOSE_MATERIAL3_ADAPTIVE" }
COMPOSE_RUNTIME = { group = "androidx.compose.runtime", atomicGroupVersion = "versions.COMPOSE" }
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 449e4b9..89417d1 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -252,6 +252,7 @@
@androidx.navigation.NavDestinationDsl public class NavDestinationBuilder<D extends androidx.navigation.NavDestination> {
ctor @Deprecated public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, @IdRes int id);
ctor public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, String? route);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, optional kotlin.reflect.KClass<?>? route, optional java.util.Map<kotlin.reflect.KType,? extends androidx.navigation.NavType<?>>? typeMap);
method @Deprecated public final void action(int actionId, kotlin.jvm.functions.Function1<? super androidx.navigation.NavActionBuilder,kotlin.Unit> actionBuilder);
method public final void argument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> argumentBuilder);
method public D build();
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
index b33cd9b..7160edf 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -148,7 +148,7 @@
}
assertThat(graph.startDestinationId).isEqualTo(15)
- graph.setStartDestination(TestClass::class)
+ graph.setStartDestination<TestClass>()
assertThat(graph.startDestinationRoute).isEqualTo("route/{arg}")
assertThat(graph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
}
@@ -162,7 +162,7 @@
// start destination not added via KClass, cannot match
assertFailsWith<IllegalStateException> {
- graph.setStartDestination(TestClass::class)
+ graph.setStartDestination<TestClass>()
}
}
@@ -205,7 +205,7 @@
addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
}
- val dest = graph.findNode(TestClass::class)
+ val dest = graph.findNode<TestClass>()
assertThat(dest).isNotNull()
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgument.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavArgument.kt
index a8eb63a..2239fdc 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavArgument.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavArgument.kt
@@ -60,7 +60,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun putDefaultValue(name: String, bundle: Bundle) {
- if (isDefaultValuePresent) {
+ // even if there is defaultValuePresent, the defaultValue itself could be null as in the
+ // case of safe args where we know there is default value present but we are not able to
+ // read the actual default (serializer limitations), so the defaultValue is set to null.
+ if (isDefaultValuePresent && defaultValue != null) {
type.put(bundle, name, defaultValue)
}
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
index 61da16f..0d2ffa9 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -90,7 +90,7 @@
*
* @return the newly constructed [NavDestination]
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@OptIn(InternalSerializationApi::class)
public constructor(
navigator: Navigator<out D>,
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 7f6ddf4..d175f19 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -192,8 +192,8 @@
* @return the node with route - the node must have been created with a route from [KClass]
*/
@OptIn(InternalSerializationApi::class)
- internal fun <T : Any> findNode(route: KClass<T>?): NavDestination? {
- return if (route != null) findNode(route.serializer().hashCode()) else null
+ internal inline fun <reified T> findNode(): NavDestination? {
+ return findNode(serializer<T>().hashCode())
}
/**
@@ -367,10 +367,9 @@
* @param startDestRoute The route of the destination as a [KClass] to be shown when navigating
* to this NavGraph.
*/
- @OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun setStartDestination(startDestRoute: KClass<*>) {
- setStartDestination(startDestRoute.serializer()) { startDestination ->
+ public inline fun <reified T> setStartDestination() {
+ setStartDestination(serializer<T>()) { startDestination ->
startDestination.route!!
}
}
@@ -394,8 +393,10 @@
}
}
+ // unfortunately needs to be public so reified setStartDestination can access this
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@OptIn(ExperimentalSerializationApi::class)
- private fun <T> setStartDestination(
+ public fun <T> setStartDestination(
serializer: KSerializer<T>,
parseRoute: (NavDestination) -> String,
) {
@@ -403,7 +404,7 @@
val startDest = findNode(id)
checkNotNull(startDest) {
"Cannot find startDestination ${serializer.descriptor.serialName} from NavGraph. " +
- "Ensure the starting NavDestination was added via KClass."
+ "Ensure the starting NavDestination was added with route from KClass."
}
// when dest id is based on serializer, we expect the dest route to have been generated
// and set
@@ -523,8 +524,8 @@
* @throws IllegalArgumentException if no destination is found with that route.
*/
@Suppress("NOTHING_TO_INLINE")
-internal inline operator fun <T : Any> NavGraph.get(route: KClass<T>): NavDestination =
- findNode(route)
+internal inline operator fun <reified T : Any> NavGraph.get(route: KClass<T>): NavDestination =
+ findNode<T>()
?: throw IllegalArgumentException("No destination for $route was found in $this")
/**
@@ -544,8 +545,9 @@
public operator fun NavGraph.contains(route: String): Boolean = findNode(route) != null
/** Returns `true` if a destination with `route` is found in this navigation graph. */
-internal operator fun <T : Any> NavGraph.contains(route: KClass<T>): Boolean =
- findNode(route) != null
+@Suppress("UNUSED_PARAMETER")
+internal inline operator fun <reified T : Any> NavGraph.contains(route: KClass<T>): Boolean =
+ findNode<T>() != null
/** Returns `true` if a destination with `route` is found in this navigation graph. */
internal operator fun <T> NavGraph.contains(route: T): Boolean = findNode(route) != null
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
index 6e42aff..e4213e9 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -21,6 +21,7 @@
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.serializer
/**
* Construct a new [NavGraph]
@@ -64,8 +65,8 @@
/**
* Construct a new [NavGraph]
*
- * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * @param startDestination the starting destination's route from a [KClass] for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -84,8 +85,8 @@
/**
* Construct a new [NavGraph]
*
- * @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * @param startDestination the starting destination's route from an Object for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -142,9 +143,9 @@
/**
* Construct a nested [NavGraph]
*
- * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
- * @param route the graph's unique route as a [KClass]
+ * @param startDestination the starting destination's route from a [KClass] for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
*
@@ -161,9 +162,9 @@
/**
* Construct a nested [NavGraph]
*
- * @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
- * @param route the graph's unique route as a [KClass]
+ * @param startDestination the starting destination's route from an Object for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
*
@@ -240,7 +241,7 @@
*
* @param provider navigator used to create the destination
* @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -263,7 +264,7 @@
*
* @param provider navigator used to create the destination
* @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -318,7 +319,7 @@
if (startDestinationRoute != null) {
navGraph.setStartDestination(startDestinationRoute!!)
} else if (startDestinationClass != null) {
- navGraph.setStartDestination(startDestinationClass!!)
+ navGraph.setStartDestination(startDestinationClass!!.serializer()) { it.route!! }
} else if (startDestinationObject != null) {
navGraph.setStartDestination(startDestinationObject!!)
} else {
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index d83b630..6e626be 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -28,6 +28,7 @@
id("AndroidXPlugin")
id("com.android.library")
id("kotlin-android")
+ alias(libs.plugins.kotlinSerialization)
}
dependencies {
@@ -37,6 +38,7 @@
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
api("androidx.annotation:annotation-experimental:1.4.0")
implementation('androidx.collection:collection:1.1.0')
+ implementation(libs.kotlinSerializationCore)
api(libs.kotlinStdlib)
androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
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 b95635f..f2405d5 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -53,6 +53,9 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertFailsWith
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers
@@ -200,12 +203,26 @@
}
}
+ @Serializable
+ class TestClass
+
+ @Serializable
+ class TestClassPathArg(val arg: Int)
+
+ @Serializable
+ class TestGraph
+
companion object {
private const val UNKNOWN_DESTINATION_ID = -1
private const val TEST_ARG = "test"
private const val TEST_ARG_VALUE = "value"
private const val TEST_OVERRIDDEN_VALUE_ARG = "test_overridden_value"
private const val TEST_OVERRIDDEN_VALUE_ARG_VALUE = "override"
+ private const val TEST_CLASS_ROUTE = "androidx.navigation.NavControllerRouteTest.TestClass"
+ private const val TEST_CLASS_PATH_ARG_ROUTE = "androidx.navigation." +
+ "NavControllerRouteTest.TestClassPathArg/{arg}"
+ private const val TEST_GRAPH_ROUTE = "androidx.navigation." +
+ "NavControllerRouteTest.TestGraph"
}
@UiThreadTest
@@ -235,6 +252,125 @@
@UiThreadTest
@Test
+ fun testStartDestinationKClass() {
+ @Serializable
+ @SerialName("test")
+ class TestClass
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = TestClass::class) {
+ test(route = TestClass::class)
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo("test")
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testStartDestinationKClassWithArgs() {
+ @Serializable
+ @SerialName("test")
+ class TestClass(val arg: Int)
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = TestClass::class) {
+ test(route = TestClass::class)
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo("test/{arg}")
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testStartDestinationObject() {
+ @Serializable
+ @SerialName("test")
+ class TestClass
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = TestClass()) {
+ test(route = TestClass::class)
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo("test")
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testStartDestinationObjectWithPathArg() {
+ @Serializable
+ @SerialName("test")
+ class TestClass(val arg: Int)
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ startDestination = TestClass(0)
+ ) {
+ test(route = TestClass::class)
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo(
+ "test/{arg}"
+ )
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ val arg = navController.currentBackStackEntry?.arguments?.getInt("arg")
+ assertThat(arg).isNotNull()
+ assertThat(arg).isEqualTo(0)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testStartDestinationObjectWithQueryArg() {
+ @Serializable
+ @SerialName("test")
+ class TestClass(val arg: Boolean = false)
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ startDestination = TestClass(false)
+ ) {
+ test(route = TestClass::class)
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo(
+ "test?arg={arg}"
+ )
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ val arg = navController.currentBackStackEntry?.arguments?.getBoolean("arg")
+ assertThat(arg).isNotNull()
+ assertThat(arg).isEqualTo(false)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testStartDestinationObjectNoMatch() {
+ @Serializable
+ @SerialName("test")
+ class TestClass(val arg: Boolean = false)
+
+ val navController = createNavController()
+
+ // Even though route string matches, startDestinations are matched based on id which
+ // is based on serializer. So this won't match. StartDestination must be added via KClass
+ assertFailsWith<IllegalStateException> {
+ navController.graph = navController.createGraph(
+ startDestination = TestClass(false)
+ ) {
+ test(route = "test?arg={arg}")
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
fun testSetGraphTwice() {
val navController = createNavController()
navController.graph = nav_start_destination_route_graph
@@ -1224,6 +1360,167 @@
@UiThreadTest
@Test
+ fun testPopBackStackWithKClass() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = "start") {
+ test("start")
+ test(TestClass::class)
+ }
+
+ // first nav
+ navController.navigate("start")
+
+ // second nav
+ navController.navigate(TEST_CLASS_ROUTE)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack<TestClass>(true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackGraphWithKClass() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ route = TestGraph::class,
+ startDestination = TestClass::class
+ ) {
+ test(TestClass::class)
+ }
+
+ assertThat(navController.currentBackStack.value.size).isEqualTo(2)
+ assertThat(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(TEST_CLASS_ROUTE)
+
+ navController.navigate(TEST_GRAPH_ROUTE)
+ assertThat(navController.currentBackStack.value.size).isEqualTo(4)
+
+ val popped = navController.popBackStack<TestGraph>(true)
+ assertThat(popped).isTrue()
+ assertThat(navController.currentBackStack.value.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithKClassArg() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = "start") {
+ test("start")
+ test(TestClassPathArg::class)
+ }
+
+ // first nav
+ navController.navigate("start")
+
+ // second nav
+ navController.navigate(TEST_CLASS_PATH_ARG_ROUTE.replace("{arg}", "0"))
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack<TestClassPathArg>(true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithObject() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = "start") {
+ test("start")
+ test(TestClass::class)
+ }
+
+ // first nav
+ navController.navigate("start")
+
+ // second nav
+ navController.navigate(TEST_CLASS_ROUTE)
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack(TestClass(), true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackGraphWithObject() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ route = TestGraph::class,
+ startDestination = TestClass::class
+ ) {
+ test(TestClass::class)
+ }
+
+ assertThat(navController.currentBackStack.value.size).isEqualTo(2)
+ assertThat(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(TEST_CLASS_ROUTE)
+
+ navController.navigate(TEST_GRAPH_ROUTE)
+ assertThat(navController.currentBackStack.value.size).isEqualTo(4)
+
+ val popped = navController.popBackStack(TestGraph(), true)
+ assertThat(popped).isTrue()
+ assertThat(navController.currentBackStack.value.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithObjectArg() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = "start") {
+ test("start")
+ test(TestClassPathArg::class)
+ }
+
+ // first nav
+ navController.navigate("start")
+
+ // second nav
+ navController.navigate(TEST_CLASS_PATH_ARG_ROUTE.replace("{arg}", "0"))
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navigator.backStack.size).isEqualTo(3)
+
+ val popped = navController.popBackStack(TestClassPathArg(0), true)
+ assertThat(popped).isTrue()
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPopBackStackWithObjectIncorrectArg() {
+ val navController = createNavController()
+ navController.graph = navController.createGraph(startDestination = "start") {
+ test("start")
+ test(TestClassPathArg::class)
+ }
+
+ // first nav
+ navController.navigate("start")
+
+ // second nav
+ navController.navigate(TEST_CLASS_PATH_ARG_ROUTE.replace("{arg}", "0"))
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navigator.backStack.size).isEqualTo(3)
+ // pop with a route that has a different arg value
+ val popped = navController.popBackStack(TestClassPathArg(1), true)
+ assertThat(popped).isFalse()
+ assertThat(navigator.backStack.size).isEqualTo(3)
+ }
+
+ @UiThreadTest
+ @Test
fun testFindDestinationWithRoute() {
val navController = createNavController()
navController.graph = nav_singleArg_graph
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 31a937b..bcddb2b 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -42,8 +42,11 @@
import androidx.navigation.NavDestination.Companion.createRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.serialization.generateRouteWithArgs
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -51,6 +54,8 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.serializer
/**
* NavController manages app navigation within a [NavHost].
@@ -522,6 +527,64 @@
}
/**
+ * Attempts to pop the controller's back stack back to a specific destination.
+ *
+ * @param route The topmost destination to retain with route from a [KClass]. The
+ * target NavDestination must have been created with route from [KClass].
+ * @param inclusive Whether the given destination should also be popped.
+ * @param saveState Whether the back stack and the state of all destinations between the
+ * current destination and the [route] should be saved for later
+ * restoration via [NavOptions.Builder.setRestoreState] or the `restoreState` attribute using
+ * the same [route] (note: this matching ID is true whether
+ * [inclusive] is true or false).
+ *
+ * @return true if the stack was popped at least once and the user has been navigated to
+ * another destination, false otherwise
+ */
+ @MainThread
+ @JvmOverloads
+ internal inline fun <reified T> popBackStack(
+ inclusive: Boolean,
+ saveState: Boolean = false
+ ): Boolean = popBackStack(serializer<T>().hashCode(), inclusive, saveState)
+
+ /**
+ * Attempts to pop the controller's back stack back to a specific destination.
+ *
+ * @param route The topmost destination to retain with route from an Object. The
+ * target NavDestination must have been created with route from [KClass].
+ * @param inclusive Whether the given destination should also be popped.
+ * @param saveState Whether the back stack and the state of all destinations between the
+ * current destination and the [route] should be saved for later
+ * restoration via [NavOptions.Builder.setRestoreState] or the `restoreState` attribute using
+ * the same [route] (note: this matching ID is true whether
+ * [inclusive] is true or false).
+ *
+ * @return true if the stack was popped at least once and the user has been navigated to
+ * another destination, false otherwise
+ */
+ @OptIn(InternalSerializationApi::class)
+ @MainThread
+ @JvmOverloads
+ internal fun <T : Any> popBackStack(
+ route: T,
+ inclusive: Boolean,
+ saveState: Boolean = false
+ ): Boolean {
+ val dest = backQueue.lastOrNull {
+ it.destination.id == route::class.serializer().hashCode()
+ }
+ if (dest == null) return false
+ // route contains arguments so we need to generate and pop with the populated route
+ // rather than popping based on route pattern
+ val finalRoute = route.generateRouteWithArgs(
+ // get argument typeMap
+ dest.destination.arguments.mapValues { it.value.type }
+ )
+ return popBackStack(finalRoute, inclusive, saveState)
+ }
+
+ /**
* Attempts to pop the controller's back stack back to a specific destination. This does
* **not** handle calling [dispatchOnDestinationChanged]
*
@@ -2602,3 +2665,37 @@
route: String? = null,
builder: NavGraphBuilder.() -> Unit
): NavGraph = navigatorProvider.navigation(startDestination, route, builder)
+
+/**
+ * Construct a new [NavGraph]
+ *
+ * @param startDestination the starting destination's route from a [KClass] for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ * @param builder the builder used to construct the graph
+ */
+internal inline fun NavController.createGraph(
+ startDestination: KClass<*>,
+ route: KClass<*>? = null,
+ typeMap: Map<KType, NavType<*>>? = null,
+ builder: NavGraphBuilder.() -> Unit
+): NavGraph = navigatorProvider.navigation(startDestination, route, typeMap, builder)
+
+/**
+ * Construct a new [NavGraph]
+ *
+ * @param startDestination the starting destination's route from an Object for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ * @param builder the builder used to construct the graph
+ */
+internal inline fun NavController.createGraph(
+ startDestination: Any,
+ route: KClass<*>? = null,
+ typeMap: Map<KType, NavType<*>>? = null,
+ builder: NavGraphBuilder.() -> Unit
+): NavGraph = navigatorProvider.navigation(startDestination, route, typeMap, builder)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
index 8c9a6a1..a85bbad 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/current/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
index cb99939..8e07975 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/current/classes.dex</dex-path>
<dex-path>RuntimeEnabledSdks/RPackage.dex</dex-path>
<java-resources-root-path>RuntimeEnabledSdks/javaresources</java-resources-root-path>
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
index 2bf4d37..38e0309 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v1.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v1/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
index ed4f3707..2caa00d 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v2.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v2/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
index ea3c856..d1e82e8 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v4.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v4/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
index 8d21c64..b8a7dc1 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v5.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v5/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
index e054456..de29be9 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
@@ -51,7 +51,7 @@
packageName = "androidx.privacysandbox.sdkruntime.testsdk.current",
versionMajor = 42,
dexPaths = listOf("test-sdks/current/classes.dex"),
- entryPoint = "androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider",
+ entryPoint = "androidx.privacysandbox.sdkruntime.testsdk.CompatProvider",
)
assertThat(result).isEqualTo(expectedConfig)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
index a2f9bbe..ac306ec 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkProviderTest.kt
@@ -35,6 +35,8 @@
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.LoadSdkCallback
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+import androidx.privacysandbox.sdkruntime.core.internal.ClientApiVersion
+import androidx.privacysandbox.sdkruntime.core.internal.ClientFeature
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -119,10 +121,7 @@
@Test
fun getSandboxedSdks_delegateToSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 2",
- sdkVersion >= 2
- )
+ assumeFeatureAvailable(ClientFeature.SDK_SANDBOX_CONTROLLER)
val expectedResult = SandboxedSdkCompat(
sdkInterface = Binder(),
@@ -147,10 +146,7 @@
@Test
fun getAppOwnedSdkSandboxInterfaces_delegateToSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 4",
- sdkVersion >= 4
- )
+ assumeFeatureAvailable(ClientFeature.APP_OWNED_INTERFACES)
val expectedResult = AppOwnedSdkSandboxInterfaceCompat(
name = "TestAppOwnedSdk",
@@ -173,10 +169,7 @@
@Test
fun registerSdkSandboxActivityHandler_delegateToSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 3",
- sdkVersion >= 3
- )
+ assumeFeatureAvailable(ClientFeature.SDK_ACTIVITY_HANDLER)
val catchingHandler = CatchingSdkActivityHandler()
@@ -198,10 +191,7 @@
@Test
fun sdkSandboxActivityHandler_ReceivesLifecycleEventsFromOriginalActivityHolder() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 3",
- sdkVersion >= 3
- )
+ assumeFeatureAvailable(ClientFeature.SDK_ACTIVITY_HANDLER)
val catchingHandler = CatchingSdkActivityHandler()
@@ -226,10 +216,7 @@
@Test
fun unregisterSdkSandboxActivityHandler_delegateToSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 3",
- sdkVersion >= 3
- )
+ assumeFeatureAvailable(ClientFeature.SDK_ACTIVITY_HANDLER)
val handler = CatchingSdkActivityHandler()
@@ -242,10 +229,7 @@
@Test
fun loadSdk_returnsResultFromSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 5",
- sdkVersion >= 5
- )
+ assumeFeatureAvailable(ClientFeature.LOAD_SDK)
val sdkName = "SDK"
val sdkParams = Bundle()
@@ -265,10 +249,7 @@
@Test
fun loadSdk_rethrowsExceptionFromSdkController() {
- assumeTrue(
- "Requires Versions.API_VERSION >= 5",
- sdkVersion >= 5
- )
+ assumeFeatureAvailable(ClientFeature.LOAD_SDK)
val expectedError = LoadSdkCompatException(
LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
@@ -311,10 +292,17 @@
}
}
+ private fun assumeFeatureAvailable(clientFeature: ClientFeature) {
+ assumeTrue(
+ "Requires $clientFeature available (API >= ${clientFeature.availableFrom})",
+ clientFeature.isAvailable(sdkVersion)
+ )
+ }
+
companion object {
/**
- * Create test params for each previously released [Versions.API_VERSION] + current one.
+ * Create test params for each supported [ClientApiVersion] + current one.
* Each released version must have test-sdk named as "vX" (where X is version to test).
* These TestSDKs should be registered in RuntimeEnabledSdkTable.xml and be compatible with
* [TestSdkWrapper].
@@ -322,16 +310,16 @@
@Parameterized.Parameters(name = "sdk: {0}, version: {1}")
@JvmStatic
fun params(): List<Array<Any>> = buildList {
- for (apiVersion in 1..Versions.API_VERSION) {
- if (apiVersion == 3) {
- continue // V3 was released as V4 (original release postponed)
- }
- add(
- arrayOf(
- "v$apiVersion",
- apiVersion,
+ ClientApiVersion.values().forEach { version ->
+ // FUTURE_VERSION tested separately
+ if (version != ClientApiVersion.FUTURE_VERSION) {
+ add(
+ arrayOf(
+ "v${version.apiLevel}",
+ version.apiLevel,
+ )
)
- )
+ }
}
add(
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
index adf7a7ee..bf5f248 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
@@ -22,6 +22,7 @@
import androidx.privacysandbox.sdkruntime.client.loader.storage.CachedLocalSdkStorage
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+import androidx.privacysandbox.sdkruntime.core.internal.ClientFeature
/**
* Load SDK bundled with App.
@@ -63,17 +64,17 @@
private fun getParentClassLoader(): ClassLoader = appContext.classLoader.parent!!
private fun createLocalSdk(
- classLoader: ClassLoader,
+ sdkClassLoader: ClassLoader,
sdkConfig: LocalSdkConfig
): LocalSdkProvider {
try {
- val apiVersion = VersionHandshake.perform(classLoader)
- ResourceRemapping.apply(classLoader, sdkConfig.resourceRemapping)
- if (apiVersion >= 2) {
- return createSdkProviderV2(classLoader, apiVersion, sdkConfig)
- } else if (apiVersion >= 1) {
- return createSdkProviderV1(classLoader, sdkConfig)
+ val sdkApiVersion = VersionHandshake.perform(sdkClassLoader)
+ ResourceRemapping.apply(sdkClassLoader, sdkConfig.resourceRemapping)
+ if (ClientFeature.SDK_SANDBOX_CONTROLLER.isAvailable(sdkApiVersion)) {
+ val controller = controllerFactory.createControllerFor(sdkConfig)
+ SandboxControllerInjector.inject(sdkClassLoader, sdkApiVersion, controller)
}
+ return SdkProviderV1.create(sdkClassLoader, sdkConfig, appContext)
} catch (ex: Exception) {
throw LoadSdkCompatException(
LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
@@ -81,28 +82,6 @@
ex
)
}
-
- throw LoadSdkCompatException(
- LoadSdkCompatException.LOAD_SDK_NOT_FOUND,
- "Incorrect Api version"
- )
- }
-
- private fun createSdkProviderV1(
- sdkClassLoader: ClassLoader,
- sdkConfig: LocalSdkConfig
- ): LocalSdkProvider {
- return SdkProviderV1.create(sdkClassLoader, sdkConfig, appContext)
- }
-
- private fun createSdkProviderV2(
- sdkClassLoader: ClassLoader,
- sdkVersion: Int,
- sdkConfig: LocalSdkConfig
- ): LocalSdkProvider {
- val controller = controllerFactory.createControllerFor(sdkConfig)
- SandboxControllerInjector.inject(sdkClassLoader, sdkVersion, controller)
- return SdkProviderV1.create(sdkClassLoader, sdkConfig, appContext)
}
companion object {
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
index e6441a0..56f29f7 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxControllerInjector.kt
@@ -24,6 +24,7 @@
import androidx.privacysandbox.sdkruntime.client.loader.impl.injector.SdkActivityHandlerWrapper
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+import androidx.privacysandbox.sdkruntime.core.internal.ClientFeature
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
@@ -65,20 +66,23 @@
val sandboxedSdkCompatProxyFactory =
SandboxedSdkCompatProxyFactory.createFor(sdkClassLoader)
- val sdkActivityHandlerWrapper = if (sdkVersion >= 3)
- SdkActivityHandlerWrapper.createFor(sdkClassLoader)
- else
- null
+ val sdkActivityHandlerWrapper =
+ if (ClientFeature.SDK_ACTIVITY_HANDLER.isAvailable(sdkVersion))
+ SdkActivityHandlerWrapper.createFor(sdkClassLoader)
+ else
+ null
- val appOwnedSdkInterfaceProxyFactory = if (sdkVersion >= 4)
- AppOwnedSdkInterfaceProxyFactory.createFor(sdkClassLoader)
- else
- null
+ val appOwnedSdkInterfaceProxyFactory =
+ if (ClientFeature.APP_OWNED_INTERFACES.isAvailable(sdkVersion))
+ AppOwnedSdkInterfaceProxyFactory.createFor(sdkClassLoader)
+ else
+ null
- val loadSdkCallbackWrapper = if (sdkVersion >= 5)
- LoadSdkCallbackWrapper.createFor(sdkClassLoader)
- else
- null
+ val loadSdkCallbackWrapper =
+ if (ClientFeature.LOAD_SDK.isAvailable(sdkVersion))
+ LoadSdkCallbackWrapper.createFor(sdkClassLoader)
+ else
+ null
val proxy = Proxy.newProxyInstance(
sdkClassLoader,
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
index 84faafc..2995cca 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
@@ -26,6 +26,8 @@
import androidx.privacysandbox.sdkruntime.core.Versions
import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.privacysandbox.sdkruntime.core.internal.ClientApiVersion
+import androidx.privacysandbox.sdkruntime.core.internal.ClientFeature
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -46,8 +48,8 @@
@Before
fun setUp() {
- // Emulate loading via client lib
- Versions.handShake(Versions.API_VERSION)
+ // Emulate loading via client lib with only base features available
+ clientHandShakeForMinSupportedVersion()
context = ApplicationProvider.getApplicationContext()
}
@@ -67,10 +69,7 @@
}
@Test
- fun loadSdk_clientApiBelow5_throwsLoadSdkNotFoundException() {
- // Emulate loading via client lib with version below 5
- Versions.handShake(4)
-
+ fun loadSdk_whenNotAvailable_throwsLoadSdkNotFoundException() {
SdkSandboxControllerCompat.injectLocalImpl(TestStubImpl())
val controllerCompat = SdkSandboxControllerCompat.from(context)
@@ -85,6 +84,8 @@
@Test
fun loadSdk_returnsLoadedSdkFromLocalImpl() {
+ clientHandShakeForVersionIncluding(ClientFeature.LOAD_SDK)
+
val expectedResult = SandboxedSdkCompat(Binder())
val stubLocalImpl = TestStubImpl(
loadSdkResult = expectedResult
@@ -105,6 +106,8 @@
@Test
fun loadSdk_rethrowsExceptionFromLocalImpl() {
+ clientHandShakeForVersionIncluding(ClientFeature.LOAD_SDK)
+
val expectedError = LoadSdkCompatException(RuntimeException(), Bundle())
SdkSandboxControllerCompat.injectLocalImpl(
TestStubImpl(
@@ -137,10 +140,7 @@
}
@Test
- fun getAppOwnedSdkSandboxInterfaces_clientApiBelow4_returnsEmptyList() {
- // Emulate loading via client lib with version below 4
- Versions.handShake(3)
-
+ fun getAppOwnedSdkSandboxInterfaces_whenNotAvailable_returnsEmptyList() {
SdkSandboxControllerCompat.injectLocalImpl(
TestStubImpl(
appOwnedSdks = listOf(
@@ -160,6 +160,8 @@
@Test
fun getAppOwnedSdkSandboxInterfaces_returnsListFromLocalImpl() {
+ clientHandShakeForVersionIncluding(ClientFeature.APP_OWNED_INTERFACES)
+
val expectedResult = listOf(
AppOwnedSdkSandboxInterfaceCompat(
name = "TestSdk",
@@ -180,6 +182,8 @@
@Test
fun registerSdkSandboxActivityHandler_registerItInLocalImpl() {
+ clientHandShakeForVersionIncluding(ClientFeature.SDK_ACTIVITY_HANDLER)
+
val localImpl = TestStubImpl()
SdkSandboxControllerCompat.injectLocalImpl(localImpl)
@@ -193,6 +197,8 @@
@Test
fun unregisterSdkSandboxActivityHandler_unregisterItFromLocalImpl() {
+ clientHandShakeForVersionIncluding(ClientFeature.SDK_ACTIVITY_HANDLER)
+
val localImpl = TestStubImpl()
SdkSandboxControllerCompat.injectLocalImpl(localImpl)
@@ -208,10 +214,7 @@
}
@Test
- fun registerSdkSandboxActivityHandler_clientApiBelow3_throwsUnsupportedOperationException() {
- // Emulate loading via client lib with version below 3
- Versions.handShake(2)
-
+ fun registerSdkSandboxActivityHandler_whenNotAvailable_throwsUnsupportedOperationException() {
SdkSandboxControllerCompat.injectLocalImpl(TestStubImpl())
val controllerCompat = SdkSandboxControllerCompat.from(context)
@@ -225,10 +228,7 @@
}
@Test
- fun unregisterSdkSandboxActivityHandler_clientApiBelow3_throwsUnsupportedOperationException() {
- // Emulate loading via client lib with version below 3
- Versions.handShake(2)
-
+ fun unregisterSdkSandboxActivityHandler_whenNotAvailable_throwsUnsupportedOperationException() {
SdkSandboxControllerCompat.injectLocalImpl(TestStubImpl())
val controllerCompat = SdkSandboxControllerCompat.from(context)
@@ -241,6 +241,22 @@
}
}
+ /**
+ * Call [Versions.handShake] to emulate loading via client lib.
+ * Using version where [clientFeature] available.
+ */
+ private fun clientHandShakeForVersionIncluding(clientFeature: ClientFeature) {
+ Versions.handShake(clientFeature.availableFrom.apiLevel)
+ }
+
+ /**
+ * Call [Versions.handShake] to emulate loading via client lib.
+ * Using [ClientApiVersion.MIN_SUPPORTED] - to check features available in all client versions.
+ */
+ private fun clientHandShakeForMinSupportedVersion() {
+ Versions.handShake(ClientApiVersion.MIN_SUPPORTED.apiLevel)
+ }
+
internal class TestStubImpl(
private val sandboxedSdks: List<SandboxedSdkCompat> = emptyList(),
private val appOwnedSdks: List<AppOwnedSdkSandboxInterfaceCompat> = emptyList(),
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
index 69d5126..6d50d2c 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
@@ -18,19 +18,22 @@
import androidx.annotation.Keep
import androidx.annotation.RestrictTo
+import androidx.privacysandbox.sdkruntime.core.internal.ClientApiVersion
import org.jetbrains.annotations.TestOnly
/**
* Store internal API version (for Client-Core communication).
- * Methods invoked via reflection.
*
+ * DO NOT CHANGE THIS CLASS.
+ * Methods invoked via reflection from previously released versions of sdkruntime-client.
*/
@Suppress("unused")
@Keep
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object Versions {
- const val API_VERSION = 5
+ @JvmField
+ val API_VERSION = ClientApiVersion.CURRENT_VERSION.apiLevel
@JvmField
var CLIENT_VERSION: Int? = null
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt
index e07d010..b542c62 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/LocalImpl.kt
@@ -25,6 +25,7 @@
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.LoadSdkCallback
import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+import androidx.privacysandbox.sdkruntime.core.internal.ClientFeature
import java.util.concurrent.Executor
/**
@@ -42,7 +43,7 @@
executor: Executor,
callback: LoadSdkCallback
) {
- if (clientVersion >= 5) {
+ if (ClientFeature.LOAD_SDK.isAvailable(clientVersion)) {
implFromClient.loadSdk(sdkName, params, executor, callback)
} else {
executor.execute {
@@ -61,7 +62,7 @@
}
override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> {
- return if (clientVersion >= 4) {
+ return if (ClientFeature.APP_OWNED_INTERFACES.isAvailable(clientVersion)) {
implFromClient.getAppOwnedSdkSandboxInterfaces()
} else {
emptyList()
@@ -71,22 +72,24 @@
override fun registerSdkSandboxActivityHandler(
handlerCompat: SdkSandboxActivityHandlerCompat
): IBinder {
- if (clientVersion < 3) {
+ if (ClientFeature.SDK_ACTIVITY_HANDLER.isAvailable(clientVersion)) {
+ return implFromClient.registerSdkSandboxActivityHandler(handlerCompat)
+ } else {
throw UnsupportedOperationException(
"Client library version doesn't support SdkActivities"
)
}
- return implFromClient.registerSdkSandboxActivityHandler(handlerCompat)
}
override fun unregisterSdkSandboxActivityHandler(
handlerCompat: SdkSandboxActivityHandlerCompat
) {
- if (clientVersion < 3) {
+ if (ClientFeature.SDK_ACTIVITY_HANDLER.isAvailable(clientVersion)) {
+ implFromClient.unregisterSdkSandboxActivityHandler(handlerCompat)
+ } else {
throw UnsupportedOperationException(
"Client library version doesn't support SdkActivities"
)
}
- implFromClient.unregisterSdkSandboxActivityHandler(handlerCompat)
}
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientApiVersion.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientApiVersion.kt
new file mode 100644
index 0000000..dc31b15
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientApiVersion.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.privacysandbox.sdkruntime.core.internal
+
+import androidx.annotation.RestrictTo
+
+/**
+ * List of all supported internal API versions (Client-Core communication).
+ *
+ * NEVER REMOVE / MODIFY RELEASED VERSIONS:
+ * That could break loading of SDKs built with previous/future library version.
+ *
+ * Adding new version here bumps internal API version for next library release:
+ * [androidx.privacysandbox.sdkruntime.core.Versions.API_VERSION]
+ * When adding a new version, ALL new features from this version should be specified
+ * (NO FUTURE CHANGES SUPPORTED).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+enum class ClientApiVersion(
+ val apiLevel: Int,
+ private val newFeatures: Set<ClientFeature> = emptySet()
+) {
+ V1__1_0_ALPHA01(apiLevel = 1),
+
+ V2__1_0_ALPHA02(
+ apiLevel = 2,
+ newFeatures = setOf(
+ ClientFeature.SDK_SANDBOX_CONTROLLER
+ )
+ ),
+
+ // V3 was released as V4 (original release postponed)
+ V4__1_0_ALPHA05(
+ apiLevel = 4,
+ newFeatures = setOf(
+ ClientFeature.SDK_ACTIVITY_HANDLER,
+ ClientFeature.APP_OWNED_INTERFACES,
+ )
+ ),
+
+ V5__1_0_ALPHA13(
+ apiLevel = 5,
+ newFeatures = setOf(
+ ClientFeature.LOAD_SDK
+ )
+ ),
+
+ /**
+ * Unreleased API version.
+ * Features not added to other versions will be automatically added here (to allow testing).
+ */
+ FUTURE_VERSION(apiLevel = Int.MAX_VALUE);
+
+ companion object {
+ val MIN_SUPPORTED = values().minBy { v -> v.apiLevel }
+ val CURRENT_VERSION = values().filter { v -> v != FUTURE_VERSION }.maxBy { v -> v.apiLevel }
+
+ private val FEATURE_TO_VERSION_MAP = buildFeatureMap()
+
+ fun minAvailableVersionFor(clientFeature: ClientFeature): ClientApiVersion {
+ return FEATURE_TO_VERSION_MAP[clientFeature]!!
+ }
+
+ /**
+ * Build mapping between [ClientFeature] and version where it first time became available.
+ * Features not added to specific version mapped as [FUTURE_VERSION]
+ */
+ private fun buildFeatureMap(): Map<ClientFeature, ClientApiVersion> {
+ if (FUTURE_VERSION.newFeatures.isNotEmpty()) {
+ throw IllegalStateException("FUTURE_VERSION MUST NOT define any features")
+ }
+ return buildMap {
+ values().forEach { version ->
+ version.newFeatures.forEach { feature ->
+ val oldVersion = put(feature, version)
+ if (oldVersion != null) {
+ throw IllegalStateException(
+ "$feature duplicated in $version and $oldVersion"
+ )
+ }
+ }
+ }
+ ClientFeature.values().forEach { feature ->
+ if (!containsKey(feature)) {
+ put(feature, FUTURE_VERSION)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientFeature.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientFeature.kt
new file mode 100644
index 0000000..5e24eb9
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/internal/ClientFeature.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.privacysandbox.sdkruntime.core.internal
+
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
+
+/**
+ * List of features using Client-Core internal API.
+ * Each feature available since particular ([ClientApiVersion]).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+enum class ClientFeature {
+ /**
+ * Support for retrieving [SdkSandboxControllerCompat] on SDK side.
+ */
+ SDK_SANDBOX_CONTROLLER,
+
+ /**
+ * Support for starting SDK Activity:
+ * [SdkSandboxControllerCompat.registerSdkSandboxActivityHandler]
+ * [SdkSandboxControllerCompat.unregisterSdkSandboxActivityHandler]
+ */
+ SDK_ACTIVITY_HANDLER,
+
+ /**
+ * Support for retrieving App-owned interfaces:
+ * [SdkSandboxControllerCompat.getAppOwnedSdkSandboxInterfaces]
+ */
+ APP_OWNED_INTERFACES,
+
+ /**
+ * Support for loading SDKs by other SDKs:
+ * [SdkSandboxControllerCompat.loadSdk]
+ */
+ LOAD_SDK;
+
+ val availableFrom: ClientApiVersion
+ get() = ClientApiVersion.minAvailableVersionFor(this)
+
+ fun isAvailable(apiLevel: Int): Boolean {
+ return apiLevel >= availableFrom.apiLevel
+ }
+}
diff --git a/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 96%
rename from privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index c6960bd..fc1f0b5 100644
--- a/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.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,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.current
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 97%
rename from privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index c2b5312..307363e 100644
--- a/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v1
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 95%
rename from privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index cf4d8c7..8240d3ca 100644
--- a/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.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,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v2
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 96%
rename from privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index a97aec2..e24fb65 100644
--- a/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.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,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v4
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v5/build.gradle b/privacysandbox/sdkruntime/test-sdks/v5/build.gradle
index cecf9a2..9b98f82 100644
--- a/privacysandbox/sdkruntime/test-sdks/v5/build.gradle
+++ b/privacysandbox/sdkruntime/test-sdks/v5/build.gradle
@@ -35,7 +35,7 @@
dependencies {
implementation(libs.kotlinCoroutinesCore)
- implementation(project(":privacysandbox:sdkruntime:sdkruntime-provider"))
+ implementation("androidx.privacysandbox.sdkruntime:sdkruntime-provider:1.0.0-alpha13")
}
/*
diff --git a/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 98%
rename from privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index bdc8581..857bdc9 100644
--- a/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v5
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index 116aea1..cc8f439 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -325,7 +325,8 @@
val dbFile = instrumentation.targetContext.getDatabasePath("test.db")
val driverHelper = MigrationTestHelper(
instrumentation = instrumentation,
- driver = AndroidSQLiteDriver(dbFile.path),
+ fileName = dbFile.path,
+ driver = AndroidSQLiteDriver(),
databaseClass = MigrationDbKotlin::class
)
assertThrows<IllegalStateException> {
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index e4b1895..52b7c70 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -34,13 +34,14 @@
class AutoMigrationTest : BaseAutoMigrationTest() {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val file = instrumentation.targetContext.getDatabasePath("test.db")
- private val driver: SQLiteDriver = BundledSQLiteDriver(file.path)
+ private val driver: SQLiteDriver = BundledSQLiteDriver()
@get:Rule
val migrationTestHelper = MigrationTestHelper(
instrumentation = instrumentation,
driver = driver,
- databaseClass = AutoMigrationDatabase::class
+ databaseClass = AutoMigrationDatabase::class,
+ fileName = file.path
)
override fun getTestHelper() = migrationTestHelper
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
index 9b81e8a..8d406e8 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
@@ -34,7 +34,7 @@
context = instrumentation.targetContext,
name = file.path,
factory = { SampleDatabase::class.instantiateImpl() }
- ).setDriver(BundledSQLiteDriver(file.path))
+ ).setDriver(BundledSQLiteDriver())
}
@BeforeTest
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
index b4a0849..4a36798 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -27,7 +27,7 @@
override fun getRoomDatabase(): SampleDatabase {
return Room.inMemoryDatabaseBuilder<SampleDatabase>(
context = instrumentation.targetContext,
- ).setDriver(BundledSQLiteDriver(":memory:"))
+ ).setDriver(BundledSQLiteDriver())
.build()
}
}
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index 6a755ab..300ff8c 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -26,8 +26,9 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
override fun getRoomDatabase(): SampleDatabase {
- return Room.inMemoryDatabaseBuilder<SampleDatabase>(instrumentation.targetContext)
- .setDriver(BundledSQLiteDriver(":memory:"))
+ return Room.inMemoryDatabaseBuilder<SampleDatabase>(
+ context = instrumentation.targetContext
+ ).setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index 19aca6e..3e36969 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -25,12 +25,13 @@
import org.junit.Rule
class AutoMigrationTest : BaseAutoMigrationTest() {
- private val tempFile = createTempFile("test.db").also { it.toFile().deleteOnExit() }
- private val driver: SQLiteDriver = BundledSQLiteDriver(tempFile.toString())
+ private val tempFilePath = createTempFile("test.db").also { it.toFile().deleteOnExit() }
+ private val driver: SQLiteDriver = BundledSQLiteDriver()
@get:Rule
val migrationTestHelper = MigrationTestHelper(
schemaDirectoryPath = Path("schemas-ksp"),
+ databasePath = tempFilePath,
driver = driver,
databaseClass = AutoMigrationDatabase::class
)
@@ -38,7 +39,7 @@
override fun getTestHelper() = migrationTestHelper
override fun getRoomDatabase(): AutoMigrationDatabase {
- return Room.databaseBuilder<AutoMigrationDatabase>(tempFile.toString())
+ return Room.databaseBuilder<AutoMigrationDatabase>(tempFilePath.toString())
.setDriver(driver).build()
}
}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
index d36069c..e83e0f9 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
@@ -25,6 +25,6 @@
override fun getRoomDatabaseBuilder(): RoomDatabase.Builder<SampleDatabase> {
val tempFile = createTempFile("test.db").also { it.toFile().deleteOnExit() }
return Room.databaseBuilder(tempFile.toString()) { SampleDatabase::class.instantiateImpl() }
- .setDriver(BundledSQLiteDriver(tempFile.toString()))
+ .setDriver(BundledSQLiteDriver())
}
}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
index 8f52839..c6ec8bb 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -23,7 +23,7 @@
override fun getRoomDatabase(): SampleDatabase {
return Room.inMemoryDatabaseBuilder<SampleDatabase>()
- .setDriver(BundledSQLiteDriver(":memory:"))
+ .setDriver(BundledSQLiteDriver())
.build()
}
}
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index ce59cf6..6be6358f 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -24,7 +24,7 @@
override fun getRoomDatabase(): SampleDatabase {
return Room.inMemoryDatabaseBuilder<SampleDatabase>()
- .setDriver(BundledSQLiteDriver(":memory:"))
+ .setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index 7909d4d..4300bb2 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -27,10 +27,11 @@
class AutoMigrationTest : BaseAutoMigrationTest() {
private val filename = "/tmp/test-${Random.nextInt()}.db"
- private val driver: SQLiteDriver = BundledSQLiteDriver(filename)
+ private val driver: SQLiteDriver = BundledSQLiteDriver()
private val migrationTestHelper = MigrationTestHelper(
schemaDirectoryPath = getSchemaDirectoryPath(),
+ fileName = filename,
driver = driver,
databaseClass = AutoMigrationDatabase::class,
databaseFactory = { AutoMigrationDatabase::class.instantiateImpl() }
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
index dd4f1fb..17e05f8 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
@@ -29,10 +29,8 @@
private val filename = "/tmp/test-${Random.nextInt()}.db"
override fun getRoomDatabaseBuilder(): RoomDatabase.Builder<SampleDatabase> {
- return Room.databaseBuilder<SampleDatabase>(filename) {
- SampleDatabase::class.instantiateImpl()
- }
- .setDriver(BundledSQLiteDriver(filename))
+ return Room.databaseBuilder(filename) { SampleDatabase::class.instantiateImpl() }
+ .setDriver(BundledSQLiteDriver())
}
@BeforeTest
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
index 8db97f5..fdc83b7 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/InvalidationTest.kt
@@ -23,7 +23,7 @@
override fun getRoomDatabase(): SampleDatabase {
return Room.inMemoryDatabaseBuilder { SampleDatabase::class.instantiateImpl() }
- .setDriver(BundledSQLiteDriver(":memory:"))
+ .setDriver(BundledSQLiteDriver())
.build()
}
}
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index 0f15cc8..5a0d3c9 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -24,10 +24,8 @@
class SimpleQueryTest : BaseSimpleQueryTest() {
override fun getRoomDatabase(): SampleDatabase {
- return Room.inMemoryDatabaseBuilder<SampleDatabase> {
- SampleDatabase::class.instantiateImpl()
- }
- .setDriver(BundledSQLiteDriver(":memory:"))
+ return Room.inMemoryDatabaseBuilder { SampleDatabase::class.instantiateImpl() }
+ .setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
index 5c64129..398be07 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
@@ -44,8 +44,10 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val file = instrumentation.targetContext.getDatabasePath("test.db")
+ override val fileName = file.path
+
override fun getDriver(): SQLiteDriver {
- return BundledSQLiteDriver(file.path)
+ return BundledSQLiteDriver()
}
@BeforeTest
@@ -63,7 +65,12 @@
@Test
fun reusingConnectionOnBlocking() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
withContext(NewThreadDispatcher()) {
pool.useConnection(isReadOnly = true) { initialConnection ->
@@ -84,7 +91,12 @@
@Test
fun newThreadDispatcherDoesNotAffectThreadConfinement() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
val job = launch(Dispatchers.IO) {
pool.useReaderConnection {
delay(500)
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
index 2083ced..1e6b4ec 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
@@ -72,11 +72,13 @@
this.connectionPool = if (configuration.name == null) {
// An in-memory database must use a single connection pool.
newSingleConnectionPool(
- driver = DriverWrapper(config.sqliteDriver)
+ driver = DriverWrapper(config.sqliteDriver),
+ fileName = ":memory:"
)
} else {
newConnectionPool(
driver = DriverWrapper(config.sqliteDriver),
+ fileName = configuration.name,
maxNumOfReaders = configuration.journalMode.getMaxNumberOfReaders(),
maxNumOfWriters = configuration.journalMode.getMaxNumberOfWriters()
)
@@ -200,7 +202,8 @@
val supportDriver: SupportSQLiteDriver
) : ConnectionPool {
private val supportConnection by lazy(LazyThreadSafetyMode.PUBLICATION) {
- SupportPooledConnection(supportDriver.open())
+ val fileName = supportDriver.openHelper.databaseName ?: ":memory:"
+ SupportPooledConnection(supportDriver.open(fileName))
}
override suspend fun <R> useConnection(
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
index e0fcabb..70fe31b 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteDriver.android.kt
@@ -24,7 +24,7 @@
class SupportSQLiteDriver(
val openHelper: SupportSQLiteOpenHelper
) : SQLiteDriver {
- override fun open(): SupportSQLiteConnection {
+ override fun open(fileName: String): SupportSQLiteConnection {
return SupportSQLiteConnection(openHelper.writableDatabase)
}
}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index 90eb6d5..f3f192c 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -585,7 +585,7 @@
val preparedQueries = mutableListOf<String>()
- override fun open(): SQLiteConnection {
+ override fun open(fileName: String): SQLiteConnection {
return FakeSQLiteConnection()
}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
index 74d9ef5..e8d3c30 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
@@ -51,10 +51,8 @@
protected inner class DriverWrapper(
private val actual: SQLiteDriver
) : SQLiteDriver {
- override fun open(): SQLiteConnection {
- // TODO(b/317973999): Try to validate connections point to the same filename as the
- // one in the database configuration provided in the builder.
- return configureConnection(actual.open())
+ override fun open(fileName: String): SQLiteConnection {
+ return configureConnection(actual.open(fileName))
}
}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
index 168aaa0..f50a613 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
@@ -73,10 +73,11 @@
* in-memory databases whose schema and data are isolated to a database connection.
*
* @param driver The driver from which to request the connection to be opened.
+ * @param fileName The database file name.
* @return The newly created connection pool
*/
-internal fun newSingleConnectionPool(driver: SQLiteDriver): ConnectionPool =
- ConnectionPoolImpl(driver)
+internal fun newSingleConnectionPool(driver: SQLiteDriver, fileName: String): ConnectionPool =
+ ConnectionPoolImpl(driver, fileName)
/**
* Creates a new [ConnectionPool] with multiple connections separated by readers and writers.
@@ -86,15 +87,17 @@
* DELETE or PERSIST) then it is recommended to create a pool of one writer and one reader.
*
* @param driver The driver from which to request new connections to be opened.
+ * @param fileName The database file name.
* @param maxNumOfReaders The maximum number of connections to be opened and used as readers.
* @param maxNumOfWriters The maximum number of connections to be opened and used as writers.
* @return The newly created connection pool
*/
internal fun newConnectionPool(
driver: SQLiteDriver,
+ fileName: String,
maxNumOfReaders: Int,
maxNumOfWriters: Int
-): ConnectionPool = ConnectionPoolImpl(driver, maxNumOfReaders, maxNumOfWriters)
+): ConnectionPool = ConnectionPoolImpl(driver, fileName, maxNumOfReaders, maxNumOfWriters)
/**
* Defines an object that provides 'raw' access to a connection.
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
index 562bbfb..887111f 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
@@ -61,17 +61,19 @@
constructor(
driver: SQLiteDriver,
+ fileName: String
) {
this.driver = driver
this.readers = Pool(
capacity = 1,
- connectionFactory = { driver.open() }
+ connectionFactory = { driver.open(fileName) }
)
this.writers = readers
}
constructor(
driver: SQLiteDriver,
+ fileName: String,
maxNumOfReaders: Int,
maxNumOfWriters: Int,
) {
@@ -85,7 +87,7 @@
this.readers = Pool(
capacity = maxNumOfReaders,
connectionFactory = {
- driver.open().also { newConnection ->
+ driver.open(fileName).also { newConnection ->
// Enforce to be read only (might be disabled by a YOLO developer)
newConnection.execSQL("PRAGMA query_only = 1")
}
@@ -93,7 +95,7 @@
)
this.writers = Pool(
capacity = maxNumOfWriters,
- connectionFactory = { driver.open() }
+ connectionFactory = { driver.open(fileName) }
)
}
diff --git a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
index 16a7bac..42ba221 100644
--- a/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
+++ b/room/room-runtime/src/commonTest/kotlin/androidx/room/coroutines/BaseConnectionPoolTest.kt
@@ -61,10 +61,17 @@
abstract fun getDriver(): SQLiteDriver
+ abstract val fileName: String
+
@Test
fun readerIsReadOnlyConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
fun assertMsg(ex: SQLiteException) {
assertThat(ex.message)
.isEqualTo("Error code: 8, message: attempt to write a readonly database")
@@ -108,7 +115,12 @@
@Test
fun reusingConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
pool.useReaderConnection { initialConnection ->
pool.useReaderConnection { reusedConnection ->
@@ -127,7 +139,12 @@
@Test
fun reusingConnectionOnLaunch() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
pool.useReaderConnection { initialConnection ->
coroutineScope {
@@ -150,7 +167,12 @@
@Test
fun reusingConnectionOnAsync() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
pool.useReaderConnection { initialConnection ->
coroutineScope {
@@ -173,7 +195,12 @@
@Test
fun reusingConnectionWithContext() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
pool.useReaderConnection { initialConnection ->
withContext(Dispatchers.IO) {
@@ -194,7 +221,12 @@
@Test
fun failureToUpgradeConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useReaderConnection {
assertThat(
assertFailsWith<SQLiteException> {
@@ -208,7 +240,12 @@
@Test
fun cannotUseAlreadyRecycledConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var leakedConnection: PooledConnection? = null
pool.useReaderConnection {
leakedConnection = it
@@ -224,7 +261,12 @@
@Test
fun cannotUseAlreadyRecycledStatement() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var leakedRawStatement: SQLiteStatement? = null
pool.useReaderConnection { connection ->
connection.usePrepared("SELECT * FROM Pet") {
@@ -242,7 +284,12 @@
@Test
fun cannotUsedAlreadyClosedPool() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.close()
assertThat(
assertFailsWith<SQLiteException> {
@@ -254,7 +301,12 @@
@Test
fun idempotentPoolClosing() {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.close()
pool.close()
}
@@ -263,7 +315,12 @@
fun connectionUsedOnWrongCoroutine() = runTest {
val singleThreadContext = newFixedThreadPoolContext(1, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useReaderConnection { connection ->
launch(singleThreadContext) {
assertThat(
@@ -283,7 +340,12 @@
fun connectionUsedOnWrongCoroutineWithLeakedContext() = runTest {
val singleThreadContext = newSingleThreadContext("Test-Thread")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var leakedContext: CoroutineContext? = null
var leakedConnection: PooledConnection? = null
val job = launch(singleThreadContext) {
@@ -313,7 +375,12 @@
fun statementUsedOnWrongThread() = runTest {
val singleThreadContext = newFixedThreadPoolContext(1, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useReaderConnection { connection ->
connection.usePrepared("SELECT * FROM Pet") { statement ->
val expectedErrorMsg =
@@ -375,7 +442,12 @@
fun useStatementLocksConnection() = runTest {
val multiThreadContext = newFixedThreadPoolContext(2, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var count = 0
pool.useReaderConnection { connection ->
coroutineScope {
@@ -412,9 +484,10 @@
val connectionsOpened = atomic(0)
val actualDriver = setupDriver()
val driver = object : SQLiteDriver by actualDriver {
- override fun open() = actualDriver.open().also { connectionsOpened.incrementAndGet() }
+ override fun open(fileName: String) = actualDriver.open(fileName)
+ .also { connectionsOpened.incrementAndGet() }
}
- val pool = newSingleConnectionPool(driver)
+ val pool = newSingleConnectionPool(driver, ":memory:")
val jobs = mutableListOf<Job>()
repeat(5) {
val job1 = launch(multiThreadContext) {
@@ -443,9 +516,15 @@
val connectionsOpened = atomic(0)
val actualDriver = setupDriver()
val driver = object : SQLiteDriver by actualDriver {
- override fun open() = actualDriver.open().also { connectionsOpened.incrementAndGet() }
+ override fun open(fileName: String) = actualDriver.open(fileName)
+ .also { connectionsOpened.incrementAndGet() }
}
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 4, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 4,
+ maxNumOfWriters = 1
+ )
repeat(5) {
pool.useReaderConnection { connection ->
var count = 0
@@ -465,7 +544,12 @@
fun cancelCoroutineWaitingForConnection() = runTest {
val multiThreadContext = newFixedThreadPoolContext(2, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
val coroutineStartedMutex = Mutex(locked = true)
var acquiredSecondConnection = false
pool.useWriterConnection {
@@ -489,7 +573,12 @@
fun timeoutCoroutineWaitingForConnection() = runTest {
val multiThreadContext = newFixedThreadPoolContext(2, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
val coroutineStartedMutex = Mutex(locked = true)
var acquiredSecondConnection = false
val testContext = coroutineContext
@@ -521,7 +610,12 @@
@Test
fun timeoutWhileUsingConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
assertFailsWith<TimeoutCancellationException> {
pool.useWriterConnection {
withTimeout(0) {
@@ -541,7 +635,12 @@
@Test
fun errorUsingConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
assertThat(
assertFailsWith<IllegalStateException> {
pool.useWriterConnection {
@@ -572,12 +671,17 @@
val connectionsArr = arrayOfNulls<CloseAwareConnection>(4)
val actualDriver = setupDriver()
val driver = object : SQLiteDriver by actualDriver {
- override fun open() =
- CloseAwareConnection(actualDriver.open()).also {
+ override fun open(fileName: String) =
+ CloseAwareConnection(actualDriver.open(fileName)).also {
connectionsArr[connectionArrCount.getAndIncrement()] = it
}
}
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 4, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 4,
+ maxNumOfWriters = 1
+ )
val multiThreadContext = newFixedThreadPoolContext(4, "Test-Threads")
val jobs = mutableListOf<Job>()
val barrier = Mutex(locked = true)
@@ -606,7 +710,12 @@
@Test
fun rollbackTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -626,7 +735,12 @@
@Test
fun rollbackTransactionWithResult() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.execSQL("CREATE TEMP TABLE Cat (name)")
val name = "Pelusa"
@@ -647,7 +761,12 @@
@Test
fun rollbackTransactionDueToException() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
assertFailsWith<TestingRollbackException> {
connection.exclusiveTransaction {
@@ -669,7 +788,12 @@
@Test
fun rollbackNestedTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -692,7 +816,12 @@
@Test
fun rollbackParentTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -716,7 +845,12 @@
@Test
fun rollbackDeeplyNestedTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -745,7 +879,12 @@
@Test
fun rollbackNestedTransactionOnReusedConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -770,7 +909,12 @@
@Test
fun rollbackNestedTransactionDueToExceptionOnReusedConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -799,7 +943,12 @@
@Test
fun rollbackEvenWhenCatchingRollbackException() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -817,7 +966,12 @@
@Test
fun nestedWriteTransactionDoesNotUpgradeConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
var nestedTransactionBlockExecuted = false
pool.useReaderConnection { connection ->
connection.deferredTransaction<Unit> {
@@ -838,7 +992,12 @@
@Test
fun endNestedTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction<Unit> {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -861,7 +1020,12 @@
@Test
fun endNestedTransactionOnReusedConnection() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.exclusiveTransaction<Unit> {
execSQL("INSERT INTO Pet (id, name) VALUES (100, 'Pelusa')")
@@ -886,7 +1050,12 @@
@Test
fun explicitRollbackTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
assertThat(
assertFailsWith<SQLiteException> {
@@ -907,7 +1076,12 @@
@Test
fun explicitEndTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
assertThat(
assertFailsWith<SQLiteException> {
@@ -928,7 +1102,12 @@
@Test
fun unfinishedExplicitTransaction() = runTest {
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useWriterConnection { connection ->
connection.execSQL("BEGIN EXCLUSIVE TRANSACTION")
}
@@ -948,7 +1127,12 @@
fun parallelConnectionUsage() = runTest {
val multiThreadContext = newFixedThreadPoolContext(4, "Test-Threads")
val driver = setupDriver()
- val pool = newConnectionPool(driver = driver, maxNumOfReaders = 1, maxNumOfWriters = 1)
+ val pool = newConnectionPool(
+ driver = driver,
+ fileName = fileName,
+ maxNumOfReaders = 1,
+ maxNumOfWriters = 1
+ )
pool.useReaderConnection { connection ->
coroutineScope {
repeat(10) {
@@ -973,7 +1157,7 @@
}
private fun setupTestDatabase(driver: SQLiteDriver) {
- val connection = driver.open()
+ val connection = driver.open(fileName)
val compileOptions = buildList {
connection.prepare("PRAGMA compile_options").use {
while (it.step()) { add(it.getText(0)) }
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
index 2787fbfe..91d9268 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomConnectionManager.jvmNative.kt
@@ -32,11 +32,13 @@
if (configuration.name == null) {
// An in-memory database must use a single connection pool.
newSingleConnectionPool(
- driver = DriverWrapper(sqliteDriver)
+ driver = DriverWrapper(sqliteDriver),
+ fileName = ":memory:"
)
} else {
newConnectionPool(
driver = DriverWrapper(sqliteDriver),
+ fileName = configuration.name,
maxNumOfReaders = configuration.journalMode.getMaxNumberOfReaders(),
maxNumOfWriters = configuration.journalMode.getMaxNumberOfWriters()
)
diff --git a/room/room-runtime/src/jvmTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt b/room/room-runtime/src/jvmTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
index b8ac5e2..d6d4e54 100644
--- a/room/room-runtime/src/jvmTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
+++ b/room/room-runtime/src/jvmTest/kotlin/androidx/room/coroutines/BundledSQLiteConnectionPoolTest.kt
@@ -18,12 +18,14 @@
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlin.io.path.absolutePathString
import kotlin.io.path.createTempFile
class BundledSQLiteConnectionPoolTest : BaseConnectionPoolTest() {
+ override val fileName = createTempFile("test.db").also { it.toFile().deleteOnExit() }
+ .absolutePathString()
override fun getDriver(): SQLiteDriver {
- val tempFile = createTempFile("test.db").also { it.toFile().deleteOnExit() }
- return BundledSQLiteDriver(tempFile.toString())
+ return BundledSQLiteDriver()
}
}
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest.kt
index 78d8d57..676817d 100644
--- a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest.kt
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest.kt
@@ -27,7 +27,7 @@
val db = databaseBuilder(
name = "TestDatabase",
factory = { TestDatabase::class.instantiateImpl() }
- ).setDriver(NativeSQLiteDriver(":memory:")).build()
+ ).setDriver(NativeSQLiteDriver()).build()
// Assert that the db is built successfully.
assertThat(db).isInstanceOf<TestDatabase>()
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/coroutines/BundledSQLiteConnectionPoolTest.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/coroutines/BundledSQLiteConnectionPoolTest.kt
index 8124a7c..36a29a4 100644
--- a/room/room-runtime/src/nativeTest/kotlin/androidx.room/coroutines/BundledSQLiteConnectionPoolTest.kt
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/coroutines/BundledSQLiteConnectionPoolTest.kt
@@ -25,10 +25,10 @@
class BundledSQLiteConnectionPoolTest : BaseConnectionPoolTest() {
- private val filename = "/tmp/test-${Random.nextInt()}.db"
+ override val fileName = "/tmp/test-${Random.nextInt()}.db"
override fun getDriver(): SQLiteDriver {
- return BundledSQLiteDriver(filename)
+ return BundledSQLiteDriver()
}
@BeforeTest
@@ -42,8 +42,8 @@
}
private fun deleteDatabaseFile() {
- remove(filename)
- remove("$filename-wal")
- remove("$filename-shm")
+ remove(fileName)
+ remove("$fileName-wal")
+ remove("$fileName-shm")
}
}
diff --git a/room/room-testing/api/current.txt b/room/room-testing/api/current.txt
index becc3e0..8699971 100644
--- a/room/room-testing/api/current.txt
+++ b/room/room-testing/api/current.txt
@@ -2,12 +2,12 @@
package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
- ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder);
ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, String fileName, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
method public void closeWhenFinished(androidx.room.RoomDatabase db);
method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
method public final androidx.sqlite.SQLiteConnection createDatabase(int version);
diff --git a/room/room-testing/api/restricted_current.txt b/room/room-testing/api/restricted_current.txt
index becc3e0..8699971 100644
--- a/room/room-testing/api/restricted_current.txt
+++ b/room/room-testing/api/restricted_current.txt
@@ -2,12 +2,12 @@
package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
- ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs);
ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, Class<? extends androidx.room.RoomDatabase> databaseClass, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> specs, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder);
ctor @Deprecated public MigrationTestHelper(android.app.Instrumentation instrumentation, String assetsFolder, optional androidx.sqlite.db.SupportSQLiteOpenHelper.Factory openFactory);
+ ctor public MigrationTestHelper(android.app.Instrumentation instrumentation, String fileName, androidx.sqlite.SQLiteDriver driver, kotlin.reflect.KClass<? extends androidx.room.RoomDatabase> databaseClass, optional kotlin.jvm.functions.Function0<? extends androidx.room.RoomDatabase> databaseFactory, optional java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
method public void closeWhenFinished(androidx.room.RoomDatabase db);
method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase db);
method public final androidx.sqlite.SQLiteConnection createDatabase(int version);
diff --git a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
index 2afe271..cec9729 100644
--- a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
+++ b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
@@ -179,6 +179,7 @@
* be used.
*
* @param instrumentation The instrumentation instance.
+ * @param fileName Name of the database.
* @param driver A driver that opens connection to a file database. A driver that opens connections
* to an in-memory database would be meaningless.
* @param databaseClass The [androidx.room.Database] annotated class.
@@ -190,6 +191,7 @@
*/
constructor(
instrumentation: Instrumentation,
+ fileName: String,
driver: SQLiteDriver,
databaseClass: KClass<out RoomDatabase>,
databaseFactory: () -> RoomDatabase = {
@@ -207,6 +209,7 @@
this.delegate = SQLiteDriverMigrationTestHelper(
instrumentation = instrumentation,
assetsFolder = assetsFolder,
+ fileName = fileName,
driver = driver,
databaseClass = databaseClass,
databaseFactory = databaseFactory,
@@ -405,10 +408,11 @@
protected fun createDatabaseConfiguration(
container: RoomDatabase.MigrationContainer,
openFactory: SupportSQLiteOpenHelper.Factory?,
- sqliteDriver: SQLiteDriver?
+ sqliteDriver: SQLiteDriver?,
+ databaseFileName: String?
) = DatabaseConfiguration(
context = instrumentation.targetContext,
- name = null,
+ name = databaseFileName,
sqliteOpenHelperFactory = openFactory,
migrationContainer = container,
callbacks = null,
@@ -517,7 +521,7 @@
}
private fun createConfiguration(container: RoomDatabase.MigrationContainer) =
- createDatabaseConfiguration(container, openFactory, null)
+ createDatabaseConfiguration(container, openFactory, null, null)
private class SupportTestConnectionManager(
override val configuration: DatabaseConfiguration,
@@ -537,7 +541,7 @@
this.driverWrapper = DriverWrapper(supportDriver)
}
- override fun openConnection() = driverWrapper.open()
+ override fun openConnection() = driverWrapper.open(configuration.name ?: ":memory:")
inner class SupportOpenHelperCallback(
version: Int
@@ -574,6 +578,7 @@
private val driver: SQLiteDriver,
databaseClass: KClass<out RoomDatabase>,
databaseFactory: () -> RoomDatabase,
+ private val fileName: String,
private val autoMigrationSpecs: List<AutoMigrationSpec>
) : AndroidMigrationTestHelper(instrumentation, assetsFolder) {
@@ -607,5 +612,5 @@
}
private fun createConfiguration(container: RoomDatabase.MigrationContainer) =
- createDatabaseConfiguration(container, null, driver)
+ createDatabaseConfiguration(container, null, driver, fileName)
}
diff --git a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
index 4470798..64ccb0d 100644
--- a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
+++ b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
@@ -219,7 +219,7 @@
private val driverWrapper = DriverWrapper(requireNotNull(configuration.sqliteDriver))
- override fun openConnection() = driverWrapper.open()
+ override fun openConnection() = driverWrapper.open(configuration.name ?: ":memory:")
}
private sealed class TestOpenDelegate(
diff --git a/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt b/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt
index dc6f6e1..fa527a3 100644
--- a/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt
+++ b/room/room-testing/src/jvmMain/kotlin/androidx/room/testing/MigrationTestHelper.jvm.kt
@@ -76,6 +76,7 @@
* create and validate schemas.
*
* @param schemaDirectoryPath The schema directory where schema files are exported.
+ * @param databasePath Name of the database.
* @param driver A driver that opens connection to a file database. A driver that opens connections
* to an in-memory database would be meaningless.
* @param databaseClass The [androidx.room.Database] annotated class.
@@ -86,6 +87,7 @@
*/
actual class MigrationTestHelper(
private val schemaDirectoryPath: Path,
+ private val databasePath: Path,
private val driver: SQLiteDriver,
private val databaseClass: KClass<out RoomDatabase>,
databaseFactory: () -> RoomDatabase = {
@@ -164,7 +166,7 @@
private fun createDatabaseConfiguration(
container: RoomDatabase.MigrationContainer,
) = DatabaseConfiguration(
- name = null,
+ name = databasePath.toString(),
migrationContainer = container,
callbacks = null,
journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
diff --git a/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt b/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt
index 90747171..c0e69bc 100644
--- a/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt
+++ b/room/room-testing/src/nativeMain/kotlin/androidx/room/testing/MigrationTestHelper.native.kt
@@ -78,6 +78,7 @@
* create and validate schemas.
*
* @param schemaDirectoryPath The schema directory where schema files are exported.
+ * @param fileName Name of the database.
* @param driver A driver that opens connection to a file database. A driver that opens connections
* to an in-memory database would be meaningless.
* @param databaseClass The [androidx.room.Database] annotated class.
@@ -88,6 +89,7 @@
*/
actual class MigrationTestHelper(
private val schemaDirectoryPath: String,
+ private val fileName: String,
private val driver: SQLiteDriver,
private val databaseClass: KClass<out RoomDatabase>,
databaseFactory: () -> RoomDatabase,
@@ -162,7 +164,7 @@
private fun createDatabaseConfiguration(
container: RoomDatabase.MigrationContainer,
) = DatabaseConfiguration(
- name = null,
+ name = fileName,
migrationContainer = container,
callbacks = null,
journalMode = RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING,
diff --git a/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/AndroidSQLiteDriverTest.kt b/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/AndroidSQLiteDriverTest.kt
index 58236fc..88a0989 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/AndroidSQLiteDriverTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/AndroidSQLiteDriverTest.kt
@@ -25,7 +25,7 @@
override val driverType = TestDriverType.ANDROID_FRAMEWORK
override fun getDriver(): SQLiteDriver {
- return AndroidSQLiteDriver(":memory:")
+ return AndroidSQLiteDriver()
}
@Ignore // TODO(b/304297717): Align exception checking test with native.
diff --git a/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt b/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
index 3806d39..1772073 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/androidInstrumentedTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
@@ -24,6 +24,6 @@
override val driverType = TestDriverType.BUNDLED
override fun getDriver(): SQLiteDriver {
- return BundledSQLiteDriver(filename = ":memory:")
+ return BundledSQLiteDriver()
}
}
diff --git a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseBundledConformanceTest.kt b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseBundledConformanceTest.kt
index 49b362c..b4d96bb 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseBundledConformanceTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseBundledConformanceTest.kt
@@ -23,7 +23,7 @@
abstract class BaseBundledConformanceTest : BaseConformanceTest() {
@Test
fun readSQLiteVersion() {
- val connection = getDriver().open()
+ val connection = getDriver().open(":memory:")
try {
val version = connection.prepare("SELECT sqlite_version()").use {
it.step()
diff --git a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
index 92b183a..fe51e5b 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
@@ -40,7 +40,7 @@
@Test
fun openAndCloseConnection() {
val driver = getDriver()
- val connection = driver.open()
+ val connection = driver.open(":memory:")
try {
val version = connection.prepare("PRAGMA user_version").use { statement ->
statement.step()
@@ -260,7 +260,7 @@
@Test
fun useClosedConnection() {
val driver = getDriver()
- val connection = driver.open()
+ val connection = driver.open(":memory:")
connection.close()
assertFailsWith<SQLiteException> {
connection.prepare("SELECT * FROM Foo")
@@ -336,7 +336,7 @@
private inline fun testWithConnection(block: (SQLiteConnection) -> Unit) {
val driver = getDriver()
- val connection = driver.open()
+ val connection = driver.open(":memory:")
try {
block.invoke(connection)
} finally {
diff --git a/sqlite/integration-tests/driver-conformance-test/src/jvmTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt b/sqlite/integration-tests/driver-conformance-test/src/jvmTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
index 3806d39..1772073 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/jvmTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/jvmTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
@@ -24,6 +24,6 @@
override val driverType = TestDriverType.BUNDLED
override fun getDriver(): SQLiteDriver {
- return BundledSQLiteDriver(filename = ":memory:")
+ return BundledSQLiteDriver()
}
}
diff --git a/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt b/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
index 3806d39..1772073 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/BundledSQLiteDriverTest.kt
@@ -24,6 +24,6 @@
override val driverType = TestDriverType.BUNDLED
override fun getDriver(): SQLiteDriver {
- return BundledSQLiteDriver(filename = ":memory:")
+ return BundledSQLiteDriver()
}
}
diff --git a/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/NativeSQLiteDriverTest.kt b/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/NativeSQLiteDriverTest.kt
index 0ef3091..3d09510 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/NativeSQLiteDriverTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/nativeTest/kotlin/androidx/sqlite/driver/test/NativeSQLiteDriverTest.kt
@@ -24,6 +24,6 @@
override val driverType = TestDriverType.NATIVE_FRAMEWORK
override fun getDriver(): SQLiteDriver {
- return NativeSQLiteDriver(":memory:")
+ return NativeSQLiteDriver()
}
}
diff --git a/sqlite/sqlite-bundled/api/current.txt b/sqlite/sqlite-bundled/api/current.txt
index a09c7d3..a0f2650 100644
--- a/sqlite/sqlite-bundled/api/current.txt
+++ b/sqlite/sqlite-bundled/api/current.txt
@@ -2,8 +2,8 @@
package androidx.sqlite.driver.bundled {
public final class BundledSQLiteDriver implements androidx.sqlite.SQLiteDriver {
- ctor public BundledSQLiteDriver(String filename);
- method public androidx.sqlite.SQLiteConnection open();
+ ctor public BundledSQLiteDriver();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
}
diff --git a/sqlite/sqlite-bundled/api/restricted_current.txt b/sqlite/sqlite-bundled/api/restricted_current.txt
index a09c7d3..a0f2650 100644
--- a/sqlite/sqlite-bundled/api/restricted_current.txt
+++ b/sqlite/sqlite-bundled/api/restricted_current.txt
@@ -2,8 +2,8 @@
package androidx.sqlite.driver.bundled {
public final class BundledSQLiteDriver implements androidx.sqlite.SQLiteDriver {
- ctor public BundledSQLiteDriver(String filename);
- method public androidx.sqlite.SQLiteConnection open();
+ ctor public BundledSQLiteDriver();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
}
diff --git a/sqlite/sqlite-bundled/src/androidJvmCommonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.androidJvmCommon.kt b/sqlite/sqlite-bundled/src/androidJvmCommonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.androidJvmCommon.kt
index 8f89ece7..4b0ec4b 100644
--- a/sqlite/sqlite-bundled/src/androidJvmCommonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.androidJvmCommon.kt
+++ b/sqlite/sqlite-bundled/src/androidJvmCommonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.androidJvmCommon.kt
@@ -25,11 +25,9 @@
* library.
*/
// TODO(b/313895287): Explore usability of @FastNative and @CriticalNative for the external functions.
-actual class BundledSQLiteDriver actual constructor(
- private val filename: String
-) : SQLiteDriver {
- override fun open(): SQLiteConnection {
- val address = nativeOpen(filename)
+actual class BundledSQLiteDriver : SQLiteDriver {
+ override fun open(fileName: String): SQLiteConnection {
+ val address = nativeOpen(fileName)
return BundledSQLiteConnection(address)
}
diff --git a/sqlite/sqlite-bundled/src/commonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.kt b/sqlite/sqlite-bundled/src/commonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.kt
index b13f846..86e016cf 100644
--- a/sqlite/sqlite-bundled/src/commonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.kt
+++ b/sqlite/sqlite-bundled/src/commonMain/kotlin/androidx/sqlite/driver/bundled/BundledSQLiteDriver.kt
@@ -22,4 +22,4 @@
* A [SQLiteDriver] that uses a bundled version of SQLite included as a native component of this
* library.
*/
-expect class BundledSQLiteDriver(filename: String) : SQLiteDriver
+expect class BundledSQLiteDriver() : SQLiteDriver
diff --git a/sqlite/sqlite-framework/api/current.txt b/sqlite/sqlite-framework/api/current.txt
index 0b10ff9..cc962ad 100644
--- a/sqlite/sqlite-framework/api/current.txt
+++ b/sqlite/sqlite-framework/api/current.txt
@@ -11,8 +11,8 @@
package androidx.sqlite.driver {
public final class AndroidSQLiteDriver implements androidx.sqlite.SQLiteDriver {
- ctor public AndroidSQLiteDriver(String filename);
- method public androidx.sqlite.SQLiteConnection open();
+ ctor public AndroidSQLiteDriver();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
}
diff --git a/sqlite/sqlite-framework/api/restricted_current.txt b/sqlite/sqlite-framework/api/restricted_current.txt
index 0b10ff9..cc962ad 100644
--- a/sqlite/sqlite-framework/api/restricted_current.txt
+++ b/sqlite/sqlite-framework/api/restricted_current.txt
@@ -11,8 +11,8 @@
package androidx.sqlite.driver {
public final class AndroidSQLiteDriver implements androidx.sqlite.SQLiteDriver {
- ctor public AndroidSQLiteDriver(String filename);
- method public androidx.sqlite.SQLiteConnection open();
+ ctor public AndroidSQLiteDriver();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
}
diff --git a/sqlite/sqlite-framework/src/androidInstrumentedTest/kotlin/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt b/sqlite/sqlite-framework/src/androidInstrumentedTest/kotlin/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt
index 2c3b1c0..e1421dc 100644
--- a/sqlite/sqlite-framework/src/androidInstrumentedTest/kotlin/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt
+++ b/sqlite/sqlite-framework/src/androidInstrumentedTest/kotlin/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt
@@ -251,6 +251,29 @@
assertThat(openAttempts).isEqualTo(3)
}
+ @Test
+ fun allowDataLossOnRecovery_onOpenRecursive() {
+ var openHelper: FrameworkSQLiteOpenHelper? = null
+ val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+ override fun onCreate(db: SupportSQLiteDatabase) {}
+ override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ openHelper!!.writableDatabase
+ }
+ }
+ // Use an open helper with a bad callback that will recursively try to open the database
+ // again, this is a user error and it is expected for the exception with the recursive
+ // stacktrace to be thrown and not swallowed.
+ openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+ try {
+ openHelper.writableDatabase
+ fail("Database should have failed to open.")
+ } catch (ex: RuntimeException) {
+ // Expected
+ assertThat(ex.message).contains("getDatabase called recursively")
+ }
+ }
+
class EmptyCallback(version: Int = 1) : SupportSQLiteOpenHelper.Callback(version) {
override fun onCreate(db: SupportSQLiteDatabase) {
}
diff --git a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.android.kt b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.android.kt
index 9ed47c09..08ab1a76 100644
--- a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.android.kt
+++ b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.android.kt
@@ -170,7 +170,6 @@
return getWritableOrReadableDatabase(writable)
} catch (t: Throwable) {
// No good, just try again...
- super.close()
}
try {
// Wait before trying to open the DB, ideally enough to account for some slow I/O.
@@ -182,7 +181,6 @@
val openRetryError: Throwable = try {
return getWritableOrReadableDatabase(writable)
} catch (t: Throwable) {
- super.close()
t
}
if (openRetryError is CallbackException) {
diff --git a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteDriver.android.kt b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteDriver.android.kt
index 9e4e28e..c9cdfe9 100644
--- a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteDriver.android.kt
+++ b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteDriver.android.kt
@@ -24,11 +24,9 @@
* A [SQLiteDriver] implemented by [android.database] and that uses the Android's SDK SQLite
* APIs.
*/
-class AndroidSQLiteDriver(
- private val filename: String
-) : SQLiteDriver {
- override fun open(): SQLiteConnection {
- val database = SQLiteDatabase.openOrCreateDatabase(filename, null)
+class AndroidSQLiteDriver : SQLiteDriver {
+ override fun open(fileName: String): SQLiteConnection {
+ val database = SQLiteDatabase.openOrCreateDatabase(fileName, null)
return AndroidSQLiteConnection(database)
}
}
diff --git a/sqlite/sqlite-framework/src/nativeMain/kotlin/androidx/sqlite/driver/NativeSQLiteDriver.kt b/sqlite/sqlite-framework/src/nativeMain/kotlin/androidx/sqlite/driver/NativeSQLiteDriver.kt
index e9efb3e..4bd45ed 100644
--- a/sqlite/sqlite-framework/src/nativeMain/kotlin/androidx/sqlite/driver/NativeSQLiteDriver.kt
+++ b/sqlite/sqlite-framework/src/nativeMain/kotlin/androidx/sqlite/driver/NativeSQLiteDriver.kt
@@ -38,13 +38,11 @@
// (b/307917398) more open flags
// (b/304295573) busy handler registering
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
-class NativeSQLiteDriver(
- private val filename: String
-) : SQLiteDriver {
- override fun open(): SQLiteConnection = memScoped {
+class NativeSQLiteDriver : SQLiteDriver {
+ override fun open(fileName: String): SQLiteConnection = memScoped {
val dbPointer = allocPointerTo<sqlite3>()
val resultCode = sqlite3_open_v2(
- filename = filename,
+ filename = fileName,
ppDb = dbPointer.ptr,
flags = SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE,
zVfs = null
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index 9e31171..0ab19f2 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -7,7 +7,7 @@
}
public interface SQLiteDriver {
- method public androidx.sqlite.SQLiteConnection open();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
public final class SQLiteKt {
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index 9e31171..0ab19f2 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -7,7 +7,7 @@
}
public interface SQLiteDriver {
- method public androidx.sqlite.SQLiteConnection open();
+ method public androidx.sqlite.SQLiteConnection open(String fileName);
}
public final class SQLiteKt {
diff --git a/sqlite/sqlite/src/commonMain/kotlin/androidx/sqlite/SQLiteDriver.kt b/sqlite/sqlite/src/commonMain/kotlin/androidx/sqlite/SQLiteDriver.kt
index d4fdf82..b30c8f8 100644
--- a/sqlite/sqlite/src/commonMain/kotlin/androidx/sqlite/SQLiteDriver.kt
+++ b/sqlite/sqlite/src/commonMain/kotlin/androidx/sqlite/SQLiteDriver.kt
@@ -23,7 +23,8 @@
/**
* Opens a new database connection.
*
+ * @param fileName Name of the database file.
* @return the database connection.
*/
- fun open(): SQLiteConnection
+ fun open(fileName: String): SQLiteConnection
}
diff --git a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
index a9bcbad..c352f5f 100644
--- a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
+++ b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigatorDestinationBuilder.kt
@@ -23,6 +23,7 @@
import androidx.navigation.NavDestinationDsl
import androidx.navigation.NavGraphBuilder
import androidx.navigation.get
+import kotlin.reflect.KClass
/**
* Construct a new [TestNavigator.Destination]
@@ -37,6 +38,11 @@
/**
* Construct a new [TestNavigator.Destination]
*/
+inline fun NavGraphBuilder.test(route: KClass<*>) = test(route) {}
+
+/**
+ * Construct a new [TestNavigator.Destination]
+ */
@Suppress("DEPRECATION")
inline fun NavGraphBuilder.test(
@IdRes id: Int,
@@ -59,6 +65,16 @@
)
/**
+ * Construct a new [TestNavigator.Destination]
+ */
+inline fun NavGraphBuilder.test(
+ route: KClass<*>,
+ builder: TestNavigatorDestinationBuilder.() -> Unit
+) = destination(
+ TestNavigatorDestinationBuilder(provider[TestNavigator::class], route).apply(builder)
+)
+
+/**
* DSL for constructing a new [TestNavigator.Destination]
*/
@NavDestinationDsl
@@ -66,4 +82,5 @@
@Suppress("DEPRECATION")
constructor(navigator: TestNavigator, @IdRes id: Int = 0) : super(navigator, id)
constructor(navigator: TestNavigator, route: String) : super(navigator, route)
+ constructor(navigator: TestNavigator, route: KClass<*>) : super(navigator, route, null)
}
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 4982912..9ac320a 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -49,6 +49,8 @@
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation(libs.kotlinTest)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.robolectric)
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
@@ -56,6 +58,10 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.truth)
+
+ // Includes the wear-sdk jar
+ compileOnly files("../../wear_sdk/wear-sdk.jar")
+ testImplementation(files("../../wear_sdk/wear-sdk.jar"))
}
android {
diff --git a/wear/compose/compose-foundation/src/main/AndroidManifest.xml b/wear/compose/compose-foundation/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..601babe
--- /dev/null
+++ b/wear/compose/compose-foundation/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <uses-library
+ android:name="wear-sdk"
+ android:required="false"/>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
index 373784a..dd78f66 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
@@ -16,6 +16,28 @@
package androidx.wear.compose.foundation.rotary
+import android.content.Context
+import android.os.Build
+import android.provider.Settings
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalView
+import com.google.wear.input.WearHapticFeedbackConstants
+import kotlin.math.abs
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.withContext
+
/**
* Handles haptics for rotary usage
*/
@@ -36,3 +58,313 @@
*/
fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean)
}
+
+@Composable
+internal fun rememberRotaryHapticHandler(
+ scrollableState: ScrollableState,
+ hapticsEnabled: Boolean
+): RotaryHapticHandler =
+ if (hapticsEnabled) {
+ // TODO(b/319103162): Add platform haptics once AndroidX updates to Android VanillaIceCream
+ rememberCustomRotaryHapticHandler(scrollableState)
+ } else {
+ rememberDisabledRotaryHapticHandler()
+ }
+
+/**
+ * Remembers custom rotary haptic handler.
+ * @param scrollableState A scrollableState, used to determine whether the end of the scrollable
+ * was reached or not.
+ */
+@Composable
+private fun rememberCustomRotaryHapticHandler(
+ scrollableState: ScrollableState,
+): RotaryHapticHandler {
+ val hapticsProvider = rememberRotaryHapticFeedbackProvider()
+ // Channel to which haptic events will be sent
+ val hapticsChannel: Channel<RotaryHapticsType> = rememberHapticChannel()
+
+ // Throttling events within specified timeframe.
+ // Only first and last events will be received. Check [throttleLatest] function for more info.
+ val throttleThresholdMs: Long = 30
+ // A scroll threshold after which haptic is produced.
+ val hapticsThresholdPx: Long = 50
+
+ LaunchedEffect(hapticsChannel, throttleThresholdMs) {
+ hapticsChannel.receiveAsFlow()
+ .throttleLatest(throttleThresholdMs)
+ .collect { hapticType ->
+ // 'withContext' launches performHapticFeedback in a separate thread,
+ // as otherwise it produces a visible lag (b/219776664)
+ val currentTime = System.currentTimeMillis()
+ debugLog { "Haptics started" }
+ withContext(Dispatchers.Default) {
+ debugLog {
+ "Performing haptics, delay: " +
+ "${System.currentTimeMillis() - currentTime}"
+ }
+ hapticsProvider.performHapticFeedback(hapticType)
+ }
+ }
+ }
+ return remember(scrollableState, hapticsChannel, hapticsProvider) {
+ CustomRotaryHapticHandler(scrollableState, hapticsChannel, hapticsThresholdPx)
+ }
+}
+
+@Composable
+private fun rememberRotaryHapticFeedbackProvider(): RotaryHapticFeedbackProvider =
+ LocalView.current.let { view ->
+ remember {
+ val hapticConstants = getCustomRotaryConstants(view)
+ RotaryHapticFeedbackProvider(view, hapticConstants)
+ }
+ }
+
+@VisibleForTesting
+internal fun getCustomRotaryConstants(view: View): HapticConstants =
+ when {
+ // Order here is very important: We want to use WearSDK haptic constants for
+ // all devices having api 34 and up, but for Wear3.5 and Wear 4 constants should be
+ // different for Galaxy watches and other devices.
+ hasWearSDK() -> HapticConstants.WearSDKHapticConstants
+ isGalaxyWatch() -> HapticConstants.GalaxyWatchConstants
+ isWear3_5(view.context) -> HapticConstants.Wear3Point5RotaryHapticConstants
+ isWear4() -> HapticConstants.Wear4RotaryHapticConstants
+ else -> HapticConstants.DisabledHapticConstants
+ }
+
+@VisibleForTesting
+internal sealed class HapticConstants(
+ val scrollFocus: Int?,
+ val scrollTick: Int?,
+ val scrollLimit: Int?
+) {
+ /**
+ * Rotary haptic constants from WearSDK
+ */
+ object WearSDKHapticConstants : HapticConstants(
+ WearHapticFeedbackConstants.getScrollItemFocus(),
+ WearHapticFeedbackConstants.getScrollTick(),
+ WearHapticFeedbackConstants.getScrollLimit()
+ )
+
+ /**
+ * Rotary haptic constants for Galaxy Watch. These constants
+ * are used by Samsung for producing rotary haptics
+ */
+ object GalaxyWatchConstants : HapticConstants(
+ 102, 101, 50107
+ )
+
+ /**
+ * Hidden constants from HapticFeedbackConstants.java
+ * API 33, Wear 4
+ */
+ object Wear4RotaryHapticConstants : HapticConstants(
+ 19, 18, 20
+ )
+
+ /**
+ * Hidden constants from HapticFeedbackConstants.java
+ * API 30, Wear 3.5
+ */
+ object Wear3Point5RotaryHapticConstants : HapticConstants(
+ 10003, 10002, 10003
+ )
+
+ object DisabledHapticConstants : HapticConstants(
+ null, null, null
+ )
+}
+
+@Composable
+private fun rememberHapticChannel() =
+ remember {
+ Channel<RotaryHapticsType>(
+ capacity = 2,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+
+/**
+ * This class handles haptic feedback based
+ * on the [scrollableState], scrolled pixels and [hapticsThresholdPx].
+ * Haptic is not fired in this class, instead it's sent to [hapticsChannel]
+ * where it'll be performed later.
+ *
+ * @param scrollableState Haptic performed based on this state
+ * @param hapticsChannel Channel to which haptic events will be sent
+ * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
+ */
+private class CustomRotaryHapticHandler(
+ private val scrollableState: ScrollableState,
+ private val hapticsChannel: Channel<RotaryHapticsType>,
+ private val hapticsThresholdPx: Long = 50
+) : RotaryHapticHandler {
+
+ private var overscrollHapticTriggered = false
+ private var currScrollPosition = 0f
+ private var prevHapticsPosition = 0f
+
+ override fun handleScrollHaptic(event: UnifiedRotaryEvent) {
+ if (scrollableState.reachedTheLimit(event.deltaInPixels)) {
+ handleLimitHaptic(event, scrollableState.canScrollBackward)
+ } else {
+ overscrollHapticTriggered = false
+ currScrollPosition += event.deltaInPixels
+ val diff = abs(currScrollPosition - prevHapticsPosition)
+
+ if (diff >= hapticsThresholdPx) {
+ hapticsChannel.trySend(RotaryHapticsType.ScrollTick)
+ prevHapticsPosition = currScrollPosition
+ }
+ }
+ }
+
+ override fun handleSnapHaptic(event: UnifiedRotaryEvent) {
+ if (scrollableState.reachedTheLimit(event.deltaInPixels)) {
+ handleLimitHaptic(event, scrollableState.canScrollBackward)
+ } else {
+ overscrollHapticTriggered = false
+ hapticsChannel.trySend(RotaryHapticsType.ScrollItemFocus)
+ }
+ }
+
+ override fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean) {
+ if (!overscrollHapticTriggered) {
+ hapticsChannel.trySend(RotaryHapticsType.ScrollLimit)
+ overscrollHapticTriggered = true
+ }
+ }
+}
+
+/**
+ * Rotary haptic types
+ */
+@JvmInline
+@VisibleForTesting
+internal value class RotaryHapticsType(private val type: Int) {
+ companion object {
+
+ /**
+ * A scroll ticking haptic. Similar to texture haptic - performed each time when
+ * a scrollable content is scrolled by a certain distance
+ */
+ public val ScrollTick: RotaryHapticsType = RotaryHapticsType(1)
+
+ /**
+ * An item focus (snap) haptic. Performed when a scrollable content is snapped
+ * to a specific item.
+ */
+ public val ScrollItemFocus: RotaryHapticsType = RotaryHapticsType(2)
+
+ /**
+ * A limit(overscroll) haptic. Performed when a list reaches the limit
+ * (start or end) and can't scroll further
+ */
+ public val ScrollLimit: RotaryHapticsType = RotaryHapticsType(3)
+ }
+}
+
+/**
+ * Remember disabled haptics handler
+ */
+@Composable
+private fun rememberDisabledRotaryHapticHandler(): RotaryHapticHandler = remember {
+ object : RotaryHapticHandler {
+ override fun handleScrollHaptic(event: UnifiedRotaryEvent) {
+ // Do nothing
+ }
+
+ override fun handleSnapHaptic(event: UnifiedRotaryEvent) {
+ // Do nothing
+ }
+
+ override fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean) {
+ // Do nothing
+ }
+ }
+}
+
+/**
+ * Rotary haptic feedback
+ */
+private class RotaryHapticFeedbackProvider(
+ private val view: View,
+ private val hapticConstants: HapticConstants
+) {
+ fun performHapticFeedback(
+ type: RotaryHapticsType,
+ ) {
+ when (type) {
+ RotaryHapticsType.ScrollItemFocus -> {
+ hapticConstants.scrollFocus?.let { view.performHapticFeedback(it) }
+ }
+
+ RotaryHapticsType.ScrollTick -> {
+ hapticConstants.scrollTick?.let { view.performHapticFeedback(it) }
+ }
+
+ RotaryHapticsType.ScrollLimit -> {
+ hapticConstants.scrollLimit?.let { view.performHapticFeedback(it) }
+ }
+ }
+ }
+}
+
+private fun isGalaxyWatch(): Boolean =
+ Build.MANUFACTURER.contains("Samsung", ignoreCase = true) &&
+ Build.MODEL.matches("^SM-R.*\$".toRegex())
+
+private fun isWear3_5(context: Context): Boolean =
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.R && getWearPlatformMrNumber(context) >= 5
+
+private fun isWear4(): Boolean =
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU
+
+private fun hasWearSDK(): Boolean =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+
+private fun getWearPlatformMrNumber(context: Context): Int =
+ Settings.Global
+ .getString(context.contentResolver, WEAR_PLATFORM_MR_NUMBER)?.toIntOrNull() ?: 0
+
+private const val WEAR_PLATFORM_MR_NUMBER: String = "wear_platform_mr_number"
+
+private fun ScrollableState.reachedTheLimit(scrollDelta: Float): Boolean =
+ (scrollDelta > 0 && !canScrollForward) || (scrollDelta < 0 && !canScrollBackward)
+
+/**
+ * Debug logging that can be enabled.
+ */
+private const val DEBUG = false
+
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("RotaryHaptics: ${generateMsg()}")
+ }
+}
+
+/**
+ * Throttling events within specified timeframe. Only first and last events will be received.
+ *
+ * For example, a flow emits elements 1 to 30, with a 100ms delay between them:
+ * ```
+ * val flow = flow {
+ * for (i in 1..30) {
+ * delay(100)
+ * emit(i)
+ * }
+ * }
+ * ```
+ * With timeframe=1000 only those integers will be received: 1, 10, 20, 30 .
+ */
+@VisibleForTesting
+internal fun <T> Flow<T>.throttleLatest(timeframe: Long): Flow<T> =
+ flow {
+ conflate().collect {
+ emit(it)
+ delay(timeframe)
+ }
+ }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Rotary.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Rotary.kt
index a82086d..5de31b2 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Rotary.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Rotary.kt
@@ -28,14 +28,21 @@
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.rotary.RotaryInputModifierNode
import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.util.fastSumBy
import androidx.compose.ui.util.lerp
@@ -56,6 +63,43 @@
import kotlinx.coroutines.launch
/**
+ * Abstract class for setting rotary parameters.
+ * Has 2 implementations - [RotaryDefaults.scrollSpec] and [RotaryDefaults.snapSpec].
+ */
+// TODO(b/278705775): make it public once haptics and other code is merged.
+@ExperimentalWearFoundationApi
+internal abstract class RotarySpec internal constructor() {
+ internal abstract val rotaryHandler: RotaryHandler
+}
+
+/**
+ * A modifier which connects rotary events with scrollable.
+ *
+ * This modifier supports rotary scrolling and snapping.
+ * The behaviour is configured by the provided [RotarySpec]:
+ * either provide [RotaryDefaults.scrollSpec] for scrolling with/without fling
+ * or pass [RotaryDefaults.snapSpec] when snap is required.
+ *
+ * @param rotarySpec Specified [RotarySpec] for proper rotary handling.
+ * @param focusRequester Requests the focus for rotary input.
+ * @param reverseDirection Reverse the direction of scrolling if required. Should be aligned with
+ * Scrollable `reverseDirection` parameter.
+ */
+@ExperimentalWearFoundationApi
+// TODO(b/278705775): make it public once haptics and other code is merged.
+internal fun Modifier.rotary(
+ rotarySpec: RotarySpec,
+ focusRequester: FocusRequester,
+ reverseDirection: Boolean = false
+): Modifier =
+ rotaryHandler(
+ rotaryHandler = rotarySpec.rotaryHandler,
+ reverseDirection = reverseDirection,
+ )
+ .focusRequester(focusRequester)
+ .focusable()
+
+/**
* An adapter which connects scrollableState to a rotary input for snapping scroll actions.
*
* This interface defines the essential properties and methods required for a scrollable
@@ -64,7 +108,7 @@
*/
@ExperimentalWearFoundationApi
// TODO(b/278705775): make it public once haptics and other code is merged.
-/* public */ internal interface RotaryScrollAdapter {
+internal interface RotaryScrollableAdapter {
/**
* The scrollable state used for performing scroll actions in response to rotary events.
@@ -92,7 +136,6 @@
*/
fun currentItemOffset(): Float
- // TODO(b/326239879) Investigate and test whether this method can be removed.
/**
* The total number of items within the scrollable in [scrollableState]
*/
@@ -106,6 +149,93 @@
// TODO(b/278705775): make it public once haptics and other code is merged.
/* public */ internal object RotaryDefaults {
+ /**
+ * Implementation of the [RotarySpec] to define scrolling behaviour with or without fling.
+ * Should be set as a parameter of [rotary] modifier.
+ *
+ * If fling is not required [flingBehavior] should be set as null.
+ * Note: If [flingBehavior] is null, flinging will not happen and the scrollable content will
+ * stop scrolling immediately after the user stops interacting with rotary input.
+ *
+ * @param scrollableState Scrollable state which will be scrolled
+ * while receiving rotary events.
+ * @param flingBehavior An optional fling behavior, which controls flinging behavior
+ * with rotary. If null fling will not happen.
+ * @param hapticFeedbackEnabled Responsible for haptic feedback during rotary
+ * rotation. By default is true.
+ */
+ @Composable
+ fun scrollSpec(
+ scrollableState: ScrollableState,
+ flingBehavior: FlingBehavior? = ScrollableDefaults.flingBehavior(),
+ hapticFeedbackEnabled: Boolean = true
+ ): RotarySpec {
+ val isLowRes = isLowResInput()
+ val viewConfiguration = ViewConfiguration.get(LocalContext.current)
+ val rotaryHaptics: RotaryHapticHandler =
+ rememberRotaryHapticHandler(scrollableState, hapticFeedbackEnabled)
+
+ return object : RotarySpec() {
+ override val rotaryHandler =
+ flingHandler(
+ scrollableState,
+ rotaryHaptics,
+ flingBehavior,
+ isLowRes,
+ viewConfiguration
+ )
+ }
+ }
+
+ /**
+ * Implementation of the [RotarySpec] to define snap behavior. Should be set as
+ * a parameter of [rotary] modifier.
+ *
+ * @param rotaryScrollableAdapter A connection between scrollable entities and rotary events.
+ * @param snapOffset An optional offset to be applied when snapping the item.
+ * After the snap the snapped items offset will be [snapOffset].
+ * @param hapticFeedbackEnabled Responsible for haptic feedback during rotary
+ * rotation. By default is true.
+ */
+ @Composable
+ fun snapSpec(
+ rotaryScrollableAdapter: RotaryScrollableAdapter,
+ snapOffset: Int = SNAP_OFFSET,
+ hapticFeedbackEnabled: Boolean = true
+ ): RotarySpec {
+ val isLowRes = isLowResInput()
+ val rotaryHaptics: RotaryHapticHandler =
+ rememberRotaryHapticHandler(
+ rotaryScrollableAdapter.scrollableState,
+ hapticFeedbackEnabled
+ )
+
+ return remember(rotaryScrollableAdapter, rotaryHaptics, snapOffset, isLowRes) {
+ object : RotarySpec() {
+ override val rotaryHandler =
+ snapHandler(
+ rotaryScrollableAdapter,
+ rotaryHaptics,
+ snapOffset,
+ THRESHOLD_DIVIDER,
+ RESISTANCE_FACTOR,
+ isLowRes
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns whether the input is Low-res (a bezel) or high-res (a crown/rsb).
+ */
+ @Composable
+ private fun isLowResInput(): Boolean = LocalContext.current.packageManager
+ .hasSystemFeature("android.hardware.rotaryencoder.lowres")
+
+ private const val SNAP_OFFSET: Int = 0
+ private const val THRESHOLD_DIVIDER: Float = 1.5f
+ private const val RESISTANCE_FACTOR: Float = 3f
+
// These values represent the timeframe for a fling event. A bigger value is assigned
// to low-res input due to the lower frequency of low-res rotary events.
internal const val lowResFlingTimeframe: Long = 100L
@@ -116,9 +246,9 @@
* An implementation of rotary scroll adapter for ScalingLazyColumn
*/
@OptIn(ExperimentalWearFoundationApi::class)
-internal class ScalingLazyColumnRotaryScrollAdapter(
+internal class ScalingLazyColumnRotaryScrollableAdapter(
override val scrollableState: ScalingLazyListState
-) : RotaryScrollAdapter {
+) : RotaryScrollableAdapter {
/**
* Calculates the average item height by averaging the height of visible items.
@@ -189,7 +319,7 @@
* @return A snap implementation of [RotaryHandler] which is either suitable for low-res or
* high-res input.
*
- * @param rotaryScrollAdapter Implementation of [RotaryScrollAdapter], which connects
+ * @param rotaryScrollableAdapter Implementation of [RotaryScrollableAdapter], which connects
* scrollableState to a rotary input for snapping scroll actions.
* @param rotaryHaptics Implementation of [RotaryHapticHandler] which handles haptics
* for rotary usage
@@ -201,7 +331,7 @@
* @param isLowRes Whether the input is Low-res (a bezel) or high-res(a crown/rsb)
*/
private fun snapHandler(
- rotaryScrollAdapter: RotaryScrollAdapter,
+ rotaryScrollableAdapter: RotaryScrollableAdapter,
rotaryHaptics: RotaryHapticHandler,
snapOffset: Int,
maxThresholdDivider: Float,
@@ -213,7 +343,7 @@
rotaryHaptics = rotaryHaptics,
snapBehaviourFactory = {
RotarySnapHelper(
- rotaryScrollAdapter,
+ rotaryScrollableAdapter,
snapOffset,
)
}
@@ -225,24 +355,26 @@
thresholdBehaviorFactory = {
ThresholdBehavior(
maxThresholdDivider,
- averageItemSize = { rotaryScrollAdapter.averageItemSize() }
+ averageItemSize = { rotaryScrollableAdapter.averageItemSize() }
)
},
snapBehaviorFactory = {
RotarySnapHelper(
- rotaryScrollAdapter,
+ rotaryScrollableAdapter,
snapOffset,
)
},
scrollBehaviorFactory = {
- RotaryScrollBehavior(rotaryScrollAdapter.scrollableState)
+ RotaryScrollBehavior(rotaryScrollableAdapter.scrollableState)
}
)
}
}
/**
- * An abstract class for handling scroll events
+ * An abstract base class for handling scroll events. Has implementations for handling scroll
+ * with/without fling [RotaryScrollHandler] and for handling snap [LowResRotarySnapHandler],
+ * [HighResRotarySnapHandler].
*/
internal abstract class RotaryHandler {
@@ -327,10 +459,10 @@
* A helper class for snapping with rotary.
*/
internal class RotarySnapHelper(
- private val rotaryScrollAdapter: RotaryScrollAdapter,
+ private val rotaryScrollableAdapter: RotaryScrollableAdapter,
private val snapOffset: Int,
) {
- private var snapTarget: Int = rotaryScrollAdapter.currentItemIndex()
+ private var snapTarget: Int = rotaryScrollableAdapter.currentItemIndex()
private var sequentialSnap: Boolean = false
private var anim = AnimationState(0f)
@@ -355,10 +487,11 @@
if (sequentialSnap) {
snapTarget += moveForElements
} else {
- snapTarget = rotaryScrollAdapter.currentItemIndex() + moveForElements
+ snapTarget = rotaryScrollableAdapter.currentItemIndex() + moveForElements
}
snapTargetUpdated = true
- snapTarget = snapTarget.coerceIn(0 until rotaryScrollAdapter.totalItemsCount())
+ snapTarget = snapTarget
+ .coerceIn(0 until rotaryScrollableAdapter.totalItemsCount())
}
/**
@@ -366,13 +499,13 @@
*/
suspend fun snapToClosestItem() {
// Perform the snapping animation
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
+ rotaryScrollableAdapter.scrollableState.scroll(MutatePriority.UserInput) {
debugLog { "snap to the closest item" }
var prevPosition = 0f
// Create and execute the snap animation
AnimationState(0f).animateTo(
- targetValue = -rotaryScrollAdapter.currentItemOffset(),
+ targetValue = -rotaryScrollableAdapter.currentItemOffset(),
animationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing)
) {
val animDelta = value - prevPosition
@@ -380,7 +513,7 @@
prevPosition = value
}
// Update the snap target to ensure consistency
- snapTarget = rotaryScrollAdapter.currentItemIndex()
+ snapTarget = rotaryScrollableAdapter.currentItemIndex()
}
}
@@ -393,7 +526,7 @@
* Returns true if bottom edge was reached
*/
fun bottomEdgeReached(): Boolean =
- snapTarget >= rotaryScrollAdapter.totalItemsCount() - 1
+ snapTarget >= rotaryScrollableAdapter.totalItemsCount() - 1
/**
* Performs snapping to the specified in [updateSnapTarget] element
@@ -401,7 +534,7 @@
suspend fun snapToTargetItem() {
if (!sequentialSnap) anim = AnimationState(0f)
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
+ rotaryScrollableAdapter.scrollableState.scroll(MutatePriority.UserInput) {
// If snapTargetUpdated is true -means the target was updated so we
// need to do snap animation again
while (snapTargetUpdated) {
@@ -412,12 +545,12 @@
// First part of animation. Performing it until the target element centered.
while (continueFirstScroll) {
- latestCenterItem = rotaryScrollAdapter.currentItemIndex()
+ latestCenterItem = rotaryScrollableAdapter.currentItemIndex()
expectedDistance = expectedDistanceTo(snapTarget, snapOffset)
debugLog {
"expectedDistance = $expectedDistance, " +
"scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
+ "${rotaryScrollableAdapter.currentItemOffset()}"
}
continueFirstScroll = false
@@ -441,20 +574,22 @@
scrollBy(animDelta)
prevPosition = value
- if (latestCenterItem != rotaryScrollAdapter.currentItemIndex()) {
+ if (latestCenterItem != rotaryScrollableAdapter.currentItemIndex()) {
continueFirstScroll = true
cancelAnimation()
return@animateTo
}
- debugLog { "centerItemIndex = ${rotaryScrollAdapter.currentItemIndex()}" }
- if (rotaryScrollAdapter.currentItemIndex() == snapTarget) {
+ debugLog {
+ "centerItemIndex = ${rotaryScrollableAdapter.currentItemIndex()}"
+ }
+ if (rotaryScrollableAdapter.currentItemIndex() == snapTarget) {
debugLog { "Target is near the centre. Cancelling first animation" }
debugLog {
"scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
+ "${rotaryScrollableAdapter.currentItemOffset()}"
}
- expectedDistance = -rotaryScrollAdapter.currentItemOffset()
+ expectedDistance = -rotaryScrollableAdapter.currentItemOffset()
continueFirstScroll = false
cancelAnimation()
return@animateTo
@@ -487,28 +622,28 @@
}
private fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
- val averageSize = rotaryScrollAdapter.averageItemSize()
- val indexesDiff = index - rotaryScrollAdapter.currentItemIndex()
+ val averageSize = rotaryScrollableAdapter.averageItemSize()
+ val indexesDiff = index - rotaryScrollableAdapter.currentItemIndex()
debugLog { "Average size $averageSize" }
return (averageSize * indexesDiff) +
- targetScrollOffset - rotaryScrollAdapter.currentItemOffset()
+ targetScrollOffset - rotaryScrollableAdapter.currentItemOffset()
}
}
/**
* A modifier which handles rotary events.
- * It accepts ScrollHandler as the input - a class that handles the main scroll logic.
+ * It accepts [RotaryHandler] as the input - a class that handles the main scroll logic.
*/
internal fun Modifier.rotaryHandler(
- rotaryScrollHandler: RotaryHandler,
+ rotaryHandler: RotaryHandler,
reverseDirection: Boolean,
inspectorInfo: InspectorInfo.() -> Unit = debugInspectorInfo {
name = "rotaryHandler"
- properties["rotaryScrollHandler"] = rotaryScrollHandler
+ properties["rotaryHandler"] = rotaryHandler
properties["reverseDirection"] = reverseDirection
}
): Modifier = this then RotaryHandlerElement(
- rotaryScrollHandler,
+ rotaryHandler,
reverseDirection,
inspectorInfo
)
@@ -949,7 +1084,7 @@
// Smoothing factor for velocity readings
private val smoothingConstant: Float = 0.4f,
private val averageItemSize: () -> Float
- ) {
+) {
private val thresholdDividerEasing: Easing = CubicBezierEasing(0.5f, 0.0f, 0.5f, 1.0f)
private val rotaryVelocityTracker = RotaryVelocityTracker()
@@ -1023,18 +1158,18 @@
}
private data class RotaryHandlerElement(
- private val rotaryScrollHandler: RotaryHandler,
+ private val rotaryHandler: RotaryHandler,
private val reverseDirection: Boolean,
private val inspectorInfo: InspectorInfo.() -> Unit
) : ModifierNodeElement<RotaryInputNode>() {
override fun create(): RotaryInputNode = RotaryInputNode(
- rotaryScrollHandler,
+ rotaryHandler,
reverseDirection,
)
override fun update(node: RotaryInputNode) {
debugLog { "Update launched!" }
- node.rotaryScrollHandler = rotaryScrollHandler
+ node.rotaryHandler = rotaryHandler
node.reverseDirection = reverseDirection
}
@@ -1048,21 +1183,21 @@
other as RotaryHandlerElement
- if (rotaryScrollHandler != other.rotaryScrollHandler) return false
+ if (rotaryHandler != other.rotaryHandler) return false
if (reverseDirection != other.reverseDirection) return false
return true
}
override fun hashCode(): Int {
- var result = rotaryScrollHandler.hashCode()
+ var result = rotaryHandler.hashCode()
result = 31 * result + reverseDirection.hashCode()
return result
}
}
private class RotaryInputNode(
- var rotaryScrollHandler: RotaryHandler,
+ var rotaryHandler: RotaryHandler,
var reverseDirection: Boolean,
) : RotaryInputModifierNode, Modifier.Node() {
@@ -1077,7 +1212,7 @@
"Scroll event received: " +
"delta:${it.deltaInPixels}, timestamp:${it.timestamp}"
}
- rotaryScrollHandler.handleScrollEvent(this, it)
+ rotaryHandler.handleScrollEvent(this, it)
}
}
}
diff --git a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
new file mode 100644
index 0000000..da6eb40
--- /dev/null
+++ b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.rotary
+
+import android.R
+import android.app.Activity
+import android.provider.Settings
+import android.view.View
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
+
+@RunWith(JUnit4::class)
+class ThrottleLatestTest {
+ private lateinit var testChannel: Channel<RotaryHapticsType>
+
+ @Before
+ fun before() {
+ testChannel = Channel(
+ capacity = 10,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+
+ @Test
+ fun single_event_sent() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 1
+
+ launch {
+ testChannel.trySend(RotaryHapticsType.ScrollTick)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_events_sent_one_filtered() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 2
+
+ // Send 3 events, receive 2 because they fall into a single timeframe and only
+ // 1st and last items are returned
+ launch {
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 10)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_events_sent_none_filtered() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 3
+ // Sent 3 events, received 3 because delay between events is bigger than a timeframe
+ launch {
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 50)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_slow_and_five_fast() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 5
+ launch {
+ // Sent 3 events, received 3 because delay between events is bigger than a timeframe
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 50)
+ delay(50)
+ // Sent 5 events, received 2 (first and last) because delay between events
+ // was smaller than a timeframe
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 5, 5)
+ delay(5)
+ testChannel.close()
+ }
+
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ private suspend fun Channel<RotaryHapticsType>.sendEventsWithDelay(
+ event: RotaryHapticsType,
+ eventCount: Int,
+ delayMillis: Long
+ ) {
+ for (i in 0 until eventCount) {
+ trySend(event)
+ if (i < eventCount - 1) {
+ delay(delayMillis)
+ }
+ }
+ }
+}
+
+@RunWith(RobolectricTestRunner::class)
+class HapticsTest {
+ @Test
+ @Config(sdk = [33])
+ fun testPixelWatch1Wear4() {
+ ShadowBuild.setManufacturer("Google")
+ ShadowBuild.setModel("Google Pixel Watch")
+
+ assertEquals(HapticConstants.Wear4RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testPixelWatch1Wear35() {
+ ShadowBuild.setManufacturer("Google")
+ ShadowBuild.setModel("Google Pixel Watch")
+ Settings.Global.putString(
+ RuntimeEnvironment.getApplication().contentResolver,
+ "wear_platform_mr_number",
+ "5",
+ )
+
+ assertEquals(HapticConstants.Wear3Point5RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGenericWear4() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.Wear4RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testGenericWear35() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+ Settings.Global.putString(
+ RuntimeEnvironment.getApplication().contentResolver,
+ "wear_platform_mr_number",
+ "5",
+ )
+
+ assertEquals(HapticConstants.Wear3Point5RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testGenericWear3() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.DisabledHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [28])
+ fun testGenericWear2() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.DisabledHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGalaxyWatchClassic() {
+ ShadowBuild.setManufacturer("Samsung")
+ // Galaxy Watch4 Classic
+ ShadowBuild.setModel("SM-R890")
+
+ assertEquals(HapticConstants.GalaxyWatchConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGalaxyWatch() {
+ ShadowBuild.setManufacturer("Samsung")
+ // Galaxy Watch 5 Pro
+ ShadowBuild.setModel("SM-R925")
+
+ assertEquals(HapticConstants.GalaxyWatchConstants, getHapticConstants())
+ }
+
+ private fun getHapticConstants(): HapticConstants {
+ val activity = Robolectric.buildActivity(Activity::class.java).get()
+ val view = activity.findViewById<View>(R.id.content)
+
+ return getCustomRotaryConstants(view)
+ }
+}
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index ceb3cb9..2171106 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -45,7 +45,7 @@
api(libs.kotlinCoroutinesAndroid)
api(libs.guavaListenableFuture)
implementation("androidx.core:core:1.12.0")
- implementation(project(":concurrent:concurrent-futures-ktx"))
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha03")
implementation("androidx.room:room-runtime:2.6.1")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index bc47ebf..5af96fc 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -285,6 +285,7 @@
public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+ ctor public OneTimeWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass);
method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
}
@@ -342,6 +343,10 @@
ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+ ctor public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ ctor public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
method public androidx.work.PeriodicWorkRequest.Builder clearNextScheduleTimeOverride();
method public androidx.work.PeriodicWorkRequest.Builder setNextScheduleTimeOverride(long nextScheduleTimeOverrideMillis);
}
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index bc47ebf..5af96fc 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -285,6 +285,7 @@
public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+ ctor public OneTimeWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass);
method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
}
@@ -342,6 +343,10 @@
ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker?> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+ ctor public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ ctor public PeriodicWorkRequest.Builder(kotlin.reflect.KClass<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
method public androidx.work.PeriodicWorkRequest.Builder clearNextScheduleTimeOverride();
method public androidx.work.PeriodicWorkRequest.Builder setNextScheduleTimeOverride(long nextScheduleTimeOverrideMillis);
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index a453890..158a9b2 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -84,13 +84,13 @@
@MediumTest
fun constraintsUpdate() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true))
.build()
workManager.enqueue(oneTimeWorkRequest).result.await()
val requestId = oneTimeWorkRequest.id
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(requestId)
.build()
@@ -102,11 +102,11 @@
@Test
@MediumTest
fun updateRunningOneTimeWork() = runTest {
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(CompletableWorker::class.java).build()
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(CompletableWorker::class).build()
workManager.enqueue(oneTimeWorkRequest).result.await()
val worker = workerFactory.await(oneTimeWorkRequest.id) as CompletableWorker
// requiresCharging constraint is faked, so it will never be satisfied
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(oneTimeWorkRequest.id)
.setConstraints(Constraints(requiresCharging = true))
.build()
@@ -118,10 +118,10 @@
@Test
@MediumTest
fun failFinished() = runTest {
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.enqueue(oneTimeWorkRequest)
workManager.awaitSuccess(oneTimeWorkRequest.id)
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(oneTimeWorkRequest.id)
.setConstraints(Constraints(requiresCharging = true))
.build()
@@ -131,10 +131,10 @@
@Test
@MediumTest
fun failWorkDoesntExit() = runTest {
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.enqueue(oneTimeWorkRequest)
workManager.awaitSuccess(oneTimeWorkRequest.id)
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(UUID.randomUUID()).build()
try {
workManager.updateWork(updatedRequest).await()
@@ -147,13 +147,13 @@
@Test
@MediumTest
fun updateTags() = runTest {
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setInitialDelay(10, DAYS)
.addTag("previous")
.build()
workManager.enqueue(oneTimeWorkRequest).result.await()
- val updatedWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setInitialDelay(10, DAYS)
.setId(oneTimeWorkRequest.id)
.addTag("test")
@@ -175,7 +175,7 @@
@Test
@MediumTest
fun updateTagsWhileRunning() = runTest {
- val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val request = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true))
.addTag("original").build()
workManager.enqueue(request).result.await()
@@ -186,7 +186,7 @@
}
// will add startWork task to the serialTaskExecutor queue
greedyScheduler.onConstraintsStateChanged(request.workSpec, ConstraintsMet)
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true))
.setId(request.id)
.addTag("updated")
@@ -204,13 +204,13 @@
@MediumTest
fun updateWorkerClass() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true))
.build()
workManager.enqueue(oneTimeWorkRequest).result.await()
val requestId = oneTimeWorkRequest.id
- val updatedRequest = OneTimeWorkRequest.Builder(CompletableWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(CompletableWorker::class)
.setId(requestId)
.build()
@@ -225,7 +225,7 @@
@MediumTest
fun progressReset() = runTest {
// requiresCharging constraint is faked, so it will be controlled in the test
- val request = OneTimeWorkRequest.Builder(ProgressWorker::class.java)
+ val request = OneTimeWorkRequest.Builder(ProgressWorker::class)
.setConstraints(Constraints(requiresCharging = true))
.build()
workManager.enqueue(request).result.await()
@@ -239,7 +239,7 @@
assertThat(info.progress).isEqualTo(TEST_DATA)
- val updatedRequest = OneTimeWorkRequest.Builder(ProgressWorker::class.java)
+ val updatedRequest = OneTimeWorkRequest.Builder(ProgressWorker::class)
.setId(request.id)
.addTag("bla")
.build()
@@ -253,11 +253,11 @@
@MediumTest
fun continuationLeafUpdate() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val step1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val step1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true)).build()
- val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.beginWith(step1).then(step2).enqueue().result.await()
- val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(step2.id).addTag("updated").build()
assertThat(workManager.updateWork(updatedStep2).await()).isEqualTo(APPLIED_IMMEDIATELY)
val workInfo = workManager.getWorkInfoById(step2.id).await()!!
@@ -269,13 +269,13 @@
@MediumTest
fun continuationLeafRoot() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val step1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val step1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true)).build()
- val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.beginWith(step1).then(step2).enqueue().result.await()
val workInfo = workManager.getWorkInfoById(step2.id).await()!!
assertThat(workInfo.state).isEqualTo(State.BLOCKED)
- val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(step1.id).build()
assertThat(workManager.updateWork(updatedStep1).await()).isEqualTo(APPLIED_IMMEDIATELY)
workManager.awaitSuccess(step2.id)
@@ -285,12 +285,12 @@
@MediumTest
fun chainsViaExistingPolicyLeafUpdate() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val step1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val step1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true)).build()
- val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1)
workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2)
- val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedStep2 = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(step2.id).addTag("updated").build()
assertThat(workManager.updateWork(updatedStep2).await()).isEqualTo(APPLIED_IMMEDIATELY)
val workInfo = workManager.getWorkInfoById(step2.id).await()!!
@@ -302,14 +302,14 @@
@MediumTest
fun chainsViaExistingPolicyRootUpdate() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val step1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val step1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true)).build()
- val step2 = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val step2 = OneTimeWorkRequest.Builder(TestWorker::class).build()
workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step1)
workManager.enqueueUniqueWork("name", ExistingWorkPolicy.APPEND, step2)
val workInfo = workManager.getWorkInfoById(step2.id).await()!!
assertThat(workInfo.state).isEqualTo(State.BLOCKED)
- val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updatedStep1 = OneTimeWorkRequest.Builder(TestWorker::class)
.setId(step1.id).build()
assertThat(workManager.updateWork(updatedStep1).await()).isEqualTo(APPLIED_IMMEDIATELY)
workManager.awaitSuccess(step2.id)
@@ -319,12 +319,11 @@
@MediumTest
fun oneTimeWorkToPeriodic() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val request = OneTimeWorkRequest.Builder(TestWorker::class)
.setConstraints(Constraints(requiresCharging = true)).build()
workManager.enqueue(request).result.await()
val updatedRequest =
- PeriodicWorkRequest.Builder(TestWorker::class.java, 1, DAYS)
- .build()
+ PeriodicWorkRequest.Builder(TestWorker::class, 1, DAYS).build()
try {
workManager.updateWork(updatedRequest).await()
throw AssertionError()
@@ -337,11 +336,11 @@
@MediumTest
fun periodicWorkToOneTime() = runTest {
// requiresCharging constraint is faked, so it will never be satisfied
- val request = PeriodicWorkRequest.Builder(TestWorker::class.java, 1, DAYS)
+ val request = PeriodicWorkRequest.Builder(TestWorker::class, 1, DAYS)
.setConstraints(Constraints(requiresCharging = true))
.build()
workManager.enqueue(request).result.await()
- val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
+ val updatedRequest = OneTimeWorkRequest.Builder(TestWorker::class).build()
try {
workManager.updateWork(updatedRequest).await()
throw AssertionError()
@@ -353,11 +352,11 @@
@Test
@MediumTest
fun updateRunningPeriodicWorkRequest() = runTest {
- val request = PeriodicWorkRequest.Builder(CompletableWorker::class.java, 1, DAYS)
+ val request = PeriodicWorkRequest.Builder(CompletableWorker::class, 1, DAYS)
.addTag("original").build()
workManager.enqueue(request).result.await()
val updatedRequest =
- PeriodicWorkRequest.Builder(CompletableWorker::class.java, 1, DAYS)
+ PeriodicWorkRequest.Builder(CompletableWorker::class, 1, DAYS)
.setId(request.id).addTag("updated").build()
val worker = workerFactory.await(request.id) as CompletableWorker
assertThat(workManager.updateWork(updatedRequest).await()).isEqualTo(APPLIED_FOR_NEXT_RUN)
@@ -374,14 +373,14 @@
@MediumTest
@Test
fun updatePeriodicWorkAfterFirstPeriod() = runTest {
- val request = PeriodicWorkRequest.Builder(TestWorker::class.java, 1, DAYS)
+ val request = PeriodicWorkRequest.Builder(TestWorker::class, 1, DAYS)
.addTag("original").build()
workManager.enqueue(request).result.await()
workerFactory.await(request.id)
workManager.awaitWorkerEnqueued(request.id)
val updatedRequest =
- PeriodicWorkRequest.Builder(TestWorker::class.java, 1, DAYS)
+ PeriodicWorkRequest.Builder(TestWorker::class, 1, DAYS)
// requiresCharging constraint is faked, so it will never be satisfied
.setConstraints(Constraints(requiresCharging = true))
.setId(request.id).addTag("updated").build()
@@ -398,7 +397,7 @@
@MediumTest
@Test
fun updateRetryingOneTimeWork() = runTest {
- val request = OneTimeWorkRequest.Builder(RetryWorker::class.java)
+ val request = OneTimeWorkRequest.Builder(RetryWorker::class)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, DAYS)
.build()
workManager.enqueue(request)
@@ -414,7 +413,7 @@
request.stringId,
spec.lastEnqueueTime - delta
)
- val updated = OneTimeWorkRequest.Builder(TestWorker::class.java).setId(request.id)
+ val updated = OneTimeWorkRequest.Builder(TestWorker::class).setId(request.id)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, DAYS)
.build()
workManager.updateWork(updated).await()
@@ -426,7 +425,7 @@
@MediumTest
@Test
fun updateCorrectNextRunTime() = runTest {
- val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val request = OneTimeWorkRequest.Builder(TestWorker::class)
.setInitialDelay(10, TimeUnit.MINUTES).build()
val enqueueTime = System.currentTimeMillis()
workManager.enqueue(request).result.await()
@@ -434,7 +433,7 @@
request.stringId,
enqueueTime - TimeUnit.MINUTES.toMillis(5)
)
- val updated = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val updated = OneTimeWorkRequest.Builder(TestWorker::class)
.setInitialDelay(20, TimeUnit.MINUTES)
.setId(request.id)
.build()
@@ -451,10 +450,10 @@
@MediumTest
@SdkSuppress(minSdkVersion = 23, maxSdkVersion = 25)
fun testUpdatePeriodicWorker_preservesConstraintTrackingWorker() = runTest {
- val originRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+ val originRequest = OneTimeWorkRequest.Builder(TestWorker::class)
.setInitialDelay(10, HOURS).build()
workManager.enqueue(originRequest).result.await()
- val updateRequest = OneTimeWorkRequest.Builder(RetryWorker::class.java)
+ val updateRequest = OneTimeWorkRequest.Builder(RetryWorker::class)
.setId(originRequest.id).setInitialDelay(10, HOURS)
.setConstraints(Constraints(requiresBatteryNotLow = true))
.build()
@@ -468,12 +467,12 @@
@Test
@MediumTest
fun updateWorkerGeneration() = runTest {
- val oneTimeWorkRequest = OneTimeWorkRequest.Builder(WorkerWithParam::class.java)
+ val oneTimeWorkRequest = OneTimeWorkRequest.Builder(WorkerWithParam::class)
.setInitialDelay(10, DAYS)
.build()
workManager.enqueue(oneTimeWorkRequest).result.await()
- val updatedWorkRequest = OneTimeWorkRequest.Builder(WorkerWithParam::class.java)
+ val updatedWorkRequest = OneTimeWorkRequest.Builder(WorkerWithParam::class)
.setId(oneTimeWorkRequest.id)
.build()
@@ -492,13 +491,13 @@
val nextRunTimeMillis = HOURS.toMillis(10)
val request = PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setInitialDelay(2, DAYS).build()
workManager.enqueue(request).result.await()
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.setNextScheduleTimeOverride(nextRunTimeMillis)
.build()
@@ -516,13 +515,13 @@
val overrideScheduleTimeMillis2 = HOURS.toMillis(12)
val request = PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setInitialDelay(2, DAYS).build()
workManager.enqueue(request).result.await()
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.setNextScheduleTimeOverride(overrideScheduleTimeMillis)
.build()
@@ -532,7 +531,7 @@
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.setNextScheduleTimeOverride(overrideScheduleTimeMillis2)
.build()
@@ -549,7 +548,7 @@
val overrideScheduleTimeMillis = HOURS.toMillis(10)
val request = PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setBackoffCriteria(BackoffPolicy.LINEAR, HOURS.toMillis(1), HOURS)
.setInitialDelay(2, DAYS)
.build()
@@ -558,7 +557,7 @@
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.setNextScheduleTimeOverride(overrideScheduleTimeMillis)
.build()
@@ -578,7 +577,7 @@
val overrideScheduleTimeMillis = HOURS.toMillis(10)
val request = PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setInitialDelay(2, DAYS)
.setNextScheduleTimeOverride(overrideScheduleTimeMillis)
.build()
@@ -586,7 +585,7 @@
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.clearNextScheduleTimeOverride()
.setInitialDelay(2, DAYS)
@@ -610,13 +609,13 @@
testClock.currentTimeMillis = HOURS.toMillis(5)
val request = PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setInitialDelay(2, DAYS).build()
workManager.enqueue(request).result.await()
workManager.updateWork(
PeriodicWorkRequest.Builder(
- TestWorker::class.java, 1, DAYS
+ TestWorker::class, 1, DAYS
).setId(request.id)
.clearNextScheduleTimeOverride()
.build()
diff --git a/work/work-runtime/src/main/java/androidx/work/OneTimeWorkRequest.kt b/work/work-runtime/src/main/java/androidx/work/OneTimeWorkRequest.kt
index 37c4559..0f41311 100644
--- a/work/work-runtime/src/main/java/androidx/work/OneTimeWorkRequest.kt
+++ b/work/work-runtime/src/main/java/androidx/work/OneTimeWorkRequest.kt
@@ -36,6 +36,13 @@
WorkRequest.Builder<Builder, OneTimeWorkRequest>(workerClass) {
/**
+ * Creates a builder for [OneTimeWorkRequest]s.
+ *
+ * @param workerClass The [ListenableWorker] class to run for this work
+ */
+ constructor(workerClass: KClass<out ListenableWorker>) : this(workerClass.java)
+
+ /**
* Specifies the [InputMerger] class name for this [OneTimeWorkRequest].
*
* Before workers run, they receive input [Data] from their parent workers, as well as
diff --git a/work/work-runtime/src/main/java/androidx/work/PeriodicWorkRequest.kt b/work/work-runtime/src/main/java/androidx/work/PeriodicWorkRequest.kt
index 6fa29f7..200dd41 100644
--- a/work/work-runtime/src/main/java/androidx/work/PeriodicWorkRequest.kt
+++ b/work/work-runtime/src/main/java/androidx/work/PeriodicWorkRequest.kt
@@ -21,6 +21,7 @@
import androidx.work.impl.utils.toMillisCompat
import java.time.Duration
import java.util.concurrent.TimeUnit
+import kotlin.reflect.KClass
/**
* A [WorkRequest] for repeating work. This work executes multiple times until it is
@@ -88,6 +89,28 @@
* may run immediately, at the end of the period, or any time in between so long as the
* other conditions are satisfied at the time. The run time of the
* [PeriodicWorkRequest] can be restricted to a flex period within an interval (see
+ * `#Builder(Class, long, TimeUnit, long, TimeUnit)`).
+ *
+ * @param workerClass The [ListenableWorker] class to run for this work
+ * @param repeatInterval The repeat interval in `repeatIntervalTimeUnit` units
+ * @param repeatIntervalTimeUnit The [TimeUnit] for `repeatInterval`
+ */
+ constructor(
+ workerClass: KClass<out ListenableWorker>,
+ repeatInterval: Long,
+ repeatIntervalTimeUnit: TimeUnit
+ ) : super(workerClass.java) {
+ workSpec.setPeriodic(repeatIntervalTimeUnit.toMillis(repeatInterval))
+ }
+
+ /**
+ * Creates a [PeriodicWorkRequest] to run periodically once every interval period. The
+ * [PeriodicWorkRequest] is guaranteed to run exactly one time during this interval
+ * (subject to OS battery optimizations, such as doze mode). The repeat interval must
+ * be greater than or equal to [PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS]. It
+ * may run immediately, at the end of the period, or any time in between so long as the
+ * other conditions are satisfied at the time. The run time of the
+ * [PeriodicWorkRequest] can be restricted to a flex period within an interval (see
* `#Builder(Class, Duration, Duration)`).
*
* @param workerClass The [ListenableWorker] class to run for this work
@@ -102,6 +125,27 @@
}
/**
+ * Creates a [PeriodicWorkRequest] to run periodically once every interval period. The
+ * [PeriodicWorkRequest] is guaranteed to run exactly one time during this interval
+ * (subject to OS battery optimizations, such as doze mode). The repeat interval must
+ * be greater than or equal to [PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS]. It
+ * may run immediately, at the end of the period, or any time in between so long as the
+ * other conditions are satisfied at the time. The run time of the
+ * [PeriodicWorkRequest] can be restricted to a flex period within an interval (see
+ * `#Builder(Class, Duration, Duration)`).
+ *
+ * @param workerClass The [ListenableWorker] class to run for this work
+ * @param repeatInterval The repeat interval
+ */
+ @RequiresApi(26)
+ constructor(
+ workerClass: KClass<out ListenableWorker>,
+ repeatInterval: Duration
+ ) : super(workerClass.java) {
+ workSpec.setPeriodic(repeatInterval.toMillisCompat())
+ }
+
+ /**
* Creates a [PeriodicWorkRequest] to run periodically once within the
* **flex period** of every interval period. See diagram below. The flex
* period begins at `repeatInterval - flexInterval` to the end of the interval.
@@ -142,6 +186,40 @@
* The repeat interval must be greater than or equal to
* [PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS] and the flex interval must
* be greater than or equal to [PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS].
+ * ```
+ * [_____before flex_____|_____flex_____][_____before flex_____|_____flex_____]...
+ * [___cannot run work___|_can run work_][___cannot run work___|_can run work_]...
+ * \____________________________________/\____________________________________/...
+ * interval 1 interval 2 ...(repeat)
+ * ```
+ *
+ * @param workerClass The [ListenableWorker] class to run for this work
+ * @param repeatInterval The repeat interval in `repeatIntervalTimeUnit` units
+ * @param repeatIntervalTimeUnit The [TimeUnit] for `repeatInterval`
+ * @param flexInterval The duration in `flexIntervalTimeUnit` units for which this
+ * work repeats from the end of the `repeatInterval`
+ * @param flexIntervalTimeUnit The [TimeUnit] for `flexInterval`
+ */
+ constructor(
+ workerClass: KClass<out ListenableWorker>,
+ repeatInterval: Long,
+ repeatIntervalTimeUnit: TimeUnit,
+ flexInterval: Long,
+ flexIntervalTimeUnit: TimeUnit
+ ) : super(workerClass.java) {
+ workSpec.setPeriodic(
+ repeatIntervalTimeUnit.toMillis(repeatInterval),
+ flexIntervalTimeUnit.toMillis(flexInterval)
+ )
+ }
+
+ /**
+ * Creates a [PeriodicWorkRequest] to run periodically once within the
+ * **flex period** of every interval period. See diagram below. The flex
+ * period begins at `repeatInterval - flexInterval` to the end of the interval.
+ * The repeat interval must be greater than or equal to
+ * [PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS] and the flex interval must
+ * be greater than or equal to [PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS].
*
* ```
* [_____before flex_____|_____flex_____][_____before flex_____|_____flex_____]...
@@ -165,6 +243,35 @@
}
/**
+ * Creates a [PeriodicWorkRequest] to run periodically once within the
+ * **flex period** of every interval period. See diagram below. The flex
+ * period begins at `repeatInterval - flexInterval` to the end of the interval.
+ * The repeat interval must be greater than or equal to
+ * [PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS] and the flex interval must
+ * be greater than or equal to [PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS].
+ *
+ * ```
+ * [_____before flex_____|_____flex_____][_____before flex_____|_____flex_____]...
+ * [___cannot run work___|_can run work_][___cannot run work___|_can run work_]...
+ * \____________________________________/\____________________________________/...
+ * interval 1 interval 2 ...(repeat)
+ * ```
+ *
+ * @param workerClass The [ListenableWorker] class to run for this work
+ * @param repeatInterval The repeat interval
+ * @param flexInterval The duration in for which this work repeats from the end of the
+ * `repeatInterval`
+ */
+ @RequiresApi(26)
+ constructor(
+ workerClass: KClass<out ListenableWorker>,
+ repeatInterval: Duration,
+ flexInterval: Duration
+ ) : super(workerClass.java) {
+ workSpec.setPeriodic(repeatInterval.toMillisCompat(), flexInterval.toMillisCompat())
+ }
+
+ /**
* Overrides the next time this work is scheduled to run.
*
* Calling this method sets a specific time at which the work will be scheduled to run next,