Merge changes I842cb2e0,I476e3c43 into androidx-main
* changes:
Cache field access on internal sets and maps in runtime
Use IdentityArraySet to store snapshot invalidations
diff --git a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
index 065f82d..081499e 100644
--- a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
+++ b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
@@ -24,7 +24,8 @@
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
import androidx.appactions.interaction.capabilities.core.values.Message
@@ -70,7 +71,7 @@
class CapabilityBuilder :
CapabilityBuilderBase<
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
- >(ACTION_SPEC) {
+ >(ACTION_SPEC) {
override fun build(): ActionCapability {
super.setProperty(Property.Builder().build())
// TODO(b/268369632): No-op remove empty property builder after Property is removed.
@@ -80,9 +81,10 @@
}
// TODO(b/268369632): Remove Property from public capability APIs.
- class Property internal constructor(
+ class Property
+ internal constructor(
val recipient: SimpleProperty?,
- val messageText: StringProperty?
+ val messageText: TypeProperty<StringValue>?
) {
override fun toString(): String {
return "Property(recipient=$recipient, messageText=$messageText)"
@@ -108,22 +110,22 @@
class Builder {
private var recipient: SimpleProperty? = null
- private var messageText: StringProperty? = null
+ private var messageText: TypeProperty<StringValue>? = null
- fun setRecipient(recipient: SimpleProperty): Builder =
- apply { this.recipient = recipient }
+ fun setRecipient(recipient: SimpleProperty): Builder = apply {
+ this.recipient = recipient
+ }
- fun setMessageText(messageText: StringProperty): Builder =
- apply { this.messageText = messageText }
+ fun setMessageText(messageText: TypeProperty<StringValue>): Builder = apply {
+ this.messageText = messageText
+ }
fun build(): Property = Property(recipient, messageText)
}
}
- class Argument internal constructor(
- val recipientList: List<Recipient>,
- val messageText: String?
- ) {
+ class Argument
+ internal constructor(val recipientList: List<Recipient>, val messageText: String?) {
override fun toString(): String {
return "Argument(recipient=$recipientList, messageTextList=$messageText)"
}
@@ -150,20 +152,20 @@
private var recipientList: List<Recipient> = mutableListOf()
private var messageText: String? = null
- fun setRecipientList(recipientList: List<Recipient>): Builder =
- apply { this.recipientList = recipientList }
+ fun setRecipientList(recipientList: List<Recipient>): Builder = apply {
+ this.recipientList = recipientList
+ }
- fun setMessageText(messageTextList: String): Builder =
- apply { this.messageText = messageTextList }
+ fun setMessageText(messageTextList: String): Builder = apply {
+ this.messageText = messageTextList
+ }
override fun build(): Argument = Argument(recipientList, messageText)
}
}
- class Output internal constructor(
- val message: Message?,
- val executionStatus: ExecutionStatus?
- ) {
+ class Output
+ internal constructor(val message: Message?, val executionStatus: ExecutionStatus?) {
override fun toString(): String {
return "Output(call=$message, executionStatus=$executionStatus)"
}
@@ -190,9 +192,7 @@
private var message: Message? = null
private var executionStatus: ExecutionStatus? = null
- fun setMessage(message: Message): Builder = apply {
- this.message = message
- }
+ fun setMessage(message: Message): Builder = apply { this.message = message }
fun setExecutionStatus(executionStatus: ExecutionStatus): Builder = apply {
this.executionStatus = executionStatus
@@ -225,9 +225,7 @@
val value: Value = Value.newBuilder().setStringValue(status).build()
return ParamValue.newBuilder()
.setStructValue(
- Struct.newBuilder()
- .putFields(TypeConverters.FIELD_NAME_TYPE, value)
- .build()
+ Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build()
)
.build()
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
index 38b03d98..aed1277 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
@@ -21,8 +21,7 @@
import androidx.annotation.NonNull;
import androidx.appactions.interaction.capabilities.core.properties.ParamProperty;
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty.PossibleValue;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
import androidx.appactions.interaction.proto.Entity;
@@ -35,16 +34,6 @@
private PropertyConverter() {}
- /** Create IntentParameter proto from a StringProperty. */
- @NonNull
- public static IntentParameter getIntentParameter(
- @NonNull String paramName, @NonNull StringProperty property) {
- IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
- extractPossibleValues(property, PropertyConverter::possibleValueToProto).stream()
- .forEach(builder::addPossibleEntities);
- return builder.build();
- }
-
/** Create IntentParameter proto from a SimpleProperty. */
@NonNull
public static IntentParameter getIntentParameter(
@@ -93,11 +82,11 @@
}
/**
- * Converts a capabilities library StringProperty.PossibleValue to a appactions Entity proto .
+ * Converts a capabilities library [PossibleStringValue] to a appactions Entity proto .
*/
@NonNull
- public static Entity possibleValueToProto(@NonNull PossibleValue possibleValue) {
- return androidx.appactions.interaction.proto.Entity.newBuilder()
+ public static Entity stringValueToProto(@NonNull StringValue possibleValue) {
+ return Entity.newBuilder()
.setIdentifier(possibleValue.getName())
.setName(possibleValue.getName())
.addAllAlternateNames(possibleValue.getAlternateNames())
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt
new file mode 100644
index 0000000..e0990c1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.impl.converters
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException
+import androidx.appactions.interaction.protobuf.Struct
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class UnionTypeSpec<T : Any> internal constructor(
+ private val bindings: List<MemberBinding<T, *>>,
+) : TypeSpec<T> {
+ internal class MemberBinding<T, M>(
+ val memberGetter: (T) -> M?,
+ val ctor: (M) -> T,
+ val typeSpec: TypeSpec<M>,
+ ) {
+ fun tryDeserialize(struct: Struct): T {
+ return ctor(typeSpec.fromStruct(struct))
+ }
+
+ fun trySerialize(obj: T): Struct? {
+ return memberGetter(obj)?.let { typeSpec.toStruct(it) }
+ }
+ }
+
+ override fun fromStruct(struct: Struct): T {
+ for (binding in bindings) {
+ try {
+ return binding.tryDeserialize(struct)
+ } catch (e: StructConversionException) {
+ continue
+ }
+ }
+ throw StructConversionException("failed to deserialize union type")
+ }
+
+ override fun toStruct(obj: T): Struct {
+ for (binding in bindings) {
+ binding.trySerialize(obj)?.let {
+ return it
+ }
+ }
+ throw StructConversionException("failed to serialize union type")
+ }
+
+ class Builder<T : Any> {
+ private val bindings = mutableListOf<MemberBinding<T, *>>()
+
+ fun <M> bindMemberType(
+ memberGetter: (T) -> M?,
+ ctor: (M) -> T,
+ typeSpec: TypeSpec<M>,
+ ) = apply {
+ bindings.add(
+ MemberBinding(
+ memberGetter,
+ ctor,
+ typeSpec,
+ ),
+ )
+ }
+
+ fun build() = UnionTypeSpec<T>(bindings.toList())
+ }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
index 2bc8502..64268bd 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
@@ -26,7 +26,7 @@
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
import androidx.appactions.interaction.capabilities.core.impl.spec.ParamBinding.ArgumentSetter;
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import androidx.appactions.interaction.capabilities.core.values.EntityValue;
import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
@@ -437,15 +437,17 @@
* Binds an optional string parameter.
*
* @param paramName the BII slot name of this parameter.
- * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
- * {@code PropertyT} instance
+ * @param propertyGetter a function that returns a {@code
+ * Optional<TypeProperty<PossibleStringValue>>} given a {@code PropertyT} instance
* @param paramConsumer a function that accepts a String into the argument builder.
*/
@NonNull
public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
bindOptionalStringParameter(
@NonNull String paramName,
- @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+ @NonNull
+ Function<? super PropertyT, Optional<TypeProperty<StringValue>>>
+ propertyGetter,
@NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
return bindParameter(
paramName,
@@ -455,7 +457,9 @@
.map(
stringProperty ->
PropertyConverter.getIntentParameter(
- paramName, stringProperty)),
+ paramName,
+ stringProperty,
+ PropertyConverter::stringValueToProto)),
(argBuilder, paramList) -> {
if (!paramList.isEmpty()) {
paramConsumer.accept(
@@ -470,15 +474,15 @@
* Binds an required string parameter.
*
* @param paramName the BII slot name of this parameter.
- * @param propertyGetter a function that returns a {@code StringProperty} given a {@code
- * PropertyT} instance
+ * @param propertyGetter a function that returns a {@code TypeProperty<PossibleStringValue>}
+ * given a {@code PropertyT} instance
* @param paramConsumer a function that accepts a String into the argument builder.
*/
@NonNull
public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
bindRequiredStringParameter(
@NonNull String paramName,
- @NonNull Function<? super PropertyT, StringProperty> propertyGetter,
+ @NonNull Function<? super PropertyT, TypeProperty<StringValue>> propertyGetter,
@NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
return bindOptionalStringParameter(
paramName, property -> Optional.of(propertyGetter.apply(property)), paramConsumer);
@@ -488,8 +492,8 @@
* Binds an repeated string parameter.
*
* @param paramName the BII slot name of this parameter.
- * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
- * {@code PropertyT} instance
+ * @param propertyGetter a function that returns a {@code
+ * Optional<TypeProperty<PossibleStringValue>>} given a {@code PropertyT} instance
* @param paramConsumer a function that accepts a {@code List<String>} into the argument
* builder.
*/
@@ -497,7 +501,9 @@
public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
bindRepeatedStringParameter(
@NonNull String paramName,
- @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+ @NonNull
+ Function<? super PropertyT, Optional<TypeProperty<StringValue>>>
+ propertyGetter,
@NonNull BiConsumer<? super ArgumentBuilderT, List<String>> paramConsumer) {
return bindParameter(
paramName,
@@ -507,7 +513,9 @@
.map(
stringProperty ->
PropertyConverter.getIntentParameter(
- paramName, stringProperty)),
+ paramName,
+ stringProperty,
+ PropertyConverter::stringValueToProto)),
(argBuilder, paramList) -> {
if (!paramList.isEmpty()) {
paramConsumer.accept(
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
index 4b04e11..4db410c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
@@ -17,7 +17,7 @@
package androidx.appactions.interaction.capabilities.core.properties
/**
- * Entities are used when defining ActionCapability for defining possible values for ParamProperty.
+ * Entities are used defining possible values for [ParamProperty].
*/
class Entity internal constructor(
val id: String?,
@@ -55,9 +55,9 @@
/** Builds and returns an Entity. */
fun build() = Entity(
id,
- requireNotNull(name, {
+ requireNotNull(name) {
"setName must be called before build"
- }),
+ },
alternateNames,
)
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt
deleted file mode 100644
index 5d211e9..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.properties
-
-/** The property which describes a string parameter for {@code ActionCapability}. */
-class StringProperty internal constructor(
- override val possibleValues: List<StringProperty.PossibleValue>,
- override val isRequired: Boolean,
- override val isValueMatchRequired: Boolean,
- override val isProhibited: Boolean,
-) : ParamProperty<StringProperty.PossibleValue> {
- /** Represents a single possible value for StringProperty. */
- class PossibleValue internal constructor(
- val name: String,
- val alternateNames: List<String>,
- ) {
- companion object {
- @JvmStatic
- fun of(name: String, vararg alternateNames: String) = PossibleValue(
- name,
- alternateNames.toList(),
- )
- }
- }
-
- /** Builder for {@link StringProperty}. */
- class Builder {
- private val possibleValues = mutableListOf<PossibleValue>()
- private var isRequired = false
- private var isValueMatchRequired = false
- private var isProhibited = false
-
- /**
- * Adds a possible string value for this property.
- *
- * @param name the possible string value.
- * @param alternateNames the alternate names for this value.
- */
- fun addPossibleValue(name: String, vararg alternateNames: String) = apply {
- possibleValues.add(PossibleValue.of(name, *alternateNames))
- }
-
- /** Sets whether or not this property requires a value for fulfillment. */
- fun setRequired(isRequired: Boolean) = apply {
- this.isRequired = isRequired
- }
-
- /**
- * Sets whether or not this property requires that the value for this property must match
- * one of
- * the string values in the defined possible values.
- */
- fun setValueMatchRequired(isValueMatchRequired: Boolean) = apply {
- this.isValueMatchRequired = isValueMatchRequired
- }
-
- /**
- * Sets whether the String property is prohibited in the response.
- *
- * @param isProhibited Whether this property is prohibited in the response.
- */
- fun setProhibited(isProhibited: Boolean) = apply {
- this.isProhibited = isProhibited
- }
-
- /** Builds the property for this string parameter. */
- fun build() =
- StringProperty(
- possibleValues.toList(),
- isRequired,
- isValueMatchRequired,
- isProhibited,
- )
- }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt
new file mode 100644
index 0000000..fbe5bef
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.core.properties
+
+/**
+ * One of the possible possible values for [ParamProperty].
+ */
+class StringValue internal constructor(
+ val name: String,
+ val alternateNames: List<String>,
+) {
+ companion object {
+ @JvmStatic
+ fun of(name: String, vararg alternateNames: String) = StringValue(
+ name,
+ alternateNames.toList(),
+ )
+ }
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
deleted file mode 100644
index 7954fa2..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task.impl;
-
-import androidx.annotation.NonNull;
-import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
-import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter;
-import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
-import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver;
-import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
-import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
-import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver;
-import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
-import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
-import androidx.appactions.interaction.capabilities.core.task.ValueListener;
-import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException;
-import androidx.appactions.interaction.capabilities.core.values.SearchAction;
-import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.auto.value.AutoOneOf;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-
-/**
- * A wrapper around all types of slot resolvers (value listeners + disambig resolvers).
- *
- * <p>This allows one type of resolver to be bound for each slot, and abstracts the details of the
- * individual resolvers. It is also the place where repeated fields are handled.
- *
- * @param <ValueTypeT>
- */
-@AutoOneOf(GenericResolverInternal.Kind.class)
-public abstract class GenericResolverInternal<ValueTypeT> {
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListener(
- @NonNull ValueListener<ValueTypeT> valueListener) {
- return AutoOneOf_GenericResolverInternal.value(valueListener);
- }
-
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListListener(
- @NonNull ValueListener<List<ValueTypeT>> valueListListener) {
- return AutoOneOf_GenericResolverInternal.valueList(valueListListener);
- }
-
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityResolver(
- @NonNull AppEntityResolver<ValueTypeT> appEntityResolver) {
- return AutoOneOf_GenericResolverInternal.appEntityResolver(appEntityResolver);
- }
-
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityListResolver(
- @NonNull AppEntityListResolver<ValueTypeT> appEntityListResolver) {
- return AutoOneOf_GenericResolverInternal.appEntityListResolver(appEntityListResolver);
- }
-
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryResolver(
- @NonNull InventoryResolver<ValueTypeT> inventoryResolver) {
- return AutoOneOf_GenericResolverInternal.inventoryResolver(inventoryResolver);
- }
-
- @NonNull
- public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryListResolver(
- @NonNull InventoryListResolver<ValueTypeT> inventoryListResolverListener) {
- return AutoOneOf_GenericResolverInternal.inventoryListResolver(
- inventoryListResolverListener);
- }
-
- /** Returns the Kind of this resolver */
- @NonNull
- public abstract Kind getKind();
-
- abstract ValueListener<ValueTypeT> value();
-
- abstract ValueListener<List<ValueTypeT>> valueList();
-
- abstract AppEntityResolver<ValueTypeT> appEntityResolver();
-
- abstract AppEntityListResolver<ValueTypeT> appEntityListResolver();
-
- abstract InventoryResolver<ValueTypeT> inventoryResolver();
-
- abstract InventoryListResolver<ValueTypeT> inventoryListResolver();
-
- /** Wrapper which should invoke the `lookupAndRender` provided by the developer. */
- @NonNull
- public ListenableFuture<EntitySearchResult<ValueTypeT>> invokeLookup(
- @NonNull SearchAction<ValueTypeT> searchAction) throws InvalidResolverException {
- switch (getKind()) {
- case APP_ENTITY_RESOLVER:
- return appEntityResolver().lookupAndRender(searchAction);
- case APP_ENTITY_LIST_RESOLVER:
- return appEntityListResolver().lookupAndRender(searchAction);
- default:
- throw new InvalidResolverException(
- String.format(
- "invokeLookup is not supported on this resolver of type %s",
- getKind().name()));
- }
- }
-
- /**
- * Wrapper which should invoke the EntityRender#renderEntities method when the Assistant is
- * prompting for disambiguation.
- */
- @NonNull
- public ListenableFuture<Void> invokeEntityRender(@NonNull List<String> entityIds)
- throws InvalidResolverException {
- switch (getKind()) {
- case INVENTORY_RESOLVER:
- return inventoryResolver().renderChoices(entityIds);
- case INVENTORY_LIST_RESOLVER:
- return inventoryListResolver().renderChoices(entityIds);
- default:
- throw new InvalidResolverException(
- String.format(
- "invokeEntityRender is not supported on this resolver of type %s",
- getKind().name()));
- }
- }
-
- /**
- * Notifies the app that a new value for this argument has been set by Assistant. This method
- * should only be called with completely grounded values.
- */
- @NonNull
- public ListenableFuture<ValidationResult> notifyValueChange(
- @NonNull List<ParamValue> paramValues,
- @NonNull ParamValueConverter<ValueTypeT> converter)
- throws StructConversionException {
- SlotTypeConverter<ValueTypeT> singularConverter = SlotTypeConverter.ofSingular(converter);
- SlotTypeConverter<List<ValueTypeT>> repeatedConverter =
- SlotTypeConverter.ofRepeated(converter);
-
- switch (getKind()) {
- case VALUE:
- return value().onReceivedAsync(singularConverter.convert(paramValues));
- case VALUE_LIST:
- return valueList().onReceivedAsync(repeatedConverter.convert(paramValues));
- case APP_ENTITY_RESOLVER:
- return appEntityResolver().onReceivedAsync(singularConverter.convert(paramValues));
- case APP_ENTITY_LIST_RESOLVER:
- return appEntityListResolver()
- .onReceivedAsync(repeatedConverter.convert(paramValues));
- case INVENTORY_RESOLVER:
- return inventoryResolver().onReceivedAsync(singularConverter.convert(paramValues));
- case INVENTORY_LIST_RESOLVER:
- return inventoryListResolver()
- .onReceivedAsync(repeatedConverter.convert(paramValues));
- }
- throw new IllegalStateException("unreachable");
- }
-
- /** The kind of resolver. */
- public enum Kind {
- VALUE,
- VALUE_LIST,
- APP_ENTITY_RESOLVER,
- APP_ENTITY_LIST_RESOLVER,
- INVENTORY_RESOLVER,
- INVENTORY_LIST_RESOLVER
- }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt
new file mode 100644
index 0000000..02909a6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.appactions.interaction.capabilities.core.task.impl
+
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
+import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException
+import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult
+import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver
+import androidx.appactions.interaction.capabilities.core.task.InventoryResolver
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult
+import androidx.appactions.interaction.capabilities.core.task.ValueListener
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException
+import androidx.appactions.interaction.capabilities.core.values.SearchAction
+import androidx.appactions.interaction.proto.ParamValue
+import androidx.concurrent.futures.await
+
+/**
+ * A wrapper around all types of slot resolvers (value listeners + disambig resolvers).
+ *
+ * This allows one type of resolver to be bound for each slot, and abstracts the details of the
+ * individual resolvers. It is also the place where repeated fields are handled.
+ *
+ * </ValueTypeT>
+ */
+internal class GenericResolverInternal<ValueTypeT>
+private constructor(
+ val value: ValueListener<ValueTypeT>? = null,
+ val valueList: ValueListener<List<ValueTypeT>>? = null,
+ val appEntityResolver: AppEntityResolver<ValueTypeT>? = null,
+ val appEntityListResolver: AppEntityListResolver<ValueTypeT>? = null,
+ val inventoryResolver: InventoryResolver<ValueTypeT>? = null,
+ val inventoryListResolver: InventoryListResolver<ValueTypeT>? = null,
+) {
+
+ /** Wrapper which should invoke the `lookupAndRender` provided by the developer. */
+ @Throws(InvalidResolverException::class)
+ suspend fun invokeLookup(
+ searchAction: SearchAction<ValueTypeT>
+ ): EntitySearchResult<ValueTypeT> {
+ return if (appEntityResolver != null) {
+ appEntityResolver.lookupAndRender(searchAction).await()
+ } else if (appEntityListResolver != null) {
+ appEntityListResolver.lookupAndRender(searchAction).await()
+ } else {
+ throw InvalidResolverException("invokeLookup is not supported on this resolver")
+ }
+ }
+
+ /**
+ * Wrapper which should invoke the EntityRender#renderEntities method when the Assistant is
+ * prompting for disambiguation.
+ */
+ @Throws(InvalidResolverException::class)
+ suspend fun invokeEntityRender(entityIds: List<String>) {
+ if (inventoryResolver != null) {
+ inventoryResolver.renderChoices(entityIds).await()
+ } else if (inventoryListResolver != null)
+ (inventoryListResolver.renderChoices(entityIds).await())
+ else {
+ throw InvalidResolverException("invokeEntityRender is not supported on this resolver")
+ }
+ }
+
+ /**
+ * Notifies the app that a new value for this argument has been set by Assistant. This method
+ * should only be called with completely grounded values.
+ */
+ @Throws(StructConversionException::class)
+ suspend fun notifyValueChange(
+ paramValues: List<ParamValue>,
+ converter: ParamValueConverter<ValueTypeT>
+ ): ValidationResult {
+ val singularConverter = SlotTypeConverter.ofSingular(converter)
+ val repeatedConverter = SlotTypeConverter.ofRepeated(converter)
+ return when {
+ value != null -> value.onReceivedAsync(singularConverter.convert(paramValues))
+ valueList != null -> valueList.onReceivedAsync(repeatedConverter.convert(paramValues))
+ appEntityResolver != null ->
+ appEntityResolver.onReceivedAsync(singularConverter.convert(paramValues))
+ appEntityListResolver != null ->
+ appEntityListResolver.onReceivedAsync(repeatedConverter.convert(paramValues))
+ inventoryResolver != null ->
+ inventoryResolver.onReceivedAsync(singularConverter.convert(paramValues))
+ inventoryListResolver != null ->
+ inventoryListResolver.onReceivedAsync(repeatedConverter.convert(paramValues))
+ else -> throw IllegalStateException("unreachable")
+ }.await()
+ }
+
+ companion object {
+ fun <ValueTypeT> fromValueListener(valueListener: ValueListener<ValueTypeT>) =
+ GenericResolverInternal(value = valueListener)
+
+ fun <ValueTypeT> fromValueListListener(valueListListener: ValueListener<List<ValueTypeT>>) =
+ GenericResolverInternal(valueList = valueListListener)
+
+ fun <ValueTypeT> fromAppEntityResolver(appEntityResolver: AppEntityResolver<ValueTypeT>) =
+ GenericResolverInternal(appEntityResolver = appEntityResolver)
+
+ fun <ValueTypeT> fromAppEntityListResolver(
+ appEntityListResolver: AppEntityListResolver<ValueTypeT>
+ ) = GenericResolverInternal(appEntityListResolver = appEntityListResolver)
+
+ fun <ValueTypeT> fromInventoryResolver(inventoryResolver: InventoryResolver<ValueTypeT>) =
+ GenericResolverInternal(inventoryResolver = inventoryResolver)
+
+ fun <ValueTypeT> fromInventoryListResolver(
+ inventoryListResolver: InventoryListResolver<ValueTypeT>
+ ) = GenericResolverInternal(inventoryListResolver = inventoryListResolver)
+ }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
index 7cb544d..df72ef9 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
@@ -16,6 +16,7 @@
package androidx.appactions.interaction.capabilities.core.task.impl
+import androidx.annotation.RestrictTo
import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter
import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter
@@ -25,59 +26,55 @@
import androidx.appactions.interaction.capabilities.core.task.InventoryResolver
import androidx.appactions.interaction.capabilities.core.task.ValueListener
import androidx.appactions.interaction.proto.ParamValue
-import java.util.function.Function
-import androidx.annotation.RestrictTo
-
-/**
- * Container of multi-turn Task related function references.
- *
- */
+/** Container of multi-turn Task related function references. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-data class TaskHandler<ConfirmationT> (
- val taskParamMap: Map<String, TaskParamBinding<*>>,
- val confirmationType: ConfirmationType,
- val confirmationDataBindings: Map<String, Function<ConfirmationT, List<ParamValue>>>,
- val onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>?,
+data class TaskHandler<ConfirmationT>
+internal constructor(
+ internal val taskParamMap: Map<String, TaskParamBinding<*>>,
+ internal val confirmationType: ConfirmationType,
+ internal val confirmationDataBindings: Map<String, (ConfirmationT) -> List<ParamValue>>,
+ internal val onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>?,
) {
class Builder<ConfirmationT>(
- val confirmationType: ConfirmationType = ConfirmationType.NOT_SUPPORTED,
+ private val confirmationType: ConfirmationType = ConfirmationType.NOT_SUPPORTED,
) {
- val mutableTaskParamMap = mutableMapOf<String, TaskParamBinding<*>>()
- val confirmationDataBindings: MutableMap<
- String, Function<ConfirmationT, List<ParamValue>>,> = mutableMapOf()
- var onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>? = null
+ private val mutableTaskParamMap = mutableMapOf<String, TaskParamBinding<*>>()
+ private val confirmationDataBindings =
+ mutableMapOf<String, (ConfirmationT) -> List<ParamValue>>()
+ private var onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>? =
+ null
fun <ValueTypeT> registerInventoryTaskParam(
paramName: String,
listener: InventoryResolver<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_IF_NO_IDENTIFIER,
- GenericResolverInternal.fromInventoryResolver(listener),
- converter,
- null,
- null,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_IF_NO_IDENTIFIER,
+ GenericResolverInternal.fromInventoryResolver(listener),
+ converter,
+ null,
+ null,
+ )
}
fun <ValueTypeT> registerInventoryListTaskParam(
paramName: String,
listener: InventoryListResolver<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_IF_NO_IDENTIFIER,
- GenericResolverInternal.fromInventoryListResolver(listener),
- converter,
- null,
- null,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_IF_NO_IDENTIFIER,
+ GenericResolverInternal.fromInventoryListResolver(listener),
+ converter,
+ null,
+ null,
+ )
}
fun <ValueTypeT> registerAppEntityTaskParam(
@@ -86,16 +83,16 @@
converter: ParamValueConverter<ValueTypeT>,
entityConverter: DisambigEntityConverter<ValueTypeT>,
searchActionConverter: SearchActionConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_IF_NO_IDENTIFIER,
- GenericResolverInternal.fromAppEntityResolver(listener),
- converter,
- entityConverter,
- searchActionConverter,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_IF_NO_IDENTIFIER,
+ GenericResolverInternal.fromAppEntityResolver(listener),
+ converter,
+ entityConverter,
+ searchActionConverter,
+ )
}
fun <ValueTypeT> registerAppEntityListTaskParam(
@@ -104,77 +101,73 @@
converter: ParamValueConverter<ValueTypeT>,
entityConverter: DisambigEntityConverter<ValueTypeT>,
searchActionConverter: SearchActionConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_IF_NO_IDENTIFIER,
- GenericResolverInternal.fromAppEntityListResolver(listener),
- converter,
- entityConverter,
- searchActionConverter,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_IF_NO_IDENTIFIER,
+ GenericResolverInternal.fromAppEntityListResolver(listener),
+ converter,
+ entityConverter,
+ searchActionConverter,
+ )
}
fun <ValueTypeT> registerValueTaskParam(
paramName: String,
listener: ValueListener<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_NEVER,
- GenericResolverInternal.fromValueListener(listener),
- converter,
- null,
- null,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_NEVER,
+ GenericResolverInternal.fromValueListener(listener),
+ converter,
+ null,
+ null,
+ )
}
fun <ValueTypeT> registerValueListTaskParam(
paramName: String,
listener: ValueListener<List<ValueTypeT>>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> {
- mutableTaskParamMap[paramName] = TaskParamBinding(
- paramName,
- GROUND_NEVER,
- GenericResolverInternal.fromValueListListener(listener),
- converter,
- null,
- null,
- )
- return this
+ ): Builder<ConfirmationT> = apply {
+ mutableTaskParamMap[paramName] =
+ TaskParamBinding(
+ paramName,
+ GROUND_NEVER,
+ GenericResolverInternal.fromValueListListener(listener),
+ converter,
+ null,
+ null,
+ )
}
/**
* Registers an optional, non-repeated confirmation data.
*
- * @param paramName the BIC confirmation data slot name of this parameter.
- * @param confirmationGetter a getter of the confirmation data from the {@code ConfirmationT}
- * instance.
- * @param converter a converter from confirmation data to a ParamValue.
+ * @param paramName the BIC confirmation data slot name of this parameter.
+ * @param confirmationGetter a getter of the confirmation data from the {@code
+ * ConfirmationT} instance.
+ * @param converter a converter from confirmation data to a ParamValue.
*/
fun <T> registerConfirmationOutput(
paramName: String,
confirmationGetter: (ConfirmationT) -> T?,
converter: (T) -> ParamValue,
- ): Builder<ConfirmationT> {
- confirmationDataBindings.put(
- paramName,
- ) { output: ConfirmationT ->
+ ): Builder<ConfirmationT> = apply {
+ confirmationDataBindings[paramName] = { output: ConfirmationT ->
listOfNotNull(confirmationGetter(output)).map(converter)
}
- return this
}
/** Sets the onReadyToConfirmListener for this capability. */
fun setOnReadyToConfirmListenerInternal(
onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>,
- ): Builder<ConfirmationT> {
+ ): Builder<ConfirmationT> = apply {
this.onReadyToConfirmListener = onReadyToConfirmListener
- return this
}
fun build(): TaskHandler<ConfirmationT> {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
index 3c5ad89..07aced8 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
@@ -467,7 +467,7 @@
taskHandler.confirmationDataBindings.entries.map {
FulfillmentResponse.StructuredOutput.OutputValue.newBuilder()
.setName(it.key)
- .addAllValues(it.value.apply(confirmation))
+ .addAllValues(it.value.invoke(confirmation))
.build()
},
)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
index e24f9fa..ff78b18 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
@@ -15,7 +15,6 @@
*/
package androidx.appactions.interaction.capabilities.core.task.impl
-import androidx.annotation.RestrictTo
import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter
import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter
@@ -24,14 +23,14 @@
/**
* A binding between a parameter and its Property converter / Argument setter.
*
- *
-</ValueTypeT> */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-data class TaskParamBinding<ValueTypeT> internal constructor(
+ * </ValueTypeT>
+ */
+internal data class TaskParamBinding<ValueTypeT>
+constructor(
val name: String,
val groundingPredicate: (ParamValue) -> Boolean,
val resolver: GenericResolverInternal<ValueTypeT>,
val converter: ParamValueConverter<ValueTypeT>,
val entityConverter: DisambigEntityConverter<ValueTypeT>?,
val searchActionConverter: SearchActionConverter<ValueTypeT>?,
-)
\ No newline at end of file
+)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
index fc41c53..94f3c8b 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
@@ -25,7 +25,6 @@
import androidx.appactions.interaction.proto.CurrentValue
import androidx.appactions.interaction.proto.DisambiguationData
import androidx.appactions.interaction.proto.ParamValue
-import androidx.concurrent.futures.await
import kotlin.IllegalStateException
import kotlin.String
import kotlin.Throws
@@ -166,7 +165,7 @@
updatedValue: List<ParamValue>,
binding: TaskParamBinding<T>
): ValidationResult {
- return binding.resolver.notifyValueChange(updatedValue, binding.converter).await()
+ return binding.resolver.notifyValueChange(updatedValue, binding.converter)
}
@Throws(InvalidResolverException::class)
@@ -175,7 +174,7 @@
binding: TaskParamBinding<*>
) {
val entityIds = disambiguationData.entitiesList.map { it.identifier }
- binding.resolver.invokeEntityRender(entityIds).await()
+ binding.resolver.invokeEntityRender(entityIds)
}
/**
@@ -237,12 +236,8 @@
val entityConverter = binding.entityConverter
val searchActionConverter = binding.searchActionConverter
val searchAction = searchActionConverter.toSearchAction(ungroundedParamValue)
- val entitySearchResult = binding.resolver.invokeLookup(searchAction).await()
- return processEntitySearchResult<T>(
- entitySearchResult,
- entityConverter,
- ungroundedParamValue
- )
+ val entitySearchResult = binding.resolver.invokeLookup(searchAction)
+ return processEntitySearchResult(entitySearchResult, entityConverter, ungroundedParamValue)
}
/**
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
index 69d9f74..05f27e4 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
@@ -27,7 +27,7 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Entity
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils
import androidx.appactions.interaction.capabilities.core.testing.FakeCallbackInternal
@@ -69,7 +69,7 @@
TypeProperty.Builder<Entity>().build(),
)
.setOptionalStringField(
- StringProperty.Builder().setProhibited(true).build(),
+ TypeProperty.Builder<StringValue>().setProhibited(true).build(),
)
.build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
@@ -122,7 +122,7 @@
TypeProperty.Builder<Entity>().build(),
)
.setOptionalStringField(
- StringProperty.Builder().setProhibited(true).build(),
+ TypeProperty.Builder<StringValue>().setProhibited(true).build(),
)
.build(),
actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
index 7c6afd3..5d8587c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
@@ -23,7 +23,7 @@
import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
import androidx.appactions.interaction.capabilities.core.properties.Entity;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import androidx.appactions.interaction.capabilities.core.testing.spec.Output;
import androidx.appactions.interaction.capabilities.core.values.EntityValue;
@@ -185,7 +185,7 @@
.build())
.setValueMatchRequired(true)
.build(),
- new StringProperty.Builder().build());
+ new TypeProperty.Builder<StringValue>().build());
assertThat(ACTION_SPEC.convertPropertyToProto(property))
.isEqualTo(
@@ -246,14 +246,17 @@
.build())
.setRequired(true)
.build()),
- new StringProperty.Builder().build(),
+ new TypeProperty.Builder<StringValue>().build(),
Optional.of(
- new StringProperty.Builder()
- .addPossibleValue("value1")
+ new TypeProperty.Builder<StringValue>()
+ .addPossibleEntities(StringValue.of("value1"))
.setValueMatchRequired(true)
.setRequired(true)
.build()),
- Optional.of(new StringProperty.Builder().setProhibited(true).build()));
+ Optional.of(
+ new TypeProperty.Builder<StringValue>()
+ .setProhibited(true)
+ .build()));
assertThat(ACTION_SPEC.convertPropertyToProto(property))
.isEqualTo(
@@ -435,9 +438,9 @@
Optional<TypeProperty<Entity>> optionalEntityField,
Optional<TypeProperty<TestEnum>> optionalEnumField,
Optional<TypeProperty<Entity>> repeatedEntityField,
- StringProperty requiredStringField,
- Optional<StringProperty> optionalStringField,
- Optional<StringProperty> repeatedStringField) {
+ TypeProperty<StringValue> requiredStringField,
+ Optional<TypeProperty<StringValue>> optionalStringField,
+ Optional<TypeProperty<StringValue>> repeatedStringField) {
return new AutoValue_ActionSpecTest_Property(
requiredEntityField,
optionalEntityField,
@@ -449,7 +452,8 @@
}
static Property create(
- TypeProperty<Entity> requiredEntityField, StringProperty requiredStringField) {
+ TypeProperty<Entity> requiredEntityField,
+ TypeProperty<StringValue> requiredStringField) {
return create(
requiredEntityField,
Optional.empty(),
@@ -468,11 +472,11 @@
abstract Optional<TypeProperty<Entity>> repeatedEntityField();
- abstract StringProperty requiredStringField();
+ abstract TypeProperty<StringValue> requiredStringField();
- abstract Optional<StringProperty> optionalStringField();
+ abstract Optional<TypeProperty<StringValue>> optionalStringField();
- abstract Optional<StringProperty> repeatedStringField();
+ abstract Optional<TypeProperty<StringValue>> repeatedStringField();
}
@AutoValue
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
index da094b2..c39ef8d 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
@@ -31,7 +31,7 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver
import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult
@@ -659,7 +659,7 @@
val property: CapabilityStructFill.Property =
CapabilityStructFill.Property.newBuilder()
.setListItem(SimpleProperty.Builder().setRequired(true).build())
- .setAnyString(StringProperty.Builder().setRequired(true).build())
+ .setAnyString(TypeProperty.Builder<StringValue>().setRequired(true).build())
.build()
val item1: ListItem = ListItem.newBuilder().setName("red apple").setId("item1").build()
val item2: ListItem = ListItem.newBuilder().setName("green apple").setId("item2").build()
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
index 54f25db..eca0b7a 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
@@ -16,7 +16,8 @@
package androidx.appactions.interaction.capabilities.core.task.impl
import androidx.appactions.interaction.capabilities.core.impl.converters.PropertyConverter
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.proto.AppActionsContext
import androidx.appactions.interaction.proto.CurrentValue
import androidx.appactions.interaction.proto.FulfillmentRequest
@@ -36,7 +37,8 @@
intentParameters.add(
PropertyConverter.getIntentParameter(
"required",
- StringProperty.Builder().setRequired(true).build()
+ TypeProperty.Builder<StringValue>().setRequired(true).build(),
+ PropertyConverter::stringValueToProto
)
)
assertThat(TaskCapabilityUtils.isSlotFillingComplete(args, intentParameters)).isTrue()
@@ -48,7 +50,8 @@
intentParameters.add(
PropertyConverter.getIntentParameter(
"required",
- StringProperty.Builder().setRequired(true).build()
+ TypeProperty.Builder<StringValue>().setRequired(true).build(),
+ PropertyConverter::stringValueToProto
)
)
assertThat(TaskCapabilityUtils.isSlotFillingComplete(emptyMap(), intentParameters))
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
index 26dab57..56c256c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
@@ -23,7 +23,8 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
import androidx.appactions.interaction.capabilities.core.values.ListItem;
@@ -85,7 +86,7 @@
public abstract Optional<SimpleProperty> listItem();
- public abstract Optional<StringProperty> anyString();
+ public abstract Optional<TypeProperty<StringValue>> anyString();
/** Builder for {@link Property} */
@AutoValue.Builder
@@ -95,7 +96,7 @@
public abstract Builder setListItem(@NonNull SimpleProperty value);
@NonNull
- public abstract Builder setAnyString(@NonNull StringProperty value);
+ public abstract Builder setAnyString(@NonNull TypeProperty<StringValue> value);
@NonNull
public abstract Property build();
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
index f504c72..f383555 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
@@ -20,7 +20,8 @@
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import com.google.auto.value.AutoValue;
@@ -75,19 +76,19 @@
return new AutoValue_CapabilityTwoStrings_Property.Builder();
}
- public abstract Optional<StringProperty> stringSlotA();
+ public abstract Optional<TypeProperty<StringValue>> stringSlotA();
- public abstract Optional<StringProperty> stringSlotB();
+ public abstract Optional<TypeProperty<StringValue>> stringSlotB();
/** Builder for {@link Property} */
@AutoValue.Builder
public abstract static class Builder {
@NonNull
- public abstract Builder setStringSlotA(@NonNull StringProperty value);
+ public abstract Builder setStringSlotA(@NonNull TypeProperty<StringValue> value);
@NonNull
- public abstract Builder setStringSlotB(@NonNull StringProperty value);
+ public abstract Builder setStringSlotB(@NonNull TypeProperty<StringValue> value);
@NonNull
public abstract Property build();
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
index f5e3839..030b5c3 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
@@ -19,7 +19,7 @@
import androidx.annotation.NonNull;
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
import androidx.appactions.interaction.capabilities.core.properties.Entity;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
import com.google.auto.value.AutoValue;
@@ -36,11 +36,11 @@
public abstract TypeProperty<Entity> requiredEntityField();
- public abstract Optional<StringProperty> optionalStringField();
+ public abstract Optional<TypeProperty<StringValue>> optionalStringField();
public abstract Optional<TypeProperty<TestEnum>> enumField();
- public abstract Optional<StringProperty> repeatedStringField();
+ public abstract Optional<TypeProperty<StringValue>> repeatedStringField();
/** Builder for the testing Property. */
@AutoValue.Builder
@@ -48,11 +48,11 @@
public abstract Builder setRequiredEntityField(TypeProperty<Entity> property);
- public abstract Builder setOptionalStringField(StringProperty property);
+ public abstract Builder setOptionalStringField(TypeProperty<StringValue> property);
public abstract Builder setEnumField(TypeProperty<TestEnum> property);
- public abstract Builder setRepeatedStringField(StringProperty property);
+ public abstract Builder setRepeatedStringField(TypeProperty<StringValue> property);
@NonNull
@Override
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
index fb2829d..574d0ee 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
@@ -22,7 +22,8 @@
import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import java.util.Optional
@@ -49,7 +50,7 @@
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
>(ACTION_SPEC) {
private var propertyBuilder: Property.Builder = Property.Builder()
- fun setNameProperty(name: StringProperty): CapabilityBuilder =
+ fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
apply {
propertyBuilder.setName(name)
}
@@ -63,7 +64,7 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
- val name: StringProperty?,
+ val name: TypeProperty<StringValue>?,
) {
override fun toString(): String {
return "Property(name=$name)"
@@ -85,9 +86,9 @@
}
class Builder {
- private var name: StringProperty? = null
+ private var name: TypeProperty<StringValue>? = null
- fun setName(name: StringProperty): Builder =
+ fun setName(name: TypeProperty<StringValue>): Builder =
apply { this.name = name }
fun build(): Property = Property(name)
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
index 4df6fab..d231eff 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
@@ -22,7 +22,8 @@
import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import java.util.Optional
@@ -49,7 +50,7 @@
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
>(ACTION_SPEC) {
private var propertyBuilder: Property.Builder = Property.Builder()
- fun setNameProperty(name: StringProperty): CapabilityBuilder =
+ fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
apply {
propertyBuilder.setName(name)
}
@@ -63,7 +64,7 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
- val name: StringProperty?,
+ val name: TypeProperty<StringValue>?,
) {
override fun toString(): String {
return "Property(name=$name)"
@@ -85,9 +86,9 @@
}
class Builder {
- private var name: StringProperty? = null
+ private var name: TypeProperty<StringValue>? = null
- fun setName(name: StringProperty): Builder =
+ fun setName(name: TypeProperty<StringValue>): Builder =
apply { this.name = name }
fun build(): Property = Property(name)
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
index 1545abc..734a0ec 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
@@ -24,7 +24,8 @@
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import java.time.Duration
import java.util.Optional
@@ -62,7 +63,7 @@
Property.Builder().setDuration(duration).build()
}
- fun setNameProperty(name: StringProperty): CapabilityBuilder =
+ fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
apply {
Property.Builder().setName(name).build()
}
@@ -77,7 +78,7 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
val duration: SimpleProperty?,
- val name: StringProperty?
+ val name: TypeProperty<StringValue>?
) {
override fun toString(): String {
return "Property(duration=$duration, name=$name)"
@@ -103,12 +104,12 @@
class Builder {
private var duration: SimpleProperty? = null
- private var name: StringProperty? = null
+ private var name: TypeProperty<StringValue>? = null
fun setDuration(duration: SimpleProperty): Builder =
apply { this.duration = duration }
- fun setName(name: StringProperty): Builder =
+ fun setName(name: TypeProperty<StringValue>): Builder =
apply { this.name = name }
fun build(): Property = Property(duration, name)
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
index d8ca5c3..6942f96 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
@@ -22,7 +22,8 @@
import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import java.util.Optional
@@ -49,7 +50,7 @@
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
>(ACTION_SPEC) {
private var propertyBuilder: Property.Builder = Property.Builder()
- fun setNameProperty(name: StringProperty): CapabilityBuilder =
+ fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
apply {
propertyBuilder.setName(name)
}
@@ -63,7 +64,7 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
- val name: StringProperty?,
+ val name: TypeProperty<StringValue>?,
) {
override fun toString(): String {
return "Property(name=$name)"
@@ -85,9 +86,9 @@
}
class Builder {
- private var name: StringProperty? = null
+ private var name: TypeProperty<StringValue>? = null
- fun setName(name: StringProperty): Builder =
+ fun setName(name: TypeProperty<StringValue>): Builder =
apply { this.name = name }
fun build(): Property = Property(name)
diff --git a/appactions/interaction/interaction-capabilities-productivity/build.gradle b/appactions/interaction/interaction-capabilities-productivity/build.gradle
index 21b8526..aa110aa 100644
--- a/appactions/interaction/interaction-capabilities-productivity/build.gradle
+++ b/appactions/interaction/interaction-capabilities-productivity/build.gradle
@@ -24,7 +24,6 @@
dependencies {
api(libs.kotlinStdlib)
- implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
implementation("androidx.annotation:annotation:1.1.0")
implementation(project(":appactions:interaction:interaction-capabilities-core"))
}
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
index b196e5f..22faf7a 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
@@ -26,7 +26,6 @@
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
-import androidx.appactions.interaction.capabilities.core.values.Timer
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
@@ -42,11 +41,11 @@
"timer",
{ property -> Optional.ofNullable(property.timerList) },
PauseTimer.Argument.Builder::setTimerList,
- TypeConverters::toTimer
+ TimerValue.FROM_PARAM_VALUE,
).bindOptionalOutput(
"executionStatus",
{ output -> Optional.ofNullable(output.executionStatus) },
- PauseTimer.ExecutionStatus::toParamValue
+ PauseTimer.ExecutionStatus::toParamValue,
).build()
// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
@@ -54,7 +53,7 @@
class CapabilityBuilder :
CapabilityBuilderBase<
- CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+ CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session,
>(ACTION_SPEC) {
override fun build(): ActionCapability {
super.setProperty(Property.Builder().build())
@@ -64,7 +63,7 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
- val timerList: SimpleProperty?
+ val timerList: SimpleProperty?,
) {
override fun toString(): String {
return "Property(timerList=$timerList}"
@@ -96,7 +95,7 @@
}
class Argument internal constructor(
- val timerList: List<Timer>?
+ val timerList: List<TimerValue>?,
) {
override fun toString(): String {
return "Argument(timerList=$timerList)"
@@ -118,9 +117,11 @@
}
class Builder : BuilderOf<Argument> {
- private var timerList: List<Timer>? = null
+ private var timerList: List<TimerValue>? = null
- fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+ fun setTimerList(timerList: List<TimerValue>): Builder = apply {
+ this.timerList = timerList
+ }
override fun build(): Argument = Argument(timerList)
}
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
index 74bec9e..069bf78 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
@@ -23,8 +23,9 @@
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
import androidx.appactions.interaction.capabilities.core.task.ValueListener
import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
@@ -96,8 +97,8 @@
// TODO(b/268369632): Remove Property from public capability APIs.
class Property internal constructor(
- val identifier: StringProperty?,
- val name: StringProperty?,
+ val identifier: TypeProperty<StringValue>?,
+ val name: TypeProperty<StringValue>?,
val duration: SimpleProperty?
) {
override fun toString(): String {
@@ -125,14 +126,14 @@
}
class Builder {
- private var identifier: StringProperty? = null
- private var name: StringProperty? = null
+ private var identifier: TypeProperty<StringValue>? = null
+ private var name: TypeProperty<StringValue>? = null
private var duration: SimpleProperty? = null
- fun setIdentifier(identifier: StringProperty): Builder =
+ fun setIdentifier(identifier: TypeProperty<StringValue>): Builder =
apply { this.identifier = identifier }
- fun setName(name: StringProperty): Builder =
+ fun setName(name: TypeProperty<StringValue>): Builder =
apply { this.name = name }
fun setDuration(duration: SimpleProperty): Builder =
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt
new file mode 100644
index 0000000..76515c8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.converters.UnionTypeSpec
+import androidx.appactions.interaction.capabilities.core.values.SearchAction
+import androidx.appactions.interaction.capabilities.core.values.Timer
+
+class TimerValue private constructor(
+ val asTimer: Timer?,
+ val asTimerFilter: SearchAction<Timer>?,
+) {
+ constructor(timer: Timer) : this(timer, null)
+
+ // TODO(b/268071906) add TimerFilter type to SearchAction
+ constructor(timerFilter: SearchAction<Timer>) : this(null, timerFilter)
+
+ companion object {
+ private val TYPE_SPEC = UnionTypeSpec.Builder<TimerValue>()
+ .bindMemberType(
+ memberGetter = TimerValue::asTimer,
+ ctor = { TimerValue(it) },
+ typeSpec = TypeConverters.TIMER_TYPE_SPEC,
+ )
+ .bindMemberType(
+ memberGetter = TimerValue::asTimerFilter,
+ ctor = { TimerValue(it) },
+ typeSpec = TypeConverters.createSearchActionTypeSpec(
+ TypeConverters.TIMER_TYPE_SPEC,
+ ),
+ )
+ .build()
+
+ internal val FROM_PARAM_VALUE = ParamValueConverter {
+ TYPE_SPEC.fromStruct(it.getStructValue())
+ }
+ }
+}
diff --git a/appcompat/appcompat-resources/build.gradle b/appcompat/appcompat-resources/build.gradle
index bab4b22..cd96606 100644
--- a/appcompat/appcompat-resources/build.gradle
+++ b/appcompat/appcompat-resources/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":appcompat:appcompat"))
- }
-
api("androidx.annotation:annotation:1.2.0")
api("androidx.core:core:1.6.0")
implementation("androidx.collection:collection:1.0.0")
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index efb2192..aea54f9 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -8,11 +8,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":appcompat:appcompat-resources"))
- }
-
api("androidx.annotation:annotation:1.3.0")
api("androidx.core:core:1.9.0")
diff --git a/autofill/autofill/api/current.txt b/autofill/autofill/api/current.txt
index 668e933..0b0a188 100644
--- a/autofill/autofill/api/current.txt
+++ b/autofill/autofill/api/current.txt
@@ -16,7 +16,12 @@
field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
field public static final String AUTOFILL_HINT_GENDER = "gender";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+ field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/api/public_plus_experimental_current.txt b/autofill/autofill/api/public_plus_experimental_current.txt
index 668e933..0b0a188 100644
--- a/autofill/autofill/api/public_plus_experimental_current.txt
+++ b/autofill/autofill/api/public_plus_experimental_current.txt
@@ -16,7 +16,12 @@
field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
field public static final String AUTOFILL_HINT_GENDER = "gender";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+ field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/api/restricted_current.txt b/autofill/autofill/api/restricted_current.txt
index d057b98..2ce5394 100644
--- a/autofill/autofill/api/restricted_current.txt
+++ b/autofill/autofill/api/restricted_current.txt
@@ -16,7 +16,12 @@
field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+ field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
field public static final String AUTOFILL_HINT_GENDER = "gender";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+ field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+ field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index ebc4328..e265e47 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
* should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
*
* <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
- * hints.
+ * hints.3
*/
public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
@@ -680,4 +680,65 @@
* hints.
*/
public static final String AUTOFILL_HINT_UPI_VPA = "upiVirtualPaymentAddress";
+
+ /**
+ * Hint indicating that this view can be autofilled with a loyalty number.
+ *
+ * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+ * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+ * should be <code>{@value #AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER}</code>).
+ *
+ * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+ * hints.
+ */
+ public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
+
+ /**
+ * Hint indicating that this view can be autofilled with a gift card code.
+ *
+ * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+ * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+ * should be <code>{@value #AUTOFILL_HINT_GIFT_CARD_CODE}</code>).
+ *
+ * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+ * hints.
+ */
+ public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+
+ /**
+ * Hint indicating that this view can be autofilled with a gift card pin.
+ *
+ * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+ * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+ * should be <code>{@value #AUTOFILL_HINT_GIFT_CARD_PIN}</code>).
+ *
+ * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+ * hints.
+ */
+ public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+
+ /**
+ * Hint indicating that this view can be autofilled with a flight number.
+ * Examples: UA 355 (United Airlines), WN 355 (Southwest), AA 9158 (American Airlines)
+ *
+ * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+ * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+ * should be <code>{@value #AUTOFILL_HINT_FLIGHT_NUMBER}</code>).
+ *
+ * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+ * hints.
+ */
+ public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
+
+ /**
+ * Hint indicating that this view can be autofilled with a flight confirmation code.
+ *
+ * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+ * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+ * should be <code>{@value #AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE}</code>).
+ *
+ * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+ * hints.
+ */
+ public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
}
diff --git a/biometric/biometric-ktx/build.gradle b/biometric/biometric-ktx/build.gradle
old mode 100755
new mode 100644
index 89d81fc..dec0bdd
--- a/biometric/biometric-ktx/build.gradle
+++ b/biometric/biometric-ktx/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":biometric:biometric-ktx"))
- }
-
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
api(project(":biometric:biometric"))
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index 4243ce5..c295ae0 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -28,11 +28,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":biometric:biometric-ktx"))
- }
-
// Public API dependencies
api("androidx.annotation:annotation:1.1.0")
api("androidx.core:core:1.3.2")
diff --git a/busytown/androidx_multiplatform_mac.sh b/busytown/androidx_multiplatform_mac.sh
index b159ca6..9f2dfbd 100755
--- a/busytown/androidx_multiplatform_mac.sh
+++ b/busytown/androidx_multiplatform_mac.sh
@@ -15,7 +15,7 @@
# Setup simulators
impl/androidx-native-mac-simulator-setup.sh
-impl/build.sh buildOnServer :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
+impl/build.sh buildOnServer :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false createAllArchives -Pandroidx.constraints=true
# run a separate createAllArchives task to prepare a repository
# folder in DIST.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 7492aa0..048c265 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -21,6 +21,8 @@
import android.annotation.SuppressLint
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
import android.util.Range
import android.util.Size
import android.view.Surface
@@ -39,6 +41,13 @@
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraState
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.BIT_DEPTH_8_BIT
+import androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION
+import androidx.camera.core.DynamicRange.FORMAT_HDR10
+import androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS
+import androidx.camera.core.DynamicRange.FORMAT_HLG
import androidx.camera.core.ExposureState
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ZoomState
@@ -177,4 +186,47 @@
Log.warn { "TODO: isPrivateReprocessingSupported are not yet supported." }
return false
}
+
+ @SuppressLint("ClassVerificationFailure")
+ override fun getSupportedDynamicRanges(): Set<DynamicRange> {
+ // TODO: use DynamicRangesCompat instead after it is migrates from camera-camera2.
+ if (Build.VERSION.SDK_INT >= 33) {
+ val availableProfiles = cameraProperties.metadata[
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]!!
+ return profileSetToDynamicRangeSet(availableProfiles.supportedProfiles)
+ }
+ return setOf(DynamicRange.SDR)
+ }
+
+ private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
+ return profileSet.map { profileToDynamicRange(it) }.toSet()
+ }
+
+ private fun profileToDynamicRange(profile: Long): DynamicRange {
+ return checkNotNull(PROFILE_TO_DR_MAP[profile]) {
+ "Dynamic range profile cannot be converted to a DynamicRange object: $profile"
+ }
+ }
+
+ companion object {
+ private val DR_HLG10 = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+ private val DR_HDR10 = DynamicRange(FORMAT_HDR10, BIT_DEPTH_10_BIT)
+ private val DR_HDR10_PLUS = DynamicRange(FORMAT_HDR10_PLUS, BIT_DEPTH_10_BIT)
+ private val DR_DOLBY_VISION_10_BIT = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_10_BIT)
+ private val DR_DOLBY_VISION_8_BIT = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_8_BIT)
+ private val PROFILE_TO_DR_MAP: Map<Long, DynamicRange> = mapOf(
+ DynamicRangeProfiles.STANDARD to DynamicRange.SDR,
+ DynamicRangeProfiles.HLG10 to DR_HLG10,
+ DynamicRangeProfiles.HDR10 to DR_HDR10,
+ DynamicRangeProfiles.HDR10_PLUS to DR_HDR10_PLUS,
+ DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM to DR_DOLBY_VISION_10_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM_PO to DR_DOLBY_VISION_10_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF to DR_DOLBY_VISION_10_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF_PO to DR_DOLBY_VISION_10_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM to DR_DOLBY_VISION_8_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM_PO to DR_DOLBY_VISION_8_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF to DR_DOLBY_VISION_8_BIT,
+ DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF_PO to DR_DOLBY_VISION_8_BIT,
+ )
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 5541a40..ed6acf5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -27,6 +27,7 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
@@ -43,11 +44,11 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
/**
* Implementation of focus and metering controls exposed by [CameraControlInternal].
@@ -110,7 +111,7 @@
val signal = CompletableDeferred<FocusMeteringResult>()
useCaseCamera?.let { useCaseCamera ->
- val job = threads.sequentialScope.launch {
+ threads.sequentialScope.launch {
cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
updateSignal = signal
@@ -158,50 +159,54 @@
} else {
(false to autoFocusTimeoutMs)
}
- withTimeoutOrNull(timeout) {
- /**
- * If device does not support a 3A region, we should not update it at all.
- * If device does support but a region list is empty, it means any previously
- * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
- */
- useCaseCamera.requestControl.startFocusAndMeteringAsync(
- aeRegions = if (maxAeRegionCount > 0)
- aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- afRegions = if (maxAfRegionCount > 0)
- afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- awbRegions = if (maxAwbRegionCount > 0)
- awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
- else null,
- afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON)
- ).await()
- }.let { result3A ->
- if (result3A != null) {
- if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
- signal.completeExceptionally(
- OperationCanceledException("Camera is not active.")
- )
- } else {
- signal.complete(result3A.toFocusMeteringResult(
- shouldTriggerAf = afRectangles.isNotEmpty()
- ))
+
+ /**
+ * If device does not support a 3A region, we should not update it at all.
+ * If device does support but a region list is empty, it means any previously
+ * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
+ */
+ val result3A = useCaseCamera.requestControl.startFocusAndMeteringAsync(
+ aeRegions = if (maxAeRegionCount > 0)
+ aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ afRegions = if (maxAfRegionCount > 0)
+ afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ awbRegions = if (maxAwbRegionCount > 0)
+ awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+ else null,
+ aeLockBehavior = if (maxAeRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ afLockBehavior = if (maxAfRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ awbLockBehavior = if (maxAwbRegionCount > 0)
+ Lock3ABehavior.AFTER_NEW_SCAN
+ else null,
+ afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
+ timeLimitNs = TimeUnit.NANOSECONDS.convert(
+ timeout,
+ TimeUnit.MILLISECONDS
+ )
+ ).await()
+
+ if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
+ signal.completeExceptionally(
+ OperationCanceledException("Camera is not active.")
+ )
+ } else if (result3A.status == Result3A.Status.TIME_LIMIT_REACHED) {
+ if (isCancelEnabled) {
+ if (signal.isActive) {
+ cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
}
} else {
- if (isCancelEnabled) {
- if (signal.isActive) {
- cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
- }
- } else {
- signal.complete(FocusMeteringResult.create(false))
- }
+ signal.complete(FocusMeteringResult.create(false))
}
- }
- }
-
- signal.invokeOnCompletion { throwable ->
- if (throwable is OperationCanceledException) {
- job.cancel()
+ } else {
+ signal.complete(result3A.toFocusMeteringResult(
+ shouldTriggerAf = afRectangles.isNotEmpty()
+ ))
}
}
} ?: run {
@@ -311,7 +316,8 @@
* in priority. On the other hand, resultAfState == null matters only if the result comes
* from a submitted request, so it should be checked after frameMetadata == null.
*
- * Ref: [FocusMeteringAction] and [Controller3A.lock3A] documentations.
+ * @see FocusMeteringAction
+ * @see androidx.camera.camera2.pipe.graph.Controller3A.lock3A
*/
val isFocusSuccessful = when {
!shouldTriggerAf -> false
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 3e96a04..bfa1fc7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
@@ -132,7 +133,11 @@
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null,
- afTriggerStartAeMode: AeMode? = null
+ aeLockBehavior: Lock3ABehavior? = null,
+ afLockBehavior: Lock3ABehavior? = null,
+ awbLockBehavior: Lock3ABehavior? = null,
+ afTriggerStartAeMode: AeMode? = null,
+ timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
): Deferred<Result3A>
suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
@@ -219,14 +224,21 @@
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
- afTriggerStartAeMode: AeMode?
+ aeLockBehavior: Lock3ABehavior?,
+ afLockBehavior: Lock3ABehavior?,
+ awbLockBehavior: Lock3ABehavior?,
+ afTriggerStartAeMode: AeMode?,
+ timeLimitNs: Long,
): Deferred<Result3A> = graph.acquireSession().use {
it.lock3A(
aeRegions = aeRegions,
afRegions = afRegions,
awbRegions = awbRegions,
- afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
- afTriggerStartAeMode = afTriggerStartAeMode
+ aeLockBehavior = aeLockBehavior,
+ afLockBehavior = afLockBehavior,
+ awbLockBehavior = awbLockBehavior,
+ afTriggerStartAeMode = afTriggerStartAeMode,
+ timeLimitNs = timeLimitNs,
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 9898814..3b0b4c2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -29,6 +29,7 @@
import android.util.Size
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
@@ -60,7 +61,6 @@
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeUseCase
-import androidx.concurrent.futures.await
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -361,6 +361,20 @@
}
@Test
+ fun startFocusAndMetering_defaultPoint_3ALocksAreCorrect() = runBlocking {
+ startFocusMeteringAndAwait(FocusMeteringAction.Builder(point1).build())
+
+ with(fakeRequestControl.focusMeteringCalls.last()) {
+ assertWithMessage("Wrong lock behavior for AE")
+ .that(aeLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ assertWithMessage("Wrong lock behavior for AF")
+ .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ assertWithMessage("Wrong lock behavior for AWB")
+ .that(awbLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+ }
+ }
+
+ @Test
fun startFocusAndMetering_multiplePoints_3ARectsAreCorrect() = runBlocking {
// Camera 0 i.e. Max AF count = 3, Max AE count = 3, Max AWB count = 1
startFocusMeteringAndAwait(
@@ -1268,7 +1282,7 @@
}
@Test
- fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotSet() {
+ fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotConfigured() {
// camera 5 supports 1 AF and 0 AE/AWB regions
focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
@@ -1282,8 +1296,14 @@
with(fakeRequestControl.focusMeteringCalls.last()) {
assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
+ assertWithMessage("Wrong lock behavior for AE").that(aeLockBehavior).isNull()
+
assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+ assertWithMessage("Wrong lock behavior for AE")
+ .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+
assertWithMessage("Wrong number of AWB regions").that(awbRegions).isNull()
+ assertWithMessage("Wrong lock behavior for AWB").that(awbLockBehavior).isNull()
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 8da683c..1ffe0a0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -27,6 +27,7 @@
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.CameraState
+import androidx.camera.core.DynamicRange
import androidx.camera.core.ExposureState
import androidx.camera.core.ZoomState
import androidx.camera.core.impl.CameraCaptureCallback
@@ -173,6 +174,10 @@
override fun getSupportedHighResolutions(format: Int): MutableList<Size> {
throw NotImplementedError("Not used in testing")
}
+
+ override fun getSupportedDynamicRanges(): MutableSet<DynamicRange> {
+ throw NotImplementedError("Not used in testing")
+ }
}
Camera2CameraInfo.from(wrongCameraInfo)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 215e70a..64d3363 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -19,6 +19,8 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.MeteringRectangle
import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.Result3A
@@ -31,9 +33,11 @@
import androidx.camera.core.UseCase
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
+import kotlinx.coroutines.withTimeoutOrNull
class FakeUseCaseCameraComponentBuilder : UseCaseCameraComponent.Builder {
private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList(), CameraStateAdapter())
@@ -101,11 +105,27 @@
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?,
- afTriggerStartAeMode: AeMode?
+ aeLockBehavior: Lock3ABehavior?,
+ afLockBehavior: Lock3ABehavior?,
+ awbLockBehavior: Lock3ABehavior?,
+ afTriggerStartAeMode: AeMode?,
+ timeLimitNs: Long,
): Deferred<Result3A> {
focusMeteringCalls.add(
- FocusMeteringParams(aeRegions, afRegions, awbRegions, afTriggerStartAeMode)
+ FocusMeteringParams(
+ aeRegions, afRegions, awbRegions,
+ aeLockBehavior, afLockBehavior, awbLockBehavior,
+ afTriggerStartAeMode,
+ timeLimitNs
+ )
)
+ withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeLimitNs, TimeUnit.NANOSECONDS)) {
+ focusMeteringResult.await()
+ }.let { result3A ->
+ if (result3A == null) {
+ focusMeteringResult.complete(Result3A(status = Result3A.Status.TIME_LIMIT_REACHED))
+ }
+ }
return focusMeteringResult
}
@@ -127,7 +147,11 @@
val aeRegions: List<MeteringRectangle>? = null,
val afRegions: List<MeteringRectangle>? = null,
val awbRegions: List<MeteringRectangle>? = null,
- val afTriggerStartAeMode: AeMode? = null
+ val aeLockBehavior: Lock3ABehavior? = null,
+ val afLockBehavior: Lock3ABehavior? = null,
+ val awbLockBehavior: Lock3ABehavior? = null,
+ val afTriggerStartAeMode: AeMode? = null,
+ val timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
)
data class RequestParameters(
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 973ad8b..0f3dd35 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -16,9 +16,11 @@
package androidx.camera.camera2.internal;
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertTrue;
@@ -31,6 +33,9 @@
import static org.mockito.Mockito.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
@@ -55,7 +60,6 @@
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
-import androidx.camera.core.InitializationException;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
@@ -100,11 +104,10 @@
import org.mockito.Mockito;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -127,7 +130,16 @@
private static final int DEFAULT_PAIRED_CAMERA_LENS_FACING = CameraSelector.LENS_FACING_FRONT;
// For the purpose of this test, always say we have 1 camera available.
private static final int DEFAULT_AVAILABLE_CAMERA_COUNT = 1;
- private static final Set<CameraInternal.State> STABLE_STATES = new HashSet<>(Arrays.asList(
+ private static final int DEFAULT_TEMPLATE_TYPE = CameraDevice.TEMPLATE_PREVIEW;
+ private static final Map<Integer, Boolean> DEFAULT_TEMPLATE_TO_ZSL_DISABLED = new HashMap<>();
+
+ static {
+ DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_PREVIEW, false);
+ DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_RECORD, true);
+ DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG, false);
+ }
+
+ private static final Set<CameraInternal.State> STABLE_STATES = new HashSet<>(asList(
CameraInternal.State.CLOSED,
CameraInternal.State.OPEN,
CameraInternal.State.RELEASED));
@@ -139,9 +151,8 @@
new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
);
- private ArrayList<FakeUseCase> mFakeUseCases = new ArrayList<>();
+ private final ArrayList<FakeUseCase> mFakeUseCases = new ArrayList<>();
private Camera2CameraImpl mCamera2CameraImpl;
- private Camera2CameraImpl mPairedCamera2CameraImpl;
private static HandlerThread sCameraHandlerThread;
private static Handler sCameraHandler;
private FakeCameraCoordinator mCameraCoordinator;
@@ -154,7 +165,7 @@
SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
@BeforeClass
- public static void classSetup() throws InitializationException {
+ public static void classSetup() {
sCameraHandlerThread = new HandlerThread("cameraThread");
sCameraHandlerThread.start();
sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
@@ -214,11 +225,11 @@
mCamera2CameraImpl.open();
UseCase useCase = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase));
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase));
mCamera2CameraImpl.release();
}
@@ -235,21 +246,21 @@
@Test
public void attachAndActiveUseCase() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
mCamera2CameraImpl.onUseCaseActive(useCase1);
verify(mMockOnImageAvailableListener, timeout(4000).atLeastOnce())
.onImageAvailable(any(ImageReader.class));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
public void detachUseCase() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
mCamera2CameraImpl.onUseCaseActive(useCase1);
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
@@ -261,8 +272,8 @@
@Test
public void unopenedCamera() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
}
@@ -270,8 +281,8 @@
@Test
public void closedCamera() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
}
@@ -283,12 +294,12 @@
mCamera2CameraImpl.release();
mCamera2CameraImpl.open();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
mCamera2CameraImpl.onUseCaseActive(useCase1);
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
@@ -297,54 +308,54 @@
mCamera2CameraImpl.open();
mCamera2CameraImpl.release();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
mCamera2CameraImpl.onUseCaseActive(useCase1);
verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
public void attach_oneUseCase_isAttached() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
public void attach_sameUseCases_staysAttached() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
boolean attachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase1);
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
assertThat(attachedAfterFirstAdd).isTrue();
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
public void attach_twoUseCases_bothBecomeAttached() {
UseCase useCase1 = createUseCase();
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
}
@Test
public void detach_detachedUseCase_staysDetached() {
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
}
@@ -353,34 +364,34 @@
public void detachOneAttachedUseCase_fromAttachedUseCases_onlyDetachedSingleUseCase() {
UseCase useCase1 = createUseCase();
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
boolean useCase1isAttachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase1);
boolean useCase2isAttachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase2);
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
assertThat(useCase1isAttachedAfterFirstAdd).isTrue();
assertThat(useCase2isAttachedAfterFirstAdd).isTrue();
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase2));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
}
@Test
public void detachSameAttachedUseCaseTwice_onlyDetachesSameUseCase() {
UseCase useCase1 = createUseCase();
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase2));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
}
@Test
@@ -389,7 +400,7 @@
blockHandler();
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
DeferrableSurface surface1 = useCase1.getSessionConfig().getSurfaces().get(0);
unblockHandler();
@@ -405,13 +416,13 @@
assertThat(surface1).isNotEqualTo(surface2);
- // Old surface is decremented when CameraCaptueSession is closed by new
+ // Old surface is decremented when CameraCaptureSession is closed by new
// CameraCaptureSession.
assertThat(surface1.getUseCount()).isEqualTo(0);
- // New surface is decremented when CameraCaptueSession is closed by
+ // New surface is decremented when CameraCaptureSession is closed by
// mCamera2CameraImpl.release()
assertThat(surface2.getUseCount()).isEqualTo(0);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
@@ -422,7 +433,7 @@
blockHandler();
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
@@ -431,12 +442,12 @@
captureConfigBuilder.addCameraCaptureCallback(captureCallback);
mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
- Arrays.asList(captureConfigBuilder.build()),
+ singletonList(captureConfigBuilder.build()),
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase2));
// Unblock camera handler to make camera operation run quickly .
// To make the single request not able to run in 1st capture session. and verify if it can
@@ -448,7 +459,7 @@
verify(captureCallback, timeout(3000).times(1))
.onCaptureCompleted(any(CameraCaptureResult.class));
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
}
@Test
@@ -461,19 +472,19 @@
UseCase useCase1 = createUseCase();
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
- captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ captureConfigBuilder.setTemplateType(DEFAULT_TEMPLATE_TYPE);
captureConfigBuilder.addSurface(useCase1.getSessionConfig().getSurfaces().get(0));
captureConfigBuilder.addCameraCaptureCallback(captureCallback);
mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
- Arrays.asList(captureConfigBuilder.build()),
+ singletonList(captureConfigBuilder.build()),
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
// Unblock camera handle to make camera operation run quickly .
// To make the single request not able to run in 1st capture session. and verify if it can
@@ -488,7 +499,7 @@
verify(captureCallback, times(0))
.onCaptureCompleted(any(CameraCaptureResult.class));
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase2));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
}
@Test
@@ -559,8 +570,10 @@
mCameraStateRegistry.registerCamera(
mockCamera,
CameraXExecutors.directExecutor(),
- () -> {},
- () -> {});
+ () -> {
+ },
+ () -> {
+ });
mCameraStateRegistry.tryOpenCamera(mockCamera);
mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
@@ -597,18 +610,18 @@
throws InterruptedException {
mCamera2CameraImpl.open();
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
mCamera2CameraImpl.onUseCaseActive(useCase1);
// Wait a little bit for the camera to open.
assertTrue(mSessionStateCallback.waitForOnConfigured(1));
// Remove the useCase1 and trigger the CaptureSession#close().
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
// Create the secondary use case immediately and open it before the first use case closed.
UseCase useCase2 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase2));
mCamera2CameraImpl.onUseCaseActive(useCase2);
// Wait for the secondary capture session is configured.
assertTrue(mSessionStateCallback.waitForOnConfigured(1));
@@ -617,7 +630,7 @@
mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
mockObserver);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase2));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
mCamera2CameraImpl.close();
// Wait for the CLOSED state. If the test fail, the CameraX might in wrong internal state,
@@ -633,27 +646,24 @@
// Create another use case to keep the camera open.
UseCase useCaseDummy = createUseCase();
UseCase useCase = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase, useCaseDummy));
+ mCamera2CameraImpl.attachUseCases(asList(useCase, useCaseDummy));
mCamera2CameraImpl.onUseCaseActive(useCase);
// Wait a little bit for the camera to open.
assertTrue(mSessionStateCallback.waitForOnConfigured(2));
// Remove the useCase and trigger the CaptureSession#close().
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase));
assertTrue(mSessionStateCallback.waitForOnClosed(2));
}
// Blocks the camera thread handler.
private void blockHandler() {
- sCameraHandler.post(new Runnable() {
- @Override
- public void run() {
- try {
- mSemaphore.acquire();
- } catch (InterruptedException e) {
-
- }
+ sCameraHandler.post(() -> {
+ try {
+ mSemaphore.acquire();
+ } catch (InterruptedException e) {
+ // Do nothing.
}
});
}
@@ -663,11 +673,14 @@
mSemaphore.release();
}
+ @NonNull
private UseCase createUseCase() {
- return createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
+ return createUseCase(DEFAULT_TEMPLATE_TYPE);
}
- private UseCase createUseCase(int template, boolean isZslDisabled) {
+ @NonNull
+ private UseCase createUseCase(int template) {
+ boolean isZslDisabled = getDefaultZslDisabled(template);
FakeUseCaseConfig.Builder configBuilder =
new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
@@ -676,12 +689,10 @@
return createUseCase(configBuilder.getUseCaseConfig(), template);
}
+ @NonNull
private UseCase createUseCase(@NonNull FakeUseCaseConfig config, int template) {
- CameraSelector selector =
- new CameraSelector.Builder().requireLensFacing(
- CameraSelector.LENS_FACING_BACK).build();
- TestUseCase testUseCase = new TestUseCase(template, config,
- selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
+ TestUseCase testUseCase = new TestUseCase(template, config, DEFAULT_BACK_CAMERA,
+ mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
testUseCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
mFakeUseCases.add(testUseCase);
@@ -693,8 +704,8 @@
TestUseCase useCase1 = spy((TestUseCase) createUseCase());
TestUseCase useCase2 = spy((TestUseCase) createUseCase());
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -704,7 +715,7 @@
verify(useCase1, times(1)).onStateAttached();
verify(useCase2, times(1)).onStateAttached();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
}
@Test
@@ -713,9 +724,9 @@
TestUseCase useCase2 = spy((TestUseCase) createUseCase());
TestUseCase useCase3 = spy((TestUseCase) createUseCase());
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+ mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2, useCase3));
+ mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2, useCase3));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -730,13 +741,14 @@
private boolean isCameraControlActive(Camera2CameraControlImpl camera2CameraControlImpl) {
ListenableFuture<Void> listenableFuture = camera2CameraControlImpl.setZoomRatio(2.0f);
try {
- // setZoom() will fail immediately when Cameracontrol is not active.
+ // setZoom() will fail immediately when CameraControl is not active.
listenableFuture.get(50, TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof CameraControl.OperationCanceledException) {
return false;
}
} catch (InterruptedException | TimeoutException e) {
+ // Do nothing.
}
return true;
}
@@ -750,12 +762,12 @@
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(isCameraControlActive(camera2CameraControlImpl)).isTrue();
- mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
}
@Test
@@ -764,11 +776,11 @@
(Camera2CameraControlImpl) mCamera2CameraImpl.getCameraControlInternal();
UseCase useCase1 = createUseCase();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(isCameraControlActive(camera2CameraControlImpl)).isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+ mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(isCameraControlActive(camera2CameraControlImpl)).isFalse();
@@ -776,9 +788,9 @@
@Test
public void attachUseCaseWithTemplatePreview() throws InterruptedException {
- UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
+ UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview));
+ mCamera2CameraImpl.attachUseCases(singletonList(preview));
mCamera2CameraImpl.onUseCaseActive(preview);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -793,15 +805,15 @@
assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
.isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview));
+ mCamera2CameraImpl.detachUseCases(singletonList(preview));
}
@Test
public void attachUseCaseWithTemplateRecord() throws InterruptedException {
- UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
- UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
+ UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, record));
+ mCamera2CameraImpl.attachUseCases(asList(preview, record));
mCamera2CameraImpl.onUseCaseActive(preview);
mCamera2CameraImpl.onUseCaseActive(record);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -817,7 +829,7 @@
assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
.isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_RECORD);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, record));
+ mCamera2CameraImpl.detachUseCases(asList(preview, record));
}
@SdkSuppress(minSdkVersion = 23)
@@ -826,14 +838,10 @@
if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
return;
}
- UseCase preview = createUseCase(
- CameraDevice.TEMPLATE_PREVIEW,
- /* isZslDisabled = */false);
- UseCase zsl = createUseCase(
- CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
- /* isZslDisabled = */false);
+ UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, zsl));
+ mCamera2CameraImpl.attachUseCases(asList(preview, zsl));
mCamera2CameraImpl.onUseCaseActive(preview);
mCamera2CameraImpl.onUseCaseActive(zsl);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -852,7 +860,7 @@
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isFalse();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, zsl));
+ mCamera2CameraImpl.detachUseCases(asList(preview, zsl));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(mCamera2CameraImpl.getCameraControlInternal()
.isZslDisabledByByUserCaseConfig()).isFalse();
@@ -864,13 +872,11 @@
if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
return;
}
- UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
- false);
- UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
- UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
- /* isZslDisabled = */false);
+ UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
+ UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, record, zsl));
+ mCamera2CameraImpl.attachUseCases(asList(preview, record, zsl));
mCamera2CameraImpl.onUseCaseActive(preview);
mCamera2CameraImpl.onUseCaseActive(record);
mCamera2CameraImpl.onUseCaseActive(zsl);
@@ -890,7 +896,7 @@
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, record, zsl));
+ mCamera2CameraImpl.detachUseCases(asList(preview, record, zsl));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
@@ -903,13 +909,11 @@
if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
return;
}
- UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
- false);
- UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
- UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
- /* isZslDisabled = */false);
+ UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
+ UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, zsl));
+ mCamera2CameraImpl.attachUseCases(asList(preview, zsl));
mCamera2CameraImpl.onUseCaseActive(preview);
mCamera2CameraImpl.onUseCaseActive(zsl);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -918,7 +922,7 @@
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isFalse();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(record));
+ mCamera2CameraImpl.attachUseCases(singletonList(record));
mCamera2CameraImpl.onUseCaseActive(record);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -926,32 +930,32 @@
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(record));
+ mCamera2CameraImpl.detachUseCases(singletonList(record));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isFalse();
- mCamera2CameraImpl.attachUseCases(Arrays.asList(record));
+ mCamera2CameraImpl.attachUseCases(singletonList(record));
mCamera2CameraImpl.onUseCaseActive(record);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(zsl));
+ mCamera2CameraImpl.detachUseCases(singletonList(zsl));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview));
+ mCamera2CameraImpl.detachUseCases(singletonList(preview));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
.isTrue();
- mCamera2CameraImpl.detachUseCases(Arrays.asList(record));
+ mCamera2CameraImpl.detachUseCases(singletonList(record));
HandlerUtil.waitForLooperToIdle(sCameraHandler);
assertThat(
mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
@@ -961,8 +965,7 @@
@SdkSuppress(minSdkVersion = 23)
@Test
public void zslDisabled_whenHighResolutionIsEnabled() throws InterruptedException {
- UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
- /* isZslDisabled = */false);
+ UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
// Creates a test use case with high resolution enabled.
ResolutionSelector highResolutionSelector =
@@ -983,7 +986,7 @@
return;
}
- mCamera2CameraImpl.attachUseCases(Arrays.asList(zsl, highResolutionUseCase));
+ mCamera2CameraImpl.attachUseCases(asList(zsl, highResolutionUseCase));
mCamera2CameraImpl.onUseCaseActive(zsl);
mCamera2CameraImpl.onUseCaseActive(highResolutionUseCase);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -1002,14 +1005,14 @@
&& pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT)) {
Camera2CameraInfoImpl pairedCamera2CameraInfo = new Camera2CameraInfoImpl(
mPairedCameraId, cameraManagerCompat);
- mPairedCamera2CameraImpl = new Camera2CameraImpl(
+ Camera2CameraImpl pairedCamera2CameraImpl = new Camera2CameraImpl(
cameraManagerCompat, mPairedCameraId, pairedCamera2CameraInfo,
mCameraCoordinator,
mCameraStateRegistry, sCameraExecutor, sCameraHandler,
DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext()));
mCameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(
new HashMap<String, CameraSelector>() {{
- put(mCameraId, CameraSelector.DEFAULT_BACK_CAMERA);
+ put(mCameraId, DEFAULT_BACK_CAMERA);
put(mPairedCameraId, CameraSelector.DEFAULT_FRONT_CAMERA);
}});
mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
@@ -1017,9 +1020,8 @@
CAMERA_OPERATING_MODE_SINGLE, CAMERA_OPERATING_MODE_CONCURRENT);
// Act.
- UseCase preview1 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
- false);
- mCamera2CameraImpl.attachUseCases(Arrays.asList(preview1));
+ UseCase preview1 = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ mCamera2CameraImpl.attachUseCases(singletonList(preview1));
mCamera2CameraImpl.onUseCaseActive(preview1);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -1029,10 +1031,9 @@
verify(mMockRepeatingCaptureCallback, never()).onCaptureCompleted(captor.capture());
// Act.
- UseCase preview2 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
- false);
- mPairedCamera2CameraImpl.attachUseCases(Arrays.asList(preview2));
- mPairedCamera2CameraImpl.onUseCaseActive(preview2);
+ UseCase preview2 = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+ pairedCamera2CameraImpl.attachUseCases(singletonList(preview2));
+ pairedCamera2CameraImpl.onUseCaseActive(preview2);
HandlerUtil.waitForLooperToIdle(sCameraHandler);
// Assert.
@@ -1043,40 +1044,18 @@
assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
.isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
- mCamera2CameraImpl.detachUseCases(Arrays.asList(preview1));
+ mCamera2CameraImpl.detachUseCases(singletonList(preview1));
}
}
- private DeferrableSurface getUseCaseSurface(UseCase useCase) {
- return useCase.getSessionConfig().getSurfaces().get(0);
- }
-
private void changeUseCaseSurface(UseCase useCase) {
useCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
}
- private void waitForCameraClose(Camera2CameraImpl camera2CameraImpl)
- throws InterruptedException {
- Semaphore semaphore = new Semaphore(0);
-
- Observable.Observer<CameraInternal.State> observer =
- new Observable.Observer<CameraInternal.State>() {
- @Override
- public void onNewData(@Nullable CameraInternal.State value) {
- // Ignore any transient states.
- if (value == CameraInternal.State.CLOSED) {
- semaphore.release();
- }
- }
-
- @Override
- public void onError(@NonNull Throwable t) { /* Ignore any transient errors. */ }
- };
-
- camera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(), observer);
-
- // Wait until camera reaches closed state
- semaphore.acquire();
+ private static boolean getDefaultZslDisabled(int templateType) {
+ Boolean isZslDisabled = DEFAULT_TEMPLATE_TO_ZSL_DISABLED.get(templateType);
+ checkState(isZslDisabled != null, "No default mapping from template to zsl disabled");
+ return isZslDisabled;
}
public static class TestUseCase extends FakeUseCase {
@@ -1084,7 +1063,6 @@
HandlerThread mHandlerThread = new HandlerThread("HandlerThread");
Handler mHandler;
FakeUseCaseConfig mConfig;
- private String mCameraId;
private DeferrableSurface mDeferrableSurface;
private final CameraCaptureCallback mRepeatingCaptureCallback;
private final int mTemplate;
@@ -1104,13 +1082,15 @@
mRepeatingCaptureCallback = repeatingCaptureCallback;
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- Integer lensFacing =
+ int lensFacing =
cameraSelector.getLensFacing() == null ? CameraSelector.LENS_FACING_BACK :
cameraSelector.getLensFacing();
- mCameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing);
- bindToCamera(new FakeCamera(mCameraId, null,
- new FakeCameraInfoInternal(mCameraId, 0, lensFacing)),
- null, null);
+ String cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing);
+ if (cameraId == null) {
+ cameraId = "FakeId";
+ }
+ bindToCamera(new FakeCamera(cameraId, null,
+ new FakeCameraInfoInternal(cameraId, 0, lensFacing)), null, null);
updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
index 04c3858..7977243 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
@@ -70,7 +70,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
@OptIn(markerClass = ExperimentalCamera2Interop.class)
-@SdkSuppress(minSdkVersion = 22) // b/272066193
+@SdkSuppress(minSdkVersion = 21)
public final class Camera2CameraControlDeviceTest {
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private CameraSelector mCameraSelector;
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
index 29a28b7..471ab2c 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
@@ -71,7 +71,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
@OptIn(markerClass = ExperimentalCamera2Interop.class)
-@SdkSuppress(minSdkVersion = 22) // b/272066193
+@SdkSuppress(minSdkVersion = 21)
public final class Camera2InteropDeviceTest {
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private CameraSelector mCameraSelector;
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 e893e30..d53901a 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
@@ -40,6 +40,7 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
+import androidx.camera.camera2.internal.compat.params.DynamicRangesCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
@@ -48,6 +49,7 @@
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.Logger;
@@ -72,6 +74,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -435,6 +438,15 @@
return size != null ? Arrays.asList(size) : Collections.emptyList();
}
+ @NonNull
+ @Override
+ public Set<DynamicRange> getSupportedDynamicRanges() {
+ DynamicRangesCompat dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(
+ mCameraCharacteristicsCompat);
+
+ return dynamicRangesCompat.getSupportedDynamicRanges();
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
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 0c0a626..235acef 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
@@ -18,6 +18,8 @@
import static android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES;
+import static androidx.camera.core.DynamicRange.SDR;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -31,6 +33,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.DynamicRangeProfiles;
import android.os.Build;
import android.util.Pair;
import android.util.Range;
@@ -45,6 +48,7 @@
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
@@ -94,8 +98,10 @@
private static final boolean CAMERA0_FLASH_INFO_BOOLEAN = true;
private static final int CAMERA0_SUPPORTED_PRIVATE_REPROCESSING =
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING;
+ private static final int CAMERA0_SUPPORTED_DYNAMIC_RANGE_TEN_BIT =
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT;
private static final int[] CAMERA0_SUPPORTED_CAPABILITIES = new int[] {
- CAMERA0_SUPPORTED_PRIVATE_REPROCESSING,
+ CAMERA0_SUPPORTED_PRIVATE_REPROCESSING, CAMERA0_SUPPORTED_DYNAMIC_RANGE_TEN_BIT
};
private static final float[] CAMERA0_LENS_FOCAL_LENGTH = new float[]{
3.0F,
@@ -105,13 +111,15 @@
private static final SizeF CAMERA0_SENSOR_PHYSICAL_SIZE = new SizeF(1.5F, 1F);
private static final Rect CAMERA0_SENSOR_ACTIVE_ARRAY_SIZE = new Rect(0, 0, 1920, 1080);
private static final Size CAMERA0_SENSOR_PIXEL_ARRAY_SIZE = new Size(1920, 1080);
-
private static final Range<?>[] CAMERA0_AE_FPS_RANGES = {
new Range<>(12, 30),
new Range<>(24, 24),
new Range<>(30, 30),
new Range<>(60, 60)
};
+ private static final DynamicRangeProfiles CAMERA0_DYNAMIC_RANGE_PROFILES =
+ new DynamicRangeProfiles(new long[]{DynamicRangeProfiles.HLG10, 0, 0});
+
private static final String CAMERA1_ID = "1";
private static final int CAMERA1_SUPPORTED_HARDWARE_LEVEL =
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3;
@@ -135,6 +143,8 @@
new Range<>(12, 30),
new Range<>(30, 30),
};
+ private static final DynamicRange HLG10 = new DynamicRange(DynamicRange.FORMAT_HLG,
+ DynamicRange.BIT_DEPTH_10_BIT);
private CameraCharacteristicsCompat mCameraCharacteristics0;
private CameraManagerCompat mCameraManagerCompat;
@@ -146,7 +156,7 @@
@Test
public void canCreateCameraInfo() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -156,7 +166,7 @@
@Test
public void cameraInfo_canReturnSensorOrientation() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -167,7 +177,7 @@
@Test
public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -186,7 +196,7 @@
@Test
public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
@@ -204,7 +214,7 @@
@Test
public void cameraInfo_canReturnLensFacing() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -214,7 +224,7 @@
@Test
public void cameraInfo_canReturnHasFlashUnit_forBackCamera()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -224,7 +234,7 @@
@Test
public void cameraInfo_canReturnHasFlashUnit_forFrontCamera()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraInfoInternal cameraInfoInternal =
new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
@@ -234,7 +244,7 @@
@Test
public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl camera2CameraInfoImpl =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -245,7 +255,7 @@
@Test
public void cameraInfoWithCameraControl_canReturnTorchState()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
Camera2CameraInfoImpl camera2CameraInfoImpl =
@@ -257,7 +267,7 @@
@Test
public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl camera2CameraInfoImpl =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -278,7 +288,7 @@
@Test
public void cameraInfoWithCameraControl_getZoom_valueIsCorrect()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
@@ -293,7 +303,7 @@
@Test
public void cameraInfoWithoutCameraControl_getDetaultZoomState()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl camera2CameraInfoImpl =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -304,7 +314,7 @@
@Test
public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl camera2CameraInfoImpl =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -324,7 +334,7 @@
@Test
public void cameraInfoWithCameraControl_canReturnExposureState()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
when(mExposureControl.getExposureState()).thenReturn(exposureState);
@@ -339,7 +349,7 @@
@Test
public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl camera2CameraInfoImpl =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -359,7 +369,7 @@
@Test
public void cameraInfo_getImplementationType_legacy() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final CameraInfoInternal cameraInfo =
new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -369,7 +379,7 @@
@Test
public void cameraInfo_getImplementationType_noneLegacy() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(
CAMERA1_ID, mCameraManagerCompat);
@@ -380,7 +390,7 @@
@Test
public void addSessionCameraCaptureCallback_isCalledToCameraControl()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA1_ID, mCameraManagerCompat);
@@ -396,7 +406,7 @@
@Test
public void removeSessionCameraCaptureCallback_isCalledToCameraControl()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA1_ID, mCameraManagerCompat);
@@ -411,7 +421,7 @@
@Test
public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA1_ID, mCameraManagerCompat);
@@ -427,7 +437,7 @@
@Test
public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA1_ID, mCameraManagerCompat);
@@ -451,7 +461,7 @@
@Test
public void cameraInfoWithCameraControl_canReturnIsFocusMeteringSupported()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -473,7 +483,7 @@
@Test
public void canReturnCameraCharacteristicsMapWithPhysicalCameras()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
CameraCharacteristics characteristics0 = mock(CameraCharacteristics.class);
CameraCharacteristics characteristicsPhysical2 = mock(CameraCharacteristics.class);
@@ -498,7 +508,7 @@
@Test
public void canReturnCameraCharacteristicsMapWithMainCamera()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", mCameraManagerCompat);
Map<String, CameraCharacteristics> map = impl.getCameraCharacteristicsMap();
@@ -510,7 +520,7 @@
@Test
public void cameraInfoWithCameraControl_canReturnIsPrivateReprocessingSupported()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -521,7 +531,7 @@
@Config(minSdk = 23)
@Test
public void isZslSupported_apiVersionMet_returnTrue() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -532,7 +542,7 @@
@Config(maxSdk = 22)
@Test
public void isZslSupported_apiVersionNotMet_returnFalse() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -543,7 +553,7 @@
@Test
public void isZslSupported_noReprocessingCapability_returnFalse()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ false);
+ init(/* hasAvailableCapabilities = */ false);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -558,7 +568,7 @@
ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-F936B");
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -573,7 +583,7 @@
ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G973");
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -588,7 +598,7 @@
ReflectionHelpers.setStaticField(Build.class, "BRAND", "xiaomi");
ReflectionHelpers.setStaticField(Build.class, "MODEL", "Mi 8");
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -603,7 +613,7 @@
ReflectionHelpers.setStaticField(Build.class, "BRAND", "xiaomi");
ReflectionHelpers.setStaticField(Build.class, "MODEL", "Mi A1");
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
CAMERA0_ID, mCameraManagerCompat);
@@ -613,7 +623,7 @@
@Test
public void canReturnSupportedResolutions() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ true);
+ init(/* hasAvailableCapabilities = */ true);
Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
mCameraManagerCompat);
@@ -637,7 +647,7 @@
@Test
public void cameraInfo_canReturnIntrinsicZoomRatio() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ false);
+ init(/* hasAvailableCapabilities = */ false);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA2_ID,
mCameraManagerCompat);
@@ -649,7 +659,7 @@
@Test
public void cameraInfo_canReturnSupportedFpsRanges() throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ false);
+ init(/* hasAvailableCapabilities = */ false);
final Camera2CameraInfoImpl cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
mCameraManagerCompat);
@@ -666,7 +676,7 @@
@Test
public void cameraInfo_returnsEmptyFpsRanges_whenNotSupported()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ false);
+ init(/* hasAvailableCapabilities = */ false);
final Camera2CameraInfoImpl cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
mCameraManagerCompat);
@@ -679,7 +689,7 @@
@Test
public void cameraInfo_checkDefaultCameraIntrinsicZoomRatio()
throws CameraAccessExceptionCompat {
- init(/* hasReprocessingCapabilities = */ false);
+ init(/* hasAvailableCapabilities = */ false);
final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
mCameraManagerCompat);
@@ -690,6 +700,31 @@
assertThat(resultZoomRatio).isEqualTo(1.0F);
}
+ @Config(minSdk = 33)
+ @Test
+ public void apiVersionMet_canReturnSupportedDynamicRanges() throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA0_ID, mCameraManagerCompat);
+
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+ assertThat(supportedDynamicRanges).containsExactly(SDR, HLG10);
+ }
+
+ @Config(maxSdk = 32)
+ @Test
+ public void apiVersionNotMet_canReturnSupportedDynamicRanges()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA0_ID, mCameraManagerCompat);
+
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+ assertThat(supportedDynamicRanges).containsExactly(SDR);
+ }
+
private CameraManagerCompat initCameraManagerWithPhysicalIds(
List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
@@ -701,8 +736,8 @@
return CameraManagerCompat.from(cameraManagerImpl);
}
- private void init(boolean hasReprocessingCapabilities) throws CameraAccessExceptionCompat {
- initCameras(hasReprocessingCapabilities);
+ private void init(boolean hasAvailableCapabilities) throws CameraAccessExceptionCompat {
+ initCameras(hasAvailableCapabilities);
mCameraManagerCompat =
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
@@ -720,7 +755,7 @@
when(mMockCameraControl.getFocusMeteringControl()).thenReturn(mFocusMeteringControl);
}
- private void initCameras(boolean hasReprocessingCapabilities) {
+ private void initCameras(boolean hasAvailableCapabilities) {
// **** Camera 0 characteristics ****//
CameraCharacteristics characteristics0 =
ShadowCameraCharacteristics.newCameraCharacteristics();
@@ -771,8 +806,14 @@
CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
CAMERA0_AE_FPS_RANGES);
+ if (Build.VERSION.SDK_INT >= 33) {
+ shadowCharacteristics0.set(
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+ CAMERA0_DYNAMIC_RANGE_PROFILES);
+ }
+
// Mock the request capability
- if (hasReprocessingCapabilities) {
+ if (hasAvailableCapabilities) {
shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
CAMERA0_SUPPORTED_CAPABILITIES);
}
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 4614cf8..2f9f00d 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -359,6 +359,12 @@
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.ResolutionInfo? getResolutionInfo();
method public int getTargetRotation();
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index fc38de4..7f210f6 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -376,6 +376,12 @@
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.ResolutionInfo? getResolutionInfo();
method public int getTargetRotation();
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 4614cf8..2f9f00d 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -359,6 +359,12 @@
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.ResolutionInfo? getResolutionInfo();
method public int getTargetRotation();
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index 287d3d4a..71f4651 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -22,9 +22,9 @@
import android.util.Size
import android.view.Surface
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
-import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.ImageOutputConfig
@@ -286,8 +286,8 @@
}
@Test
- fun setMirrorModeFrontOn_isMirroringRequiredDependsOnCamera() {
- val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_FRONT_ON)
+ fun setMirrorModeOnFrontOnly_isMirroringRequiredDependsOnCamera() {
+ val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_ON_FRONT_ONLY)
assertThat(fakeUseCase.isMirroringRequired(fakeCamera)).isFalse()
assertThat(fakeUseCase.isMirroringRequired(fakeFrontCamera)).isTrue()
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
index 72e2efa..49af138 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
@@ -23,13 +23,12 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-// TODO: to public API
/**
* The mirror mode.
*
+ * <p>Constants describing image mirroring transforms.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class MirrorMode {
/** No mirror effect will be applied. */
public static final int MIRROR_MODE_OFF = 0;
@@ -38,17 +37,17 @@
public static final int MIRROR_MODE_ON = 1;
/**
- * The mirroring effect is applied only when the lens facing of the associated camera is
+ * The mirror effect is applied only when the lens facing of the associated camera is
* {@link CameraSelector#LENS_FACING_FRONT}.
*/
- public static final int MIRROR_MODE_FRONT_ON = 2;
+ public static final int MIRROR_MODE_ON_FRONT_ONLY = 2;
private MirrorMode() {
}
/**
*/
- @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_FRONT_ON})
+ @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_ON_FRONT_ONLY})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface Mirror {
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 f994a2d..1164a47 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
@@ -17,7 +17,7 @@
package androidx.camera.core;
import static androidx.camera.core.CameraEffect.PREVIEW;
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
@@ -737,7 +737,7 @@
public static final class Defaults implements ConfigProvider<PreviewConfig> {
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 2;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
- private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_FRONT_ON;
+ private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_ON_FRONT_ONLY;
private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
new ResolutionSelector.Builder().setAspectRatioStrategy(
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 3257ece..0aa20f0 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
@@ -16,12 +16,13 @@
package androidx.camera.core;
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.camera.core.processing.TargetUtils.isSuperset;
import static androidx.core.util.Preconditions.checkArgument;
@@ -30,6 +31,7 @@
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ImageReader;
+import android.util.Range;
import android.util.Size;
import android.view.Surface;
@@ -353,6 +355,17 @@
}
/**
+ * Returns the target frame rate range for the associated VideoCapture use case.
+ *
+ * @return The target frame rate.
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected Range<Integer> getTargetFramerateInternal() {
+ return mCurrentConfig.getTargetFramerate(FRAME_RATE_RANGE_UNSPECIFIED);
+ }
+
+ /**
* Returns the mirror mode.
*
* <p>If mirror mode is not set, defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
@@ -376,7 +389,7 @@
return false;
case MIRROR_MODE_ON:
return true;
- case MIRROR_MODE_FRONT_ON:
+ case MIRROR_MODE_ON_FRONT_ONLY:
return camera.isFrontFacing();
default:
throw new AssertionError("Unknown mirrorMode: " + mirrorMode);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index f7ec5a0..84beaf0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -25,10 +25,12 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.DynamicRange;
import androidx.core.util.Preconditions;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -103,6 +105,14 @@
@NonNull
List<Size> getSupportedHighResolutions(int format);
+ /**
+ * Returns the supported dynamic ranges of this camera.
+ *
+ * @return a set of supported dynamic range, or an empty set if no dynamic range is supported.
+ */
+ @NonNull
+ Set<DynamicRange> getSupportedDynamicRanges();
+
/** {@inheritDoc} */
@NonNull
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
index f37cb38..d8152fa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
@@ -85,6 +85,9 @@
/** Constant representing bit depth 8. */
public static final int BIT_DEPTH_8 = 8;
+ /** Constant representing bit depth 10. */
+ public static final int BIT_DEPTH_10 = 10;
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({H263, H264, HEVC, VP8, MPEG_4_SP, VP9, DOLBY_VISION, AV1,
MediaRecorder.VideoEncoder.DEFAULT})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index 390e103..9cef56c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -414,7 +414,7 @@
* Sets the mirror mode of the intended target for images from this configuration.
*
* <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
- * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
+ * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
*
* @param mirrorMode The mirror mode of the intended target.
* @return The current Builder.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 3556349..345fe82 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -18,6 +18,7 @@
import static androidx.camera.core.CameraEffect.PREVIEW;
import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
import static androidx.camera.core.processing.TargetUtils.getNumberOfTargets;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
@@ -554,8 +555,17 @@
if (!newUseCases.isEmpty()) {
Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
Map<UseCaseConfig<?>, List<Size>> configToSupportedSizesMap = new HashMap<>();
+ Rect sensorRect;
+ try {
+ sensorRect = ((CameraControlInternal) getCameraControl()).getSensorRect();
+ } catch (NullPointerException e) {
+ // TODO(b/274531208): Remove the unnecessary SENSOR_INFO_ACTIVE_ARRAY_SIZE NPE
+ // check related code only which is used for robolectric tests
+ sensorRect = null;
+ }
SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
- (CameraInfoInternal) getCameraInfo());
+ (CameraInfoInternal) getCameraInfo(),
+ sensorRect != null ? rectToSize(sensorRect) : null);
for (UseCase useCase : newUseCases) {
ConfigPair configPair = configPairMap.get(useCase);
// Combine with default configuration.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index 9f7524c..2a84e43 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -74,11 +74,14 @@
private final boolean mIsSensorLandscapeResolution;
private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
- SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal) {
+ SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
+ @Nullable Size activeArraySize) {
mCameraInfoInternal = cameraInfoInternal;
mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
mLensFacing = mCameraInfoInternal.getLensFacing();
- mFullFovRatio = calculateFullFovRatio(mCameraInfoInternal);
+ mFullFovRatio = activeArraySize != null ? calculateFullFovRatioFromActiveArraySize(
+ activeArraySize) : calculateFullFovRatioFromSupportedOutputSizes(
+ mCameraInfoInternal);
// Determines the sensor resolution orientation info by the full FOV ratio.
mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
>= mFullFovRatio.getDenominator() : true;
@@ -87,6 +90,14 @@
}
/**
+ * Calculates the full FOV ratio by the active array size.
+ */
+ @NonNull
+ private Rational calculateFullFovRatioFromActiveArraySize(@NonNull Size activeArraySize) {
+ return new Rational(activeArraySize.getWidth(), activeArraySize.getHeight());
+ }
+
+ /**
* Calculates the full FOV ratio by the output sizes retrieved from CameraInfoInternal.
*
* <p>For most devices, the full FOV ratio should match the aspect ratio of the max supported
@@ -94,7 +105,8 @@
* test to fail if it is not set in the test environment.
*/
@Nullable
- private Rational calculateFullFovRatio(@NonNull CameraInfoInternal cameraInfoInternal) {
+ private Rational calculateFullFovRatioFromSupportedOutputSizes(
+ @NonNull CameraInfoInternal cameraInfoInternal) {
List<Size> jpegOutputSizes = cameraInfoInternal.getSupportedResolutions(ImageFormat.JPEG);
if (jpegOutputSizes.isEmpty()) {
return null;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
index d144363..40b9b28 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
@@ -29,6 +29,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
+import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.SurfaceRequest;
@@ -42,6 +43,7 @@
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -53,13 +55,14 @@
@RequiresApi(21)
public class DefaultSurfaceProcessor implements SurfaceProcessorInternal,
SurfaceTexture.OnFrameAvailableListener {
+ private static final String TAG = "DefaultSurfaceProcessor";
private final OpenGlRenderer mGlRenderer;
@VisibleForTesting
final HandlerThread mGlThread;
private final Executor mGlExecutor;
@VisibleForTesting
final Handler mGlHandler;
- private final AtomicBoolean mIsReleased = new AtomicBoolean(false);
+ private final AtomicBoolean mIsReleaseRequested = new AtomicBoolean(false);
private final float[] mTextureMatrix = new float[16];
private final float[] mSurfaceOutputMatrix = new float[16];
// Map of current set of available outputs. Only access this on GL thread.
@@ -68,6 +71,8 @@
// Only access this on GL thread.
private int mInputSurfaceCount = 0;
+ // Only access this on GL thread.
+ private boolean mIsReleased = false;
/** Constructs {@link DefaultSurfaceProcessor} with default shaders. */
DefaultSurfaceProcessor() {
@@ -99,11 +104,11 @@
*/
@Override
public void onInputSurface(@NonNull SurfaceRequest surfaceRequest) {
- if (mIsReleased.get()) {
+ if (mIsReleaseRequested.get()) {
surfaceRequest.willNotProvideSurface();
return;
}
- mGlExecutor.execute(() -> {
+ executeSafely(() -> {
mInputSurfaceCount++;
SurfaceTexture surfaceTexture = new SurfaceTexture(mGlRenderer.getTextureName());
surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
@@ -117,7 +122,7 @@
checkReadyToRelease();
});
surfaceTexture.setOnFrameAvailableListener(this, mGlHandler);
- });
+ }, surfaceRequest::willNotProvideSurface);
}
/**
@@ -125,11 +130,11 @@
*/
@Override
public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
- if (mIsReleased.get()) {
+ if (mIsReleaseRequested.get()) {
surfaceOutput.close();
return;
}
- mGlExecutor.execute(() -> {
+ executeSafely(() -> {
Surface surface = surfaceOutput.getSurface(mGlExecutor, event -> {
surfaceOutput.close();
Surface removedSurface = mOutputSurfaces.remove(surfaceOutput);
@@ -139,7 +144,7 @@
});
mGlRenderer.registerOutputSurface(surface);
mOutputSurfaces.put(surfaceOutput, surface);
- });
+ }, surfaceOutput::close);
}
/**
@@ -147,10 +152,13 @@
*/
@Override
public void release() {
- if (mIsReleased.getAndSet(true)) {
+ if (mIsReleaseRequested.getAndSet(true)) {
return;
}
- mGlExecutor.execute(this::checkReadyToRelease);
+ executeSafely(() -> {
+ mIsReleased = true;
+ checkReadyToRelease();
+ });
}
/**
@@ -158,7 +166,7 @@
*/
@Override
public void onFrameAvailable(@NonNull SurfaceTexture surfaceTexture) {
- if (mIsReleased.get()) {
+ if (mIsReleaseRequested.get()) {
// Ignore frame update if released.
return;
}
@@ -183,7 +191,7 @@
@WorkerThread
private void checkReadyToRelease() {
- if (mIsReleased.get() && mInputSurfaceCount == 0) {
+ if (mIsReleased && mInputSurfaceCount == 0) {
// Once release is called, we can stop sending frame to output surfaces.
for (SurfaceOutput surfaceOutput : mOutputSurfaces.keySet()) {
surfaceOutput.close();
@@ -196,7 +204,7 @@
private void initGlRenderer(@NonNull ShaderProvider shaderProvider) {
ListenableFuture<Void> initFuture = CallbackToFutureAdapter.getFuture(completer -> {
- mGlExecutor.execute(() -> {
+ executeSafely(() -> {
try {
mGlRenderer.init(shaderProvider);
completer.set(null);
@@ -220,6 +228,27 @@
}
}
+ private void executeSafely(@NonNull Runnable runnable) {
+ executeSafely(runnable, () -> {
+ // Do nothing.
+ });
+ }
+
+ private void executeSafely(@NonNull Runnable runnable, @NonNull Runnable onFailure) {
+ try {
+ mGlExecutor.execute(() -> {
+ if (mIsReleased) {
+ onFailure.run();
+ } else {
+ runnable.run();
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ Logger.w(TAG, "Unable to executor runnable", e);
+ onFailure.run();
+ }
+ }
+
/**
* Factory class that produces {@link DefaultSurfaceProcessor}.
*
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 36df55c..a6289c1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -16,8 +16,8 @@
package androidx.camera.core;
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static com.google.common.truth.Truth.assertThat;
@@ -187,7 +187,7 @@
@Test(expected = UnsupportedOperationException.class)
public void setMirrorMode_throwException() {
- new ImageAnalysis.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON);
+ new ImageAnalysis.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY);
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index bff9468..65f6131 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -33,7 +33,7 @@
import androidx.camera.core.ImageCapture.ImageCaptureRequest
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CameraFactory
@@ -193,7 +193,7 @@
@Test(expected = UnsupportedOperationException::class)
fun setMirrorMode_throwException() {
- ImageCapture.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+ ImageCapture.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 2968588..b6933d2 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -30,7 +30,7 @@
import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
@@ -203,14 +203,14 @@
}
@Test
- fun defaultMirrorModeIsFrontOn() {
+ fun defaultMirrorModeIsOnFrontOnly() {
val preview = Preview.Builder().build()
- assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_FRONT_ON)
+ assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
}
@Test(expected = UnsupportedOperationException::class)
fun setMirrorMode_throwException() {
- Preview.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+ Preview.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index e7265d0..96211ee 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -80,6 +80,8 @@
Size(640, 480),
Size(360, 480)
)
+private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
+private val PORTRAIT_ACTIVE_ARRAY_SIZE = Size(1440, 1920)
/**
* Unit tests for [SupportedOutputSizesSorter].
@@ -92,7 +94,8 @@
setSupportedResolutions(ImageFormat.JPEG, DEFAULT_SUPPORTED_SIZES)
setSupportedHighResolutions(ImageFormat.JPEG, HIGH_RESOLUTION_SUPPORTED_SIZES)
}
- private val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+ private val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
@Test
fun canSelectCustomOrderedResolutions() {
@@ -101,7 +104,8 @@
val cameraInfoInternal = FakeCameraInfoInternal().apply {
setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
}
- val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+ val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
// Sets up the custom ordered resolutions
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
@@ -122,7 +126,8 @@
val cameraInfoInternal = FakeCameraInfoInternal().apply {
setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
}
- val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+ val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
// Sets up the custom supported resolutions
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
@@ -367,7 +372,8 @@
val cameraInfoInternal = FakeCameraInfoInternal().apply {
setSupportedResolutions(ImageFormat.JPEG, PORTRAIT_SUPPORTED_SIZES)
}
- val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+ val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, PORTRAIT_ACTIVE_ARRAY_SIZE)
verifySupportedOutputSizesWithResolutionSelectorSettings(
outputSizesSorter = supportedOutputSizesSorter,
preferredAspectRatio = AspectRatio.RATIO_16_9,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
index 4c84b93..170b11b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
@@ -155,7 +155,25 @@
int videoFrameWidth,
int videoFrameHeight
) {
- VideoProfileProxy videoProfile = VideoProfileProxy.create(
+ VideoProfileProxy videoProfile = createFakeVideoProfileProxy(videoFrameWidth,
+ videoFrameHeight);
+ AudioProfileProxy audioProfile = createFakeAudioProfileProxy();
+
+ return ImmutableEncoderProfilesProxy.create(
+ DEFAULT_DURATION,
+ DEFAULT_OUTPUT_FORMAT,
+ Collections.singletonList(audioProfile),
+ Collections.singletonList(videoProfile)
+ );
+ }
+
+ /** A utility method to create a VideoProfileProxy with some default values. */
+ @NonNull
+ public static VideoProfileProxy createFakeVideoProfileProxy(
+ int videoFrameWidth,
+ int videoFrameHeight
+ ) {
+ return VideoProfileProxy.create(
DEFAULT_VIDEO_CODEC,
DEFAULT_VIDEO_MEDIA_TYPE,
DEFAULT_VIDEO_BITRATE,
@@ -167,7 +185,12 @@
DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
DEFAULT_VIDEO_HDR_FORMAT
);
- AudioProfileProxy audioProfile = AudioProfileProxy.create(
+ }
+
+ /** A utility method to create an AudioProfileProxy with some default values. */
+ @NonNull
+ public static AudioProfileProxy createFakeAudioProfileProxy() {
+ return AudioProfileProxy.create(
DEFAULT_AUDIO_CODEC,
DEFAULT_AUDIO_MEDIA_TYPE,
DEFAULT_AUDIO_BITRATE,
@@ -175,13 +198,6 @@
DEFAULT_AUDIO_CHANNELS,
DEFAULT_AUDIO_PROFILE
);
-
- return ImmutableEncoderProfilesProxy.create(
- DEFAULT_DURATION,
- DEFAULT_OUTPUT_FORMAT,
- Collections.singletonList(audioProfile),
- Collections.singletonList(videoProfile)
- );
}
// This class is not instantiable.
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
index dae1356..dd5ac4e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
@@ -18,6 +18,7 @@
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.camera.core.CameraSelector
import androidx.camera.testing.LabTestRule.LabTestFrontCamera
import androidx.camera.testing.LabTestRule.LabTestOnly
import androidx.camera.testing.LabTestRule.LabTestRearCamera
@@ -142,5 +143,23 @@
fun isInLabTest(): Boolean {
return Log.isLoggable("MH", Log.DEBUG)
}
+
+ /**
+ * Checks if it is CameraX lab environment where the enabled camera uses the specified
+ * [lensFacing] direction.
+ *
+ * For example, if [lensFacing] is [CameraSelector.LENS_FACING_BACK], this method will
+ * return true if the rear camera is enabled on a device in CameraX lab environment.
+ *
+ * @param lensFacing the required camera direction relative to the device screen.
+ * @return if enabled camera is in same direction as [lensFacing] in CameraX lab environment
+ */
+ @JvmStatic
+ fun isLensFacingEnabledInLabTest(@CameraSelector.LensFacing lensFacing: Int) =
+ when (lensFacing) {
+ CameraSelector.LENS_FACING_BACK -> Log.isLoggable("rearCameraE2E", Log.DEBUG)
+ CameraSelector.LENS_FACING_FRONT -> Log.isLoggable("frontCameraE2E", Log.DEBUG)
+ else -> false
+ }
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 29855d5..41abf57 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -16,6 +16,8 @@
package androidx.camera.testing.fakes;
+import static androidx.camera.core.DynamicRange.SDR;
+
import android.util.Range;
import android.util.Rational;
import android.util.Size;
@@ -26,6 +28,7 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.TorchState;
@@ -47,8 +50,10 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -64,6 +69,7 @@
new Range<>(30, 30),
new Range<>(60, 60))
);
+ private static final Set<DynamicRange> DEFAULT_DYNAMIC_RANGES = Collections.singleton(SDR);
private final String mCameraId;
private final int mSensorRotation;
@CameraSelector.LensFacing
@@ -73,6 +79,8 @@
private final Map<Integer, List<Size>> mSupportedResolutionMap = new HashMap<>();
private final Map<Integer, List<Size>> mSupportedHighResolutionMap = new HashMap<>();
private MutableLiveData<CameraState> mCameraStateLiveData;
+
+ private final Set<DynamicRange> mSupportedDynamicRanges = new HashSet<>(DEFAULT_DYNAMIC_RANGES);
private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
// Leave uninitialized to support camera-core:1.0.0 dependencies.
@@ -206,6 +214,12 @@
return resolutions != null ? resolutions : Collections.emptyList();
}
+ @NonNull
+ @Override
+ public Set<DynamicRange> getSupportedDynamicRanges() {
+ return mSupportedDynamicRanges;
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
@@ -294,6 +308,12 @@
mIntrinsicZoomRatio = zoomRatio;
}
+ /** Set the supported dynamic ranges for testing */
+ public void setSupportedDynamicRanges(@NonNull Set<DynamicRange> dynamicRanges) {
+ mSupportedDynamicRanges.clear();
+ mSupportedDynamicRanges.addAll(dynamicRanges);
+ }
+
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
static final class FakeExposureState implements ExposureState {
@Override
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
index b5d9233..7855897 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
@@ -79,4 +79,9 @@
assertThat(mFakeCameraInfo.getSupportedFpsRanges()).isNotEmpty();
}
+
+ @Test
+ public void canRetrieveSupportedDynamicRanges() {
+ assertThat(mFakeCameraInfo.getSupportedDynamicRanges()).isNotEmpty();
+ }
}
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -132,7 +132,9 @@
}
@RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public int getMirrorMode();
method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFramerate();
method public int getTargetRotation();
method public void setTargetRotation(int);
method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
@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!> 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);
}
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -132,7 +132,9 @@
}
@RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public int getMirrorMode();
method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFramerate();
method public int getTargetRotation();
method public void setTargetRotation(int);
method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
@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!> 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);
}
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -132,7 +132,9 @@
}
@RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+ method public int getMirrorMode();
method public T getOutput();
+ method public android.util.Range<java.lang.Integer!> getTargetFramerate();
method public int getTargetRotation();
method public void setTargetRotation(int);
method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
@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!> 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);
}
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 3eb0bf2..eb803dd 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
@@ -32,6 +32,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
import static androidx.camera.core.impl.utils.Threads.isMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
@@ -72,6 +73,7 @@
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Logger;
import androidx.camera.core.MirrorMode;
+import androidx.camera.core.Preview;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
@@ -253,6 +255,21 @@
}
/**
+ * Returns the target frame rate range for the associated VideoCapture use case.
+ *
+ * <p>The rotation can be set prior to constructing a VideoCapture using
+ * {@link VideoCapture.Builder#setTargetFramerate(Range)}
+ * If not set, the target frame rate defaults to the value of
+ * {@link StreamSpec#FRAME_RATE_RANGE_UNSPECIFIED}
+ *
+ * @return The rotation of the intended target.
+ */
+ @NonNull
+ public Range<Integer> getTargetFramerate() {
+ return getTargetFramerateInternal();
+ }
+
+ /**
* Sets the desired rotation of the output video.
*
* <p>Valid values include: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
@@ -366,17 +383,14 @@
setTargetRotation(orientationDegreesToSurfaceRotation(degrees));
}
- // TODO: to public API
/**
* Returns the mirror mode.
*
* <p>The mirror mode is set by {@link VideoCapture.Builder#setMirrorMode(int)}. If not set,
- * it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ * it defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
*
* @return The mirror mode of the intended target.
- *
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
@MirrorMode.Mirror
public int getMirrorMode() {
return getMirrorModeInternal();
@@ -384,6 +398,7 @@
/**
* {@inheritDoc}
+ *
*/
@SuppressWarnings("unchecked")
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -409,6 +424,7 @@
/**
* {@inheritDoc}
+ *
*/
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -419,6 +435,7 @@
/**
* {@inheritDoc}
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@@ -445,6 +462,7 @@
/**
* {@inheritDoc}
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@@ -465,6 +483,7 @@
/**
* {@inheritDoc}
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -479,6 +498,7 @@
/**
* {@inheritDoc}
+ *
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@@ -682,6 +702,7 @@
}
/**
+ *
*/
@Nullable
@RestrictTo(Scope.TESTS)
@@ -694,6 +715,7 @@
*
* <p>These values may be overridden by the implementation. They only provide a minimum set of
* defaults that are implementation independent.
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final class Defaults implements ConfigProvider<VideoCaptureConfig<?>> {
@@ -1309,6 +1331,7 @@
/**
* {@inheritDoc}
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@@ -1319,6 +1342,7 @@
/**
* {@inheritDoc}
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -1390,6 +1414,7 @@
* setTargetAspectRatio is not supported on VideoCapture
*
* <p>To set aspect ratio, see {@link Recorder.Builder#setAspectRatio(int)}.
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -1432,19 +1457,21 @@
return this;
}
- // TODO: to public API
/**
* Sets the mirror mode.
*
* <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
- * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
- * If not set, it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
+ * If not set, it defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ *
+ * <p>This API only changes the mirroring behavior on VideoCapture, but does not affect
+ * other UseCases. If the application wants to be consistent with the default
+ * {@link Preview} behavior where the rear camera is not mirrored but the front camera is
+ * mirrored, then {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY} is recommended.
*
* @param mirrorMode The mirror mode of the intended target.
* @return The current Builder.
- *
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
public Builder<T> setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
@@ -1456,6 +1483,7 @@
* setTargetResolution is not supported on VideoCapture
*
* <p>To set resolution, see {@link Recorder.Builder#setQualitySelector(QualitySelector)}.
+ *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -1606,5 +1634,22 @@
getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
return this;
}
+
+ /**
+ * Sets the target frame rate range for the associated VideoCapture use case.
+ *
+ * <p>This target will be used as a part of the heuristics for the algorithm that determines
+ * the final frame rate range and resolution of all concurrently bound use cases.
+ * <p>It is not guaranteed that this target frame rate will be the final range,
+ * as other use cases as well as frame rate restrictions of the device may affect the
+ * outcome of the algorithm that chooses the actual frame rate.
+ *
+ * @param targetFrameRate the target frame rate range.
+ */
+ @NonNull
+ public Builder<T> setTargetFramerate(@NonNull Range<Integer> targetFrameRate) {
+ getMutableConfig().insertOption(OPTION_TARGET_FRAME_RATE, targetFrameRate);
+ return this;
+ }
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java
new file mode 100644
index 0000000..b8c05c1
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal;
+
+import static androidx.camera.video.internal.utils.DynamicRangeUtil.DR_TO_VP_BIT_DEPTH_MAP;
+import static androidx.camera.video.internal.utils.DynamicRangeUtil.DR_TO_VP_FORMAT_MAP;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation that provides {@link EncoderProfilesProxy} containing video information
+ * matched with the target {@link DynamicRange}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class DynamicRangeMatchedEncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private final EncoderProfilesProvider mEncoderProfilesProvider;
+ private final DynamicRange mDynamicRange;
+ private final Map<Integer, EncoderProfilesProxy> mEncoderProfilesCache = new HashMap<>();
+
+ public DynamicRangeMatchedEncoderProfilesProvider(@NonNull EncoderProfilesProvider provider,
+ @NonNull DynamicRange dynamicRange) {
+ mEncoderProfilesProvider = provider;
+ mDynamicRange = dynamicRange;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ if (!mEncoderProfilesProvider.hasProfile(quality)) {
+ return false;
+ }
+
+ return getProfilesInternal(quality) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ return getProfilesInternal(quality);
+ }
+
+ @Nullable
+ private EncoderProfilesProxy getProfilesInternal(int quality) {
+ if (mEncoderProfilesCache.containsKey(quality)) {
+ return mEncoderProfilesCache.get(quality);
+ }
+
+ EncoderProfilesProxy profiles = null;
+ if (mEncoderProfilesProvider.hasProfile(quality)) {
+ EncoderProfilesProxy baseProfiles = mEncoderProfilesProvider.getAll(quality);
+ profiles = filterUnmatchedDynamicRange(baseProfiles, mDynamicRange);
+ mEncoderProfilesCache.put(quality, profiles);
+ }
+
+ return profiles;
+ }
+
+ @Nullable
+ private static EncoderProfilesProxy filterUnmatchedDynamicRange(
+ @Nullable EncoderProfilesProxy encoderProfiles, @NonNull DynamicRange dynamicRange) {
+ if (encoderProfiles == null) {
+ return null;
+ }
+
+ List<VideoProfileProxy> validVideoProfiles = new ArrayList<>();
+ for (VideoProfileProxy videoProfile : encoderProfiles.getVideoProfiles()) {
+ if (isBitDepthMatched(videoProfile, dynamicRange) && isHdrFormatMatched(videoProfile,
+ dynamicRange)) {
+ validVideoProfiles.add(videoProfile);
+ }
+ }
+
+ return validVideoProfiles.isEmpty() ? null : ImmutableEncoderProfilesProxy.create(
+ encoderProfiles.getDefaultDurationSeconds(),
+ encoderProfiles.getRecommendedFileFormat(),
+ encoderProfiles.getAudioProfiles(),
+ validVideoProfiles
+ );
+ }
+
+ private static boolean isBitDepthMatched(@NonNull VideoProfileProxy videoProfile,
+ @NonNull DynamicRange dynamicRange) {
+ Set<Integer> matchedBitDepths = DR_TO_VP_BIT_DEPTH_MAP.get(dynamicRange.getBitDepth());
+
+ return matchedBitDepths != null && matchedBitDepths.contains(videoProfile.getBitDepth());
+ }
+
+ private static boolean isHdrFormatMatched(@NonNull VideoProfileProxy videoProfile,
+ @NonNull DynamicRange dynamicRange) {
+ Set<Integer> matchedHdrFormats = DR_TO_VP_FORMAT_MAP.get(dynamicRange.getFormat());
+
+ return matchedHdrFormats != null && matchedHdrFormats.contains(videoProfile.getHdrFormat());
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
new file mode 100644
index 0000000..2b081c7
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.utils;
+
+import static android.media.EncoderProfiles.VideoProfile.HDR_DOLBY_VISION;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HDR10;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HDR10PLUS;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HLG;
+import static android.media.EncoderProfiles.VideoProfile.HDR_NONE;
+
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT;
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_8_BIT;
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR10;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.FORMAT_HLG;
+import static androidx.camera.core.DynamicRange.FORMAT_SDR;
+import static androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_10;
+import static androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility class for dynamic range related operations.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class DynamicRangeUtil {
+
+ public static final Map<Integer, Set<Integer>> DR_TO_VP_BIT_DEPTH_MAP = new HashMap<>();
+ public static final Map<Integer, Set<Integer>> DR_TO_VP_FORMAT_MAP = new HashMap<>();
+
+ private DynamicRangeUtil() {
+ }
+
+ static {
+ // DynamicRange bit depth to VideoProfile bit depth.
+ DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_8_BIT, new HashSet<>(singletonList(BIT_DEPTH_8)));
+ DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_10_BIT, new HashSet<>(singletonList(BIT_DEPTH_10)));
+ DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_UNSPECIFIED,
+ new HashSet<>(asList(BIT_DEPTH_8, BIT_DEPTH_10)));
+
+ // DynamicRange format to VideoProfile HDR format.
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_SDR, new HashSet<>(singletonList(HDR_NONE)));
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR_UNSPECIFIED,
+ new HashSet<>(asList(HDR_HLG, HDR_HDR10, HDR_HDR10PLUS, HDR_DOLBY_VISION)));
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_HLG, new HashSet<>(singletonList(HDR_HLG)));
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR10, new HashSet<>(singletonList(HDR_HDR10)));
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR10_PLUS, new HashSet<>(singletonList(HDR_HDR10PLUS)));
+ DR_TO_VP_FORMAT_MAP.put(FORMAT_DOLBY_VISION,
+ new HashSet<>(singletonList(HDR_DOLBY_VISION)));
+ }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index c03128a..d0e5fb9 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -43,7 +43,7 @@
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.CameraXConfig
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
import androidx.camera.core.SurfaceRequest
@@ -796,8 +796,8 @@
@Test
fun canGetSetMirrorMode() {
- val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_FRONT_ON)
- assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_FRONT_ON)
+ val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_ON_FRONT_ONLY)
+ assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
}
@Test
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..7c9a766
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal
+
+import android.media.CamcorderProfile.QUALITY_1080P
+import android.media.EncoderProfiles.VideoProfile.HDR_DOLBY_VISION
+import android.media.EncoderProfiles.VideoProfile.HDR_HDR10
+import android.media.EncoderProfiles.VideoProfile.HDR_HDR10PLUS
+import android.media.EncoderProfiles.VideoProfile.HDR_HLG
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.os.Build
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION
+import androidx.camera.core.DynamicRange.FORMAT_HDR10
+import androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS
+import androidx.camera.core.DynamicRange.FORMAT_HLG
+import androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT
+import androidx.camera.core.DynamicRange.SDR
+import androidx.camera.core.impl.EncoderProfilesProvider
+import androidx.camera.core.impl.EncoderProfilesProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_10
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.EncoderProfilesUtil
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_1080P
+import androidx.camera.testing.EncoderProfilesUtil.createFakeAudioProfileProxy
+import androidx.camera.testing.EncoderProfilesUtil.createFakeVideoProfileProxy
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeMatchedEncoderProfilesProviderTest {
+
+ private val defaultProvider = createFakeEncoderProfilesProvider(
+ arrayOf(Pair(QUALITY_1080P, PROFILES_1080P_FULL_DYNAMIC_RANGE))
+ )
+
+ @Test
+ fun hasNoProfile_canNotGetProfiles() {
+ val emptyProvider = createFakeEncoderProfilesProvider()
+ val sdrProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, SDR)
+ val hlgProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HLG)
+ val hdr10Provider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR10)
+ val hdr10PlusProvider =
+ DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR10_PLUS)
+ val dolbyProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, DOLBY_VISION)
+ val hdrUnspecifiedProvider =
+ DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR_UNSPECIFIED_10_BIT)
+
+ assertThat(sdrProvider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(hlgProvider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(hdr10Provider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(hdr10PlusProvider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(dolbyProvider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(hdrUnspecifiedProvider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(sdrProvider.getAll(QUALITY_1080P)).isNull()
+ assertThat(hlgProvider.getAll(QUALITY_1080P)).isNull()
+ assertThat(hdr10Provider.getAll(QUALITY_1080P)).isNull()
+ assertThat(hdr10PlusProvider.getAll(QUALITY_1080P)).isNull()
+ assertThat(dolbyProvider.getAll(QUALITY_1080P)).isNull()
+ assertThat(hdrUnspecifiedProvider.getAll(QUALITY_1080P)).isNull()
+ }
+
+ @Test
+ fun sdr_onlyContainsSdrProfile() {
+ val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, SDR)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 1).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_NONE).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_8).isTrue()
+ }
+
+ @Test
+ fun hlg_onlyContainsHlgProfile() {
+ val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HLG)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 1).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_HLG).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+ }
+
+ @Test
+ fun hdr10_onlyContainsHdr10Profile() {
+ val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR10)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 1).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_HDR10).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+ }
+
+ @Test
+ fun hdr10Plus_onlyContainsHdr10PlusProfile() {
+ val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR10_PLUS)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 1).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_HDR10PLUS).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+ }
+
+ @Test
+ fun dolbyVision_onlyContainsDolbyVisionProfile() {
+ val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, DOLBY_VISION)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 1).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_DOLBY_VISION).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+ }
+
+ @Test
+ fun hdrUnspecified_containsAllHdrProfiles() {
+ val provider =
+ DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR_UNSPECIFIED_10_BIT)
+
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+ assertThat(videoProfiles.size == 4).isTrue()
+ assertThat(videoProfiles[0].hdrFormat == HDR_HLG).isTrue()
+ assertThat(videoProfiles[1].hdrFormat == HDR_HDR10).isTrue()
+ assertThat(videoProfiles[2].hdrFormat == HDR_HDR10PLUS).isTrue()
+ assertThat(videoProfiles[3].hdrFormat == HDR_DOLBY_VISION).isTrue()
+ assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+ assertThat(videoProfiles[1].bitDepth == BIT_DEPTH_10).isTrue()
+ assertThat(videoProfiles[2].bitDepth == BIT_DEPTH_10).isTrue()
+ assertThat(videoProfiles[3].bitDepth == BIT_DEPTH_10).isTrue()
+ }
+
+ private fun createFakeEncoderProfilesProvider(
+ qualityToProfilesPairs: Array<Pair<Int, EncoderProfilesProxy>> = emptyArray()
+ ): EncoderProfilesProvider {
+ return FakeEncoderProfilesProvider.Builder().also { builder ->
+ for (pair in qualityToProfilesPairs) {
+ builder.add(pair.first, pair.second)
+ }
+ }.build()
+ }
+
+ companion object {
+ private val HLG = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+ private val HDR10 = DynamicRange(FORMAT_HDR10, BIT_DEPTH_10_BIT)
+ private val HDR10_PLUS = DynamicRange(FORMAT_HDR10_PLUS, BIT_DEPTH_10_BIT)
+ private val DOLBY_VISION = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_10_BIT)
+ private val VIDEO_PROFILES_1080P_SDR =
+ createFakeVideoProfileProxy(RESOLUTION_1080P.width, RESOLUTION_1080P.height)
+ private val VIDEO_PROFILES_1080P_HLG =
+ VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HLG, BIT_DEPTH_10)
+ private val VIDEO_PROFILES_1080P_HDR10 =
+ VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HDR10, BIT_DEPTH_10)
+ private val VIDEO_PROFILES_1080P_HDR10_PLUS =
+ VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HDR10PLUS, BIT_DEPTH_10)
+ private val VIDEO_PROFILES_1080P_DOLBY_VISION =
+ VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_DOLBY_VISION, BIT_DEPTH_10)
+ private val PROFILES_1080P_FULL_DYNAMIC_RANGE = ImmutableEncoderProfilesProxy.create(
+ EncoderProfilesUtil.DEFAULT_DURATION,
+ EncoderProfilesUtil.DEFAULT_OUTPUT_FORMAT,
+ listOf(createFakeAudioProfileProxy()),
+ listOf(
+ VIDEO_PROFILES_1080P_SDR,
+ VIDEO_PROFILES_1080P_HLG,
+ VIDEO_PROFILES_1080P_HDR10,
+ VIDEO_PROFILES_1080P_HDR10_PLUS,
+ VIDEO_PROFILES_1080P_DOLBY_VISION
+ )
+ )
+
+ private fun VideoProfileProxy.modifyDynamicRangeInfo(
+ hdrFormat: Int,
+ bitDepth: Int
+ ): VideoProfileProxy {
+ return VideoProfileProxy.create(
+ this.codec,
+ this.mediaType,
+ this.bitrate,
+ this.frameRate,
+ this.width,
+ this.height,
+ this.profile,
+ bitDepth,
+ this.chromaSubsampling,
+ hdrFormat
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
index 88c8b26..870c376 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
@@ -35,6 +35,7 @@
import android.content.Context
import android.os.Build
import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
import androidx.camera.testing.fakes.FakeLifecycleOwner
@@ -121,6 +122,8 @@
@Test
fun initialRecorder_canMakeRecorderReady(): Unit = runBlocking {
+ Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
viewModel.initialRecorder(context, lifecycleOwner)
assertThat(viewModel.isRecorderReady).isTrue()
@@ -167,6 +170,8 @@
@Test
fun startAndStopRecording_canWorkCorrectlyAfterRecorderReady(): Unit = runBlocking {
+ Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
// Arrange.
viewModel.initialRecorder(context, lifecycleOwner)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index f96b0b1..42ed11b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -39,6 +39,7 @@
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.LabTestRule.Companion.isLensFacingEnabledInLabTest
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -311,6 +312,85 @@
assertFutureCompletes(future)
}
+ /**
+ * The following tests check if a device can complete 3A convergence, by setting an auto
+ * cancellation with [FocusMeteringAction.Builder.setAutoCancelDuration] which ensures throwing
+ * an exception in case of a timeout.
+ *
+ * Since some devices may require a long time to complete convergence, we are setting a long
+ * [FocusMeteringAction.mAutoCancelDurationInMillis] in these tests.
+ */
+
+ @Test
+ fun futureCompletes_whenFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AF/AE/AWB region available on this device!",
+ hasMeteringRegion(cameraSelector)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(validMeteringPoint)
+ .setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
+ @Test
+ fun futureCompletes_whenOnlyAfFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AF region available on this device!",
+ hasMeteringRegion(cameraSelector, FLAG_AF)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(
+ validMeteringPoint,
+ FLAG_AF
+ ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
+ @Test
+ fun futureCompletes_whenAeAwbFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+ Assume.assumeTrue(
+ "Not CameraX lab environment," +
+ " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+ isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+ )
+
+ Assume.assumeTrue(
+ "No AE/AWB region available on this device!",
+ hasMeteringRegion(cameraSelector, FLAG_AE or FLAG_AWB)
+ )
+
+ val focusMeteringAction = FocusMeteringAction.Builder(
+ validMeteringPoint,
+ FLAG_AE or FLAG_AWB
+ ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+ .build()
+
+ val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+ assertFutureCompletes(resultFuture)
+ }
+
private fun hasMeteringRegion(
selector: CameraSelector,
@FocusMeteringAction.MeteringMode flags: Int = FLAG_AF or FLAG_AE or FLAG_AWB
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
index e8e96f9..96508df 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
@@ -140,6 +140,8 @@
@Test
fun exceedMaxImagesWithoutClosing_doNotCrash() = runBlocking {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
// Arrange.
val queueDepth = 3
val semaphore = Semaphore(0)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 129edde..c8c1d21 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -1182,6 +1182,8 @@
@Test
fun useCaseCanBeReusedInDifferentCamera() = runBlocking {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index b744eea..f7b75c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -108,6 +108,8 @@
@Before
@Throws(ExecutionException::class, InterruptedException::class)
fun setUp() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+
context = ApplicationProvider.getApplicationContext()
CameraXUtil.initialize(context!!, cameraConfig).get()
@@ -424,6 +426,8 @@
@Test
@Throws(InterruptedException::class)
fun useCaseCanBeReusedInDifferentCamera() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
val preview = defaultBuilder!!.build()
instrumentation.runOnMainSync { preview.setSurfaceProvider(getSurfaceProvider(null)) }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 04bdd7d..c745a9f 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -24,6 +24,7 @@
import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
@@ -1514,7 +1515,9 @@
if (mVideoQuality != QUALITY_AUTO) {
builder.setQualitySelector(QualitySelector.from(mVideoQuality));
}
- VideoCapture<Recorder> videoCapture = VideoCapture.withOutput(builder.build());
+ VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(builder.build())
+ .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
+ .build();
useCases.add(videoCapture);
}
return useCases;
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
index df83e57..fe3031d 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
@@ -47,8 +47,7 @@
<application
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false">
+ android:icon="@drawable/ic_launcher">
<meta-data
android:name="com.android.automotive"
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 0fb4867..091708d 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -56,8 +56,7 @@
<application
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false">
+ android:icon="@drawable/ic_launcher">
<meta-data
android:name="com.android.automotive"
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
index b9ba68b..5ea244a 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
@@ -30,8 +30,7 @@
<application
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false">
+ android:icon="@drawable/ic_launcher">
<activity
android:name="androidx.car.app.sample.navigation.common.app.MainActivity"
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 3fda109c..d88bd0d 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -56,8 +56,7 @@
<application
android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:extractNativeLibs="false">
+ android:label="@string/app_name">
<meta-data
android:name="com.android.automotive"
@@ -105,4 +104,4 @@
</activity>
</application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index 92aba90..5dac37e 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -60,7 +60,6 @@
<application
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false"
android:supportsRtl="true">
<meta-data
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index e1efe8c..bb049ca 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -68,8 +68,7 @@
<application
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false">
+ android:icon="@drawable/ic_launcher">
<meta-data
android:name="com.android.automotive"
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index d72c73a..59a17ab 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -41,7 +41,6 @@
<application
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false"
android:supportsRtl="true">
<meta-data
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 25976cb..ea3b0c54 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -48,8 +48,7 @@
<application
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"
- android:extractNativeLibs="false">
+ android:icon="@drawable/ic_launcher">
<meta-data
android:name="com.google.android.gms.car.application"
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 25638b0..5b45600 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -866,7 +866,7 @@
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
method public androidx.car.app.model.CarText getBody();
method public long getReceivedTimeEpochMillis();
- method public androidx.core.app.Person getSender();
+ method public androidx.core.app.Person? getSender();
method public boolean isRead();
}
@@ -876,7 +876,7 @@
method public androidx.car.app.messaging.model.CarMessage.Builder setBody(androidx.car.app.model.CarText);
method public androidx.car.app.messaging.model.CarMessage.Builder setRead(boolean);
method public androidx.car.app.messaging.model.CarMessage.Builder setReceivedTimeEpochMillis(long);
- method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person);
+ method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person?);
}
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi public interface ConversationCallback {
@@ -894,6 +894,7 @@
method public androidx.car.app.model.CarIcon? getIcon();
method public String getId();
method public java.util.List<androidx.car.app.messaging.model.CarMessage!> getMessages();
+ method public androidx.core.app.Person getSelf();
method public androidx.car.app.model.CarText getTitle();
method public boolean isGroupConversation();
}
@@ -906,6 +907,7 @@
method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
+ method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
index 26429f7..8614a79 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
@@ -37,7 +37,7 @@
@RequiresCarApi(6)
@KeepFields
public class CarMessage {
- @NonNull
+ @Nullable
private final Bundle mSender;
@NonNull
private final CarText mBody;
@@ -47,7 +47,7 @@
@Override
public int hashCode() {
return Objects.hash(
- getPersonHashCode(getSender()),
+ PersonsEqualityHelper.getPersonHashCode(getSender()),
mBody,
mReceivedTimeEpochMillis,
mIsRead
@@ -65,53 +65,14 @@
CarMessage otherCarMessage = (CarMessage) other;
return
- arePeopleEqual(getSender(), otherCarMessage.getSender())
+ PersonsEqualityHelper.arePersonsEqual(getSender(), otherCarMessage.getSender())
&& Objects.equals(mBody, otherCarMessage.mBody)
&& mReceivedTimeEpochMillis == otherCarMessage.mReceivedTimeEpochMillis
- && mIsRead == otherCarMessage.mIsRead
- ;
- }
-
- // TODO(b/266877597): Move to androidx.core.app.Person
- private static boolean arePeopleEqual(Person person1, Person person2) {
- // If a unique ID was provided, use it
- String key1 = person1.getKey();
- String key2 = person2.getKey();
- if (key1 != null || key2 != null) {
- return Objects.equals(key1, key2);
- }
-
- // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
- String name1 = Objects.toString(person1.getName());
- String name2 = Objects.toString(person2.getName());
-
- // Fallback: Compare field-by-field
- return
- Objects.equals(name1, name2)
- && Objects.equals(person1.getUri(), person2.getUri())
- && Objects.equals(person1.isBot(), person2.isBot())
- && Objects.equals(person1.isImportant(), person2.isImportant());
- }
-
- // TODO(b/266877597): Move to androidx.core.app.Person
- private static int getPersonHashCode(Person person) {
- // If a unique ID was provided, use it
- String key = person.getKey();
- if (key != null) {
- return key.hashCode();
- }
-
- // Fallback: Use hash code for individual fields
- return Objects.hash(
- person.getName(),
- person.getUri(),
- person.isBot(),
- person.isImportant()
- );
+ && mIsRead == otherCarMessage.mIsRead;
}
CarMessage(@NonNull Builder builder) {
- this.mSender = requireNonNull(builder.mSender).toBundle();
+ this.mSender = builder.mSender == null ? null : requireNonNull(builder.mSender).toBundle();
this.mBody = requireNonNull(builder.mBody);
this.mReceivedTimeEpochMillis = builder.mReceivedTimeEpochMillis;
this.mIsRead = builder.mIsRead;
@@ -119,17 +80,22 @@
/** Default constructor for serialization. */
private CarMessage() {
- this.mSender = new Person.Builder().setName("").build().toBundle();
+ this.mSender = null;
this.mBody = new CarText.Builder("").build();
this.mReceivedTimeEpochMillis = 0;
this.mIsRead = false;
}
- /** Returns a {@link Person} representing the message sender */
- @NonNull
+ /**
+ * Returns a {@link Person} representing the message sender.
+ *
+ * <p> For self-sent messages, this method will return {@code null} or
+ * {@link ConversationItem#getSelf()}.
+ */
+ @Nullable
public Person getSender() {
- return Person.fromBundle(mSender);
+ return mSender == null ? null : Person.fromBundle(mSender);
}
/** Returns a {@link CarText} representing the message body */
@@ -158,7 +124,7 @@
boolean mIsRead;
/** Sets a {@link Person} representing the message sender */
- public @NonNull Builder setSender(@NonNull Person sender) {
+ public @NonNull Builder setSender(@Nullable Person sender) {
mSender = sender;
return this;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 4c98df4..5956a59 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -21,6 +21,7 @@
import static java.util.Objects.requireNonNull;
import android.annotation.SuppressLint;
+import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@
import androidx.car.app.model.CarText;
import androidx.car.app.model.Item;
import androidx.car.app.utils.CollectionUtils;
+import androidx.core.app.Person;
import java.util.ArrayList;
import java.util.List;
@@ -47,6 +49,8 @@
private final String mId;
@NonNull
private final CarText mTitle;
+ @NonNull
+ private final Bundle mSelf;
@Nullable
private final CarIcon mIcon;
private final boolean mIsGroupConversation;
@@ -58,6 +62,7 @@
@Override
public int hashCode() {
return Objects.hash(
+ PersonsEqualityHelper.getPersonHashCode(getSelf()),
mId,
mTitle,
mIcon,
@@ -80,6 +85,8 @@
Objects.equals(mId, otherConversationItem.mId)
&& Objects.equals(mTitle, otherConversationItem.mTitle)
&& Objects.equals(mIcon, otherConversationItem.mIcon)
+ && PersonsEqualityHelper
+ .arePersonsEqual(getSelf(), otherConversationItem.getSelf())
&& mIsGroupConversation == otherConversationItem.mIsGroupConversation
&& Objects.equals(mMessages, otherConversationItem.mMessages)
;
@@ -88,6 +95,7 @@
ConversationItem(@NonNull Builder builder) {
this.mId = requireNonNull(builder.mId);
this.mTitle = requireNonNull(builder.mTitle);
+ this.mSelf = requireNonNull(builder.mSelf).toBundle();
this.mIcon = builder.mIcon;
this.mIsGroupConversation = builder.mIsGroupConversation;
this.mMessages = requireNonNull(CollectionUtils.unmodifiableCopy(builder.mMessages));
@@ -100,6 +108,7 @@
private ConversationItem() {
mId = "";
mTitle = new CarText.Builder("").build();
+ mSelf = new Person.Builder().setName("").build().toBundle();
mIcon = null;
mIsGroupConversation = false;
mMessages = new ArrayList<>();
@@ -133,6 +142,12 @@
return mTitle;
}
+ /** Returns a {@link Person} for the conversation */
+ @NonNull
+ public Person getSelf() {
+ return Person.fromBundle(mSelf);
+ }
+
/** Returns a {@link CarIcon} for the conversation, or {@code null} if not set */
@Nullable
public CarIcon getIcon() {
@@ -167,6 +182,8 @@
@Nullable
CarText mTitle;
@Nullable
+ Person mSelf;
+ @Nullable
CarIcon mIcon;
boolean mIsGroupConversation;
@Nullable
@@ -204,6 +221,13 @@
return this;
}
+ /** Sets a {@link Person} for the conversation */
+ @NonNull
+ public Builder setSelf(@NonNull Person self) {
+ mSelf = self;
+ return this;
+ }
+
/**
* Specifies whether this conversation involves 3+ participants (a "group" conversation)
*
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java
new file mode 100644
index 0000000..e5268d6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging.model;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.Person;
+
+import java.util.Objects;
+
+/** Helper functions to compare two {@link Person} object. */
+class PersonsEqualityHelper {
+
+ /** Calculate the hashcode for {@link Person} object. */
+ // TODO(b/266877597): Move to androidx.core.app.Person
+ public static int getPersonHashCode(@Nullable Person person) {
+ if (person == null) {
+ return 0;
+ }
+
+ // If a unique ID was provided, use it
+ String key = person.getKey();
+ if (key != null) {
+ return key.hashCode();
+ }
+
+ // Fallback: Use hash code for individual fields
+ return Objects.hash(person.getName(), person.getUri(), person.isBot(),
+ person.isImportant());
+ }
+
+ /** Compare two {@link Person} objects. */
+ // TODO(b/266877597): Move to androidx.core.app.Person
+ public static boolean arePersonsEqual(@Nullable Person person1, @Nullable Person person2) {
+ if (person1 == null && person2 == null) {
+ return true;
+ } else if (person1 == null || person2 == null) {
+ return false;
+ }
+
+ // If a unique ID was provided, use it
+ String key1 = person1.getKey();
+ String key2 = person2.getKey();
+ if (key1 != null || key2 != null) {
+ return Objects.equals(key1, key2);
+ }
+
+ // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
+ String name1 = Objects.toString(person1.getName());
+ String name2 = Objects.toString(person2.getName());
+
+ // Fallback: Compare field-by-field
+ return
+ Objects.equals(name1, name2)
+ && Objects.equals(person1.getUri(), person2.getUri())
+ && Objects.equals(person1.isBot(), person2.isBot())
+ && Objects.equals(person1.isImportant(), person2.isImportant());
+ }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
index f250727..a7a2fb2 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
@@ -51,18 +51,6 @@
// Ignore nullability, so we can null out a builder field
@SuppressWarnings("ConstantConditions")
@Test
- public void build_throwsException_ifSenderMissing() {
- assertThrows(
- NullPointerException.class,
- () -> TestConversationFactory.createMinimalMessageBuilder()
- .setSender(null)
- .build()
- );
- }
-
- // Ignore nullability, so we can null out a builder field
- @SuppressWarnings("ConstantConditions")
- @Test
public void build_throwsException_ifMessageBodyMissing() {
assertThrows(
NullPointerException.class,
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
index 9f61ade..2090047 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
@@ -110,6 +110,12 @@
.createFullyPopulatedConversationItemBuilder()
.setGroupConversation(!fullyPopulatedItem.isGroupConversation())
.build();
+ ConversationItem modifiedSelf =
+ TestConversationFactory
+ .createFullyPopulatedConversationItemBuilder()
+ .setSelf(
+ TestConversationFactory.createFullyPopulatedPersonBuilder().build())
+ .build();
List<CarMessage> modifiedMessages = new ArrayList<>(1);
modifiedMessages.add(
TestConversationFactory
@@ -125,6 +131,7 @@
ConversationItem modifiedConversationCallback =
TestConversationFactory
.createFullyPopulatedConversationItemBuilder()
+ .setSelf(TestConversationFactory.createMinimalPersonBuilder().build())
.setConversationCallback(new ConversationCallback() {
@Override
public void onMarkAsRead() {
@@ -144,6 +151,7 @@
assertNotEqual(fullyPopulatedItem, modifiedIcon);
assertNotEqual(fullyPopulatedItem, modifiedGroupStatus);
assertNotEqual(fullyPopulatedItem, modifiedMessageList);
+ assertNotEqual(fullyPopulatedItem, modifiedSelf);
// NOTE: Conversation Callback does not affect equality
assertEqual(fullyPopulatedItem, modifiedConversationCallback);
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java
new file mode 100644
index 0000000..13b4d4f
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import androidx.core.app.Person;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link PersonsEqualityHelper}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class PersonsEqualityHelperTest {
+
+ @Test
+ public void equalsAndHashCode_minimalPersons_areEqual() {
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isTrue();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+
+ @Test
+ public void equalsAndHashCode_nullPersons_areEqual() {
+ assertThat(PersonsEqualityHelper.getPersonHashCode(null)).isEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(null));
+ assertThat(PersonsEqualityHelper.arePersonsEqual(null, null)).isTrue();
+ }
+
+ @Test
+ public void equalsAndHashCode_differentName_areNotEqual() {
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().setName("Person1").build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().setName("Person2").build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+
+ @Test
+ public void equalsAndHashCode_differentKey_areNotEqual() {
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().setKey("Person1").build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().setKey("Person2").build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+
+ @Test
+ public void equalsAndHashCode_differentUri_areNotEqual() {
+ Uri uri1 =
+ Uri.parse("http://foo.com/test/sender/uri1");
+ Uri uri2 =
+ Uri.parse("http://foo.com/test/sender/uri2");
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().setUri(
+ uri1.toString()).build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().setName(
+ uri2.toString()).build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+
+ @Test
+ public void equalsAndHashCode_differentBot_areNotEqual() {
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().setBot(true).build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().setBot(false).build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+
+ @Test
+ public void equalsAndHashCode_differentImportant_areNotEqual() {
+ Person person1 =
+ TestConversationFactory.createMinimalPersonBuilder().setImportant(true).build();
+ Person person2 =
+ TestConversationFactory.createMinimalPersonBuilder().setImportant(false).build();
+
+ assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+ assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+ PersonsEqualityHelper.getPersonHashCode(person2));
+ }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
index 19359bb..dc3097b 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
@@ -98,7 +98,6 @@
*/
public static CarMessage.Builder createMinimalMessageBuilder() {
return new CarMessage.Builder()
- .setSender(createMinimalPerson())
.setBody(CarText.create("Message body"));
}
@@ -118,6 +117,7 @@
*/
public static CarMessage.Builder createFullyPopulatedMessageBuilder() {
return createMinimalMessageBuilder()
+ .setSender(createFullyPopulatedPerson())
.setRead(true)
.setReceivedTimeEpochMillis(12345);
}
@@ -146,6 +146,7 @@
return new ConversationItem.Builder()
.setId("conversation_id")
.setTitle(CarText.create("Conversation Title"))
+ .setSelf(createMinimalPerson())
.setMessages(messages)
.setConversationCallback(EMPTY_CONVERSATION_CALLBACK);
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
index 3bfd3f1..4aad0a8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
@@ -94,6 +94,264 @@
}
}
+ @Test
+ fun testExpectWithGetExpectedPropertyInDefaultValueExpression() {
+ runCompose(
+ testFunBody = """
+ ExpectComposable { value ->
+ results["defaultValue"] = value
+ }
+ ExpectComposable({ expectedProperty + expectedProperty.reversed() }) { value ->
+ results["anotherValue"] = value
+ }
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ expect val expectedProperty: String
+
+ @Composable
+ expect fun ExpectComposable(
+ value: () -> String = { expectedProperty },
+ content: @Composable (v: String) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ actual val expectedProperty = "actualExpectedProperty"
+
+ @Composable
+ actual fun ExpectComposable(
+ value: () -> String,
+ content: @Composable (v: String) -> Unit
+ ) {
+ content(value())
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("actualExpectedProperty", results["defaultValue"])
+ assertEquals(
+ "actualExpectedProperty" + "actualExpectedProperty".reversed(),
+ results["anotherValue"]
+ )
+ }
+ }
+
+ @Test
+ fun testExpectWithComposableExpressionInDefaultValue() {
+ runCompose(
+ testFunBody = """
+ ExpectComposable { value ->
+ results["defaultValue"] = value
+ }
+ ExpectComposable("anotherValue") { value ->
+ results["anotherValue"] = value
+ }
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun defaultValueComposable(): String {
+ return "defaultValueComposable"
+ }
+
+ @Composable
+ expect fun ExpectComposable(
+ value: String = defaultValueComposable(),
+ content: @Composable (v: String) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ actual fun ExpectComposable(
+ value: String,
+ content: @Composable (v: String) -> Unit
+ ) {
+ content(value)
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("defaultValueComposable", results["defaultValue"])
+ assertEquals("anotherValue", results["anotherValue"])
+ }
+ }
+
+ @Test
+ fun testExpectWithTypedParameter() {
+ runCompose(
+ testFunBody = """
+ ExpectComposable<String>("aeiouy") { value ->
+ results["defaultValue"] = value
+ }
+ ExpectComposable<String>("aeiouy", { "anotherValue" }) { value ->
+ results["anotherValue"] = value
+ }
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ expect fun <T> ExpectComposable(
+ value: T,
+ composeValue: @Composable () -> T = { value },
+ content: @Composable (T) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ actual fun <T> ExpectComposable(
+ value: T,
+ composeValue: @Composable () -> T,
+ content: @Composable (T) -> Unit
+ ) {
+ content(composeValue())
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("aeiouy", results["defaultValue"])
+ assertEquals("anotherValue", results["anotherValue"])
+ }
+ }
+
+ @Test
+ fun testExpectWithRememberInDefaultValueExpression() {
+ runCompose(
+ testFunBody = """
+ ExpectComposable { value ->
+ results["defaultValue"] = value
+ }
+ ExpectComposable(remember { "anotherRememberedValue" }) { value ->
+ results["anotherValue"] = value
+ }
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ expect fun ExpectComposable(
+ value: String = remember { "rememberedDefaultValue" },
+ content: @Composable (v: String) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ actual fun ExpectComposable(
+ value: String,
+ content: @Composable (v: String) -> Unit
+ ) {
+ content(value)
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("rememberedDefaultValue", results["defaultValue"])
+ assertEquals("anotherRememberedValue", results["anotherValue"])
+ }
+ }
+
+ @Test
+ fun testExpectWithDefaultValueUsingAnotherArgument() {
+ runCompose(
+ testFunBody = """
+ ExpectComposable("AbccbA") { value ->
+ results["defaultValue"] = value
+ }
+ ExpectComposable("123", { s -> s + s.reversed() }) { value ->
+ results["anotherValue"] = value
+ }
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ expect fun ExpectComposable(
+ value: String,
+ composeText: (String) -> String = { value },
+ content: @Composable (v: String) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ actual fun ExpectComposable(
+ value: String,
+ composeText: (String) -> String,
+ content: @Composable (v: String) -> Unit
+ ) {
+ content(composeText(value))
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("AbccbA", results["defaultValue"])
+ assertEquals("123321", results["anotherValue"])
+ }
+ }
+
+ @Test
+ fun testNonComposableFunWithComposableParam() {
+ runCompose(
+ testFunBody = """
+ savedContentLambda = null
+ ExpectFunWithComposableParam { value ->
+ results["defaultValue"] = value
+ }
+ savedContentLambda!!.invoke()
+
+ savedContentLambda = null
+ ExpectFunWithComposableParam("3.14") { value ->
+ results["anotherValue"] = value
+ }
+ savedContentLambda!!.invoke()
+ """.trimIndent(),
+ files = mapOf(
+ "Expect.kt" to """
+ import androidx.compose.runtime.*
+
+ var savedContentLambda: (@Composable () -> Unit)? = null
+
+ expect fun ExpectFunWithComposableParam(
+ value: String = "000",
+ content: @Composable (v: String) -> Unit
+ )
+ """.trimIndent(),
+ "Actual.kt" to """
+ import androidx.compose.runtime.*
+
+ @Composable
+ actual fun ExpectFunWithComposableParam(
+ value: String,
+ content: @Composable (v: String) -> Unit
+ ) {
+ savedContentLambda = {
+ content(value)
+ }
+ }
+ """.trimIndent()
+ )
+ ) { results ->
+ assertEquals("000", results["defaultValue"])
+ assertEquals("3.14", results["anotherValue"])
+ }
+ }
+
// This method was partially borrowed/copy-pasted from RobolectricComposeTester
// where some of the code was commented out. Those commented out parts are needed here.
private fun runCompose(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index bbb5435..4629463 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -108,7 +108,7 @@
metrics
).lower(moduleFragment)
- CopyDefaultValuesFromExpectLowering().lower(moduleFragment)
+ CopyDefaultValuesFromExpectLowering(pluginContext).lower(moduleFragment)
val mangler = when {
pluginContext.platform.isJs() -> JsManglerIr
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
index 439cfbe..1ce5f87 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
@@ -16,15 +16,42 @@
package androidx.compose.compiler.plugins.kotlin.lower
+import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
import androidx.compose.compiler.plugins.kotlin.hasComposableAnnotation
-import org.jetbrains.kotlin.descriptors.FunctionDescriptor
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.descriptors.MemberDescriptor
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrEnumEntry
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrProperty
+import org.jetbrains.kotlin.ir.declarations.IrTypeParameter
+import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
+import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
+import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
+import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
+import org.jetbrains.kotlin.ir.symbols.IrEnumEntrySymbol
+import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
+import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol
+import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
+import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
+import org.jetbrains.kotlin.ir.symbols.IrValueParameterSymbol
+import org.jetbrains.kotlin.ir.symbols.IrValueSymbol
+import org.jetbrains.kotlin.ir.util.DeepCopyIrTreeWithSymbols
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.DeepCopyTypeRemapper
+import org.jetbrains.kotlin.ir.util.hasAnnotation
+import org.jetbrains.kotlin.ir.util.patchDeclarationParents
+import org.jetbrains.kotlin.ir.util.referenceFunction
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
-import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleExpectsForActual
+import org.jetbrains.kotlin.resolve.descriptorUtil.module
+import org.jetbrains.kotlin.resolve.descriptorUtil.propertyIfAccessor
+import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleActualsForExpected
/**
* [ComposableFunctionBodyTransformer] relies on presence of default values in
@@ -37,55 +64,207 @@
* This lowering needs to run before [ComposableFunctionBodyTransformer] and
* before [ComposerParamTransformer].
*
- * Fixes https://github.com/JetBrains/compose-jb/issues/1407
+ * Fixes:
+ * https://github.com/JetBrains/compose-jb/issues/1407
+ * https://github.com/JetBrains/compose-multiplatform/issues/2816
+ * https://github.com/JetBrains/compose-multiplatform/issues/2806
+ *
+ * This implementation is borrowed from Kotlin's ExpectToActualDefaultValueCopier.
+ * Currently, it heavily relies on descriptors to find expect for actuals or vice versa:
+ * findCompatibleActualsForExpected.
+ * Unlike ExpectToActualDefaultValueCopier, this lowering performs its transformations
+ * only for functions marked with @Composable annotation or
+ * for functions with @Composable lambdas in parameters.
+ *
+ * TODO(karpovich): When adding support for FIR we'll need to use different API.
+ * Likely: fun FirBasedSymbol<*>.getSingleCompatibleExpectForActualOrNull(): FirBasedSymbol<*>?
*/
@OptIn(ObsoleteDescriptorBasedAPI::class)
-class CopyDefaultValuesFromExpectLowering : ModuleLoweringPass {
+class CopyDefaultValuesFromExpectLowering(
+ pluginContext: IrPluginContext
+) : ModuleLoweringPass, IrElementTransformerVoid() {
+
+ private val symbolTable = pluginContext.symbolTable
+
+ private fun isApplicable(declaration: IrFunction): Boolean {
+ return declaration.hasComposableAnnotation() ||
+ declaration.valueParameters.any {
+ it.type.hasAnnotation(ComposeFqNames.Composable)
+ }
+ }
+
+ override fun visitFunction(declaration: IrFunction): IrStatement {
+ val original = super.visitFunction(declaration) as? IrFunction ?: return declaration
+
+ if (!original.isExpect || !isApplicable(original)) {
+ return original
+ }
+
+ val actualForExpected = original.findActualForExpected()
+
+ original.valueParameters.forEachIndexed { index, expectValueParameter ->
+ val actualValueParameter = actualForExpected.valueParameters[index]
+ val expectDefaultValue = expectValueParameter.defaultValue
+ if (expectDefaultValue != null) {
+ actualValueParameter.defaultValue = expectDefaultValue
+ .remapExpectValueSymbols()
+ .patchDeclarationParents(actualForExpected)
+
+ // Remove a default value in the expect fun in order to prevent
+ // Kotlin expect/actual-related lowerings trying to copy the default values again
+ expectValueParameter.defaultValue = null
+ }
+ }
+ return original
+ }
override fun lower(module: IrModuleFragment) {
- // it uses FunctionDescriptor since current API (findCompatibleExpectedForActual)
- // can return only a descriptor
- val expectComposables = mutableMapOf<FunctionDescriptor, IrFunction>()
+ module.transformChildrenVoid(this)
+ }
- // first pass to find expect functions with default values
- module.transformChildrenVoid(object : IrElementTransformerVoid() {
- override fun visitFunction(declaration: IrFunction): IrStatement {
- if (declaration.isExpect && declaration.hasComposableAnnotation()) {
- val hasDefaultValues = declaration.valueParameters.any {
- it.defaultValue != null
+ private inline fun <reified T : IrFunction> T.findActualForExpected(): T =
+ symbolTable.referenceFunction(descriptor.findActualForExpect()).owner as T
+
+ private fun IrProperty.findActualForExpected(): IrProperty =
+ symbolTable.referenceProperty(descriptor.findActualForExpect()).owner
+
+ private fun IrClass.findActualForExpected(): IrClass =
+ symbolTable.referenceClass(descriptor.findActualForExpect()).owner
+
+ private fun IrEnumEntry.findActualForExpected(): IrEnumEntry =
+ symbolTable.referenceEnumEntry(descriptor.findActualForExpect()).owner
+
+ private inline fun <reified T : MemberDescriptor> T.findActualForExpect(): T {
+ if (!this.isExpect) error(this)
+ return (findCompatibleActualsForExpected(module).singleOrNull() ?: error(this)) as T
+ }
+
+ private fun IrExpressionBody.remapExpectValueSymbols(): IrExpressionBody {
+ class SymbolRemapper : DeepCopySymbolRemapper() {
+ override fun getReferencedClass(symbol: IrClassSymbol) =
+ if (symbol.descriptor.isExpect)
+ symbol.owner.findActualForExpected().symbol
+ else super.getReferencedClass(symbol)
+
+ override fun getReferencedClassOrNull(symbol: IrClassSymbol?) =
+ symbol?.let { getReferencedClass(it) }
+
+ override fun getReferencedClassifier(symbol: IrClassifierSymbol): IrClassifierSymbol =
+ when (symbol) {
+ is IrClassSymbol -> getReferencedClass(symbol)
+ is IrTypeParameterSymbol -> remapExpectTypeParameter(symbol).symbol
+ else -> error("Unexpected symbol $symbol ${symbol.descriptor}")
+ }
+
+ override fun getReferencedConstructor(symbol: IrConstructorSymbol) =
+ if (symbol.descriptor.isExpect)
+ symbol.owner.findActualForExpected().symbol
+ else super.getReferencedConstructor(symbol)
+
+ override fun getReferencedFunction(symbol: IrFunctionSymbol): IrFunctionSymbol =
+ when (symbol) {
+ is IrSimpleFunctionSymbol -> getReferencedSimpleFunction(symbol)
+ is IrConstructorSymbol -> getReferencedConstructor(symbol)
+ else -> error("Unexpected symbol $symbol ${symbol.descriptor}")
+ }
+
+ override fun getReferencedSimpleFunction(symbol: IrSimpleFunctionSymbol) = when {
+ symbol.descriptor.isExpect -> symbol.owner.findActualForExpected().symbol
+
+ symbol.descriptor.propertyIfAccessor.isExpect -> {
+ val property = symbol.owner.correspondingPropertySymbol!!.owner
+ val actualPropertyDescriptor = property.descriptor.findActualForExpect()
+ val accessorDescriptor = when (symbol.owner) {
+ property.getter -> actualPropertyDescriptor.getter!!
+ property.setter -> actualPropertyDescriptor.setter!!
+ else -> error("Unexpected accessor of $symbol ${symbol.descriptor}")
}
- if (hasDefaultValues) {
- expectComposables[declaration.descriptor] = declaration
+ symbolTable.referenceFunction(accessorDescriptor) as IrSimpleFunctionSymbol
+ }
+
+ else -> super.getReferencedSimpleFunction(symbol)
+ }
+
+ override fun getReferencedProperty(symbol: IrPropertySymbol) =
+ if (symbol.descriptor.isExpect)
+ symbol.owner.findActualForExpected().symbol
+ else
+ super.getReferencedProperty(symbol)
+
+ override fun getReferencedEnumEntry(symbol: IrEnumEntrySymbol): IrEnumEntrySymbol =
+ if (symbol.descriptor.isExpect)
+ symbol.owner.findActualForExpected().symbol
+ else
+ super.getReferencedEnumEntry(symbol)
+
+ override fun getReferencedValue(symbol: IrValueSymbol) =
+ remapExpectValue(symbol)?.symbol ?: super.getReferencedValue(symbol)
+ }
+
+ val symbolRemapper = SymbolRemapper()
+ acceptVoid(symbolRemapper)
+
+ return transform(
+ transformer = DeepCopyIrTreeWithSymbols(
+ symbolRemapper, DeepCopyTypeRemapper(symbolRemapper)
+ ),
+ data = null
+ )
+ }
+
+ private fun remapExpectTypeParameter(symbol: IrTypeParameterSymbol): IrTypeParameter {
+ val parameter = symbol.owner
+ val parent = parameter.parent
+
+ return when (parent) {
+ is IrClass ->
+ if (!parent.descriptor.isExpect)
+ parameter
+ else parent.findActualForExpected().typeParameters[parameter.index]
+
+ is IrFunction ->
+ if (!parent.descriptor.isExpect)
+ parameter
+ else parent.findActualForExpected().typeParameters[parameter.index]
+
+ else -> error(parent)
+ }
+ }
+
+ private fun remapExpectValue(symbol: IrValueSymbol): IrValueParameter? {
+ if (symbol !is IrValueParameterSymbol) {
+ return null
+ }
+
+ val parameter = symbol.owner
+ val parent = parameter.parent
+
+ return when (parent) {
+ is IrClass ->
+ if (!parent.descriptor.isExpect)
+ null
+ else {
+ assert(parameter == parent.thisReceiver)
+ parent.findActualForExpected().thisReceiver!!
+ }
+
+ is IrFunction ->
+ if (!parent.descriptor.isExpect)
+ null
+ else when (parameter) {
+ parent.dispatchReceiverParameter ->
+ parent.findActualForExpected().dispatchReceiverParameter!!
+
+ parent.extensionReceiverParameter ->
+ parent.findActualForExpected().extensionReceiverParameter!!
+
+ else -> {
+ assert(parent.valueParameters[parameter.index] == parameter)
+ parent.findActualForExpected().valueParameters[parameter.index]
}
}
- return super.visitFunction(declaration)
- }
- })
- // second pass to set corresponding default values
- module.transformChildrenVoid(object : IrElementTransformerVoid() {
- override fun visitFunction(declaration: IrFunction): IrStatement {
- if (declaration.descriptor.isActual && declaration.hasComposableAnnotation()) {
- val compatibleExpects = declaration.descriptor.findCompatibleExpectsForActual {
- module.descriptor == it
- }
- if (compatibleExpects.isNotEmpty()) {
- val expectFun = compatibleExpects.firstOrNull {
- it in expectComposables
- }?.let {
- expectComposables[it]
- }
-
- if (expectFun != null) {
- declaration.valueParameters.forEachIndexed { index, it ->
- it.defaultValue =
- it.defaultValue ?: expectFun.valueParameters[index].defaultValue
- }
- }
- }
- }
- return super.visitFunction(declaration)
- }
- })
+ else -> error(parent)
+ }
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
index e353496..3f36fd9 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
@@ -76,9 +76,9 @@
text = buildAnnotatedString {
append("This word is a link: ")
withAnnotation(UrlAnnotation("https://google.com")) {
- append("Google\n")
+ append("Google")
}
- append("This word is not a link: google.com")
+ append("\nThis word is not a link: google.com")
},
style = TextStyle(fontSize = fontSize8)
)
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
index b0f117c..0443df8 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
@@ -92,11 +92,11 @@
"\uD83C\uDF55",
fontSize = 32.sp,
// apply other transformations like rotation and zoom on the pizza slice emoji
- modifier = Modifier.graphicsLayer(
- scaleX = scale,
- scaleY = scale,
+ modifier = Modifier.graphicsLayer {
+ scaleX = scale
+ scaleY = scale
rotationZ = rotation
- )
+ }
)
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
new file mode 100644
index 0000000..f7b269f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.modifiers
+
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LayoutUtilsKtTest {
+
+ @Test
+ fun finalConstraints_returnsTightWidth() {
+ val subject = finalConstraints(
+ Constraints(500, 500, 0, 50),
+ true,
+ TextOverflow.Ellipsis,
+ 42f
+ )
+ assertThat(subject.maxWidth).isEqualTo(500)
+ }
+
+ @Test
+ fun finalConstraints_returnsMaxIntrinsicWhenUnbound() {
+ val subject = finalConstraints(
+ Constraints(500, 500, 0, 50),
+ false,
+ TextOverflow.Clip,
+ 1234.1f
+ )
+ assertThat(subject.maxWidth).isEqualTo(1235)
+ }
+
+ @Test
+ fun finalMaxWidth_returnsTightWidth() {
+ val subject = finalMaxWidth(
+ Constraints(500, 500, 0, 50),
+ true,
+ TextOverflow.Ellipsis,
+ 42f
+ )
+ assertThat(subject).isEqualTo(500)
+ }
+
+ @Test
+ fun finalMaxWidth_returnsMaxIntrinsicWhenUnbound() {
+ val subject = finalMaxWidth(
+ Constraints(500, 500, 0, 50),
+ false,
+ TextOverflow.Clip,
+ 1234.1f
+ )
+ assertThat(subject).isEqualTo(1235)
+ }
+
+ @Test
+ fun finalMaxLines_negative() {
+ val subject = finalMaxLines(true, TextOverflow.Clip, -1)
+ assertThat(subject).isEqualTo(1)
+ }
+
+ @Test
+ fun finalMaxLines_positive_noOverride() {
+ val subject = finalMaxLines(true, TextOverflow.Clip, 4)
+ assertThat(subject).isEqualTo(4)
+ }
+
+ @Test
+ fun finalMaxLines_overrideOn_TextOverflowEllipsis_andSoftwrapFalse() {
+ val subject = finalMaxLines(false, TextOverflow.Ellipsis, 4)
+ assertThat(subject).isEqualTo(1)
+ }
+
+ @Test
+ fun canChangeBreak_canWrap_false() {
+ val subject = canChangeBreaks(
+ canWrap = false,
+ newConstraints = Constraints(0),
+ oldConstraints = Constraints(0),
+ maxIntrinsicWidth = 42f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isFalse()
+ }
+
+ @Test
+ fun canChangeBreak_sameWidth() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(50),
+ oldConstraints = Constraints.fixedWidth(50),
+ maxIntrinsicWidth = 1234f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isFalse()
+ }
+
+ @Test
+ fun canChangeBreak_textSmallerThanConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(50),
+ oldConstraints = Constraints.fixedWidth(40),
+ maxIntrinsicWidth = 12f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isFalse()
+ }
+
+ @Test
+ fun canChangeBreak_textBiggerThanConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(100),
+ oldConstraints = Constraints.fixedWidth(200),
+ maxIntrinsicWidth = 300f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isTrue()
+ }
+
+ @Test
+ fun canChangeBreak_shrinking_textSmallerThanNewConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(50),
+ oldConstraints = Constraints.fixedWidth(60),
+ maxIntrinsicWidth = 45f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isFalse()
+ }
+
+ @Test
+ fun canChangeBreak_shrinking_textBiggerThanNewConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(50),
+ oldConstraints = Constraints.fixedWidth(60),
+ maxIntrinsicWidth = 59f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isTrue()
+ }
+
+ @Test
+ fun canChangeBreak_growing_textSmallerThanNewConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(60),
+ oldConstraints = Constraints.fixedWidth(50),
+ maxIntrinsicWidth = 45f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isFalse()
+ }
+
+ @Test
+ fun canChangeBreak_growing_textBiggerThanNewConstraints() {
+ val subject = canChangeBreaks(
+ canWrap = true,
+ newConstraints = Constraints.fixedWidth(60),
+ oldConstraints = Constraints.fixedWidth(50),
+ maxIntrinsicWidth = 59f,
+ softWrap = true,
+ overflow = TextOverflow.Ellipsis
+ )
+ assertThat(subject).isTrue()
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
index 19a823d..4da7839 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
@@ -17,32 +17,21 @@
package androidx.compose.foundation.relocation
import android.graphics.Rect as AndroidRect
-import android.view.View
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalView
-@Composable
-internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
- val view = LocalView.current
- return remember(view) { AndroidBringIntoViewParent(view) }
-}
-
-/**
- * A [BringIntoViewParent] that delegates to the [View] hosting the composition.
- */
-private class AndroidBringIntoViewParent(private val view: View) : BringIntoViewParent {
- override suspend fun bringChildIntoView(
- childCoordinates: LayoutCoordinates,
- boundsProvider: () -> Rect?
- ) {
+internal actual fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+ BringIntoViewParent =
+ BringIntoViewParent { childCoordinates, boundsProvider ->
+ val view = currentValueOf(LocalView)
val childOffset = childCoordinates.positionInRoot()
- val rootRect = boundsProvider()?.translate(childOffset) ?: return
- view.requestRectangleOnScreen(rootRect.toRect(), false)
+ val rootRect = boundsProvider()?.translate(childOffset)
+ if (rootRect != null) {
+ view.requestRectangleOnScreen(rootRect.toRect(), false)
+ }
}
-}
private fun Rect.toRect() = AndroidRect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
index 7e5402e..4c532f3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -15,28 +15,25 @@
*/
package androidx.compose.foundation.relocation
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnPlacedModifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ModifierLocalNode
import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
/**
* The Key for the ModifierLocal that can be used to access the [BringIntoViewParent].
*/
-@OptIn(ExperimentalFoundationApi::class)
internal val ModifierLocalBringIntoViewParent = modifierLocalOf<BringIntoViewParent?> { null }
/**
* Platform-specific "root" of the [BringIntoViewParent] chain to call into when there are no
- * [ModifierLocalBringIntoViewParent]s above a [BringIntoViewChildModifier]. The value returned by
- * this function should be passed to the [BringIntoViewChildModifier] constructor.
+ * [ModifierLocalBringIntoViewParent]s above a [BringIntoViewChildNode].
*/
-@Composable
-internal expect fun rememberDefaultBringIntoViewParent(): BringIntoViewParent
+internal expect fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+ BringIntoViewParent
/**
* A node that can respond to [bringChildIntoView] requests from its children by scrolling its
@@ -67,16 +64,15 @@
* [BringIntoViewParent]: either one read from the [ModifierLocalBringIntoViewParent], or if no
* modifier local is specified then the [defaultParent].
*
- * @param defaultParent The [BringIntoViewParent] to use if there is no
+ * @property defaultParent The [BringIntoViewParent] to use if there is no
* [ModifierLocalBringIntoViewParent] available to read. This parent should always be obtained by
- * calling [rememberDefaultBringIntoViewParent] to support platform-specific integration.
+ * calling [defaultBringIntoViewParent] to support platform-specific integration.
*/
-internal abstract class BringIntoViewChildModifier(
- private val defaultParent: BringIntoViewParent
-) : ModifierLocalConsumer,
- OnPlacedModifier {
+internal abstract class BringIntoViewChildNode : Modifier.Node(),
+ ModifierLocalNode, LayoutAwareModifierNode, CompositionLocalConsumerModifierNode {
+ private val defaultParent = defaultBringIntoViewParent()
- private var localParent: BringIntoViewParent? = null
+ private val localParent: BringIntoViewParent? get() = ModifierLocalBringIntoViewParent.current
/** The [LayoutCoordinates] of this modifier, if attached. */
protected var layoutCoordinates: LayoutCoordinates? = null
@@ -86,12 +82,6 @@
protected val parent: BringIntoViewParent
get() = localParent ?: defaultParent
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- with(scope) {
- localParent = ModifierLocalBringIntoViewParent.current
- }
- }
-
override fun onPlaced(coordinates: LayoutCoordinates) {
layoutCoordinates = coordinates
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index 0636f8c..290860c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -17,14 +17,12 @@
package androidx.compose.foundation.relocation
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.toSize
/**
@@ -97,29 +95,15 @@
* [bringIntoView][BringIntoViewRequester.bringIntoView] requests to parents
* of the current composable.
*/
+@Suppress("ModifierInspectorInfo")
@ExperimentalFoundationApi
fun Modifier.bringIntoViewRequester(
bringIntoViewRequester: BringIntoViewRequester
-): Modifier = composed(debugInspectorInfo {
- name = "bringIntoViewRequester"
- properties["bringIntoViewRequester"] = bringIntoViewRequester
-}) {
- val defaultResponder = rememberDefaultBringIntoViewParent()
- val modifier = remember(defaultResponder) {
- BringIntoViewRequesterModifier(defaultResponder)
- }
- if (bringIntoViewRequester is BringIntoViewRequesterImpl) {
- DisposableEffect(bringIntoViewRequester) {
- bringIntoViewRequester.modifiers += modifier
- onDispose { bringIntoViewRequester.modifiers -= modifier }
- }
- }
- return@composed modifier
-}
+): Modifier = this.then(BringIntoViewRequesterElement(bringIntoViewRequester))
@ExperimentalFoundationApi
private class BringIntoViewRequesterImpl : BringIntoViewRequester {
- val modifiers = mutableVectorOf<BringIntoViewRequesterModifier>()
+ val modifiers = mutableVectorOf<BringIntoViewRequesterNode>()
override suspend fun bringIntoView(rect: Rect?) {
modifiers.forEach {
@@ -128,15 +112,64 @@
}
}
+@ExperimentalFoundationApi
+private class BringIntoViewRequesterElement(
+ private val requester: BringIntoViewRequester
+) : ModifierNodeElement<BringIntoViewRequesterNode>() {
+ override fun create(): BringIntoViewRequesterNode {
+ return BringIntoViewRequesterNode(requester)
+ }
+
+ override fun update(node: BringIntoViewRequesterNode): BringIntoViewRequesterNode =
+ node.also {
+ it.updateRequester(requester)
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "bringIntoViewRequester"
+ properties["bringIntoViewRequester"] = requester
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return (this === other) ||
+ (other is BringIntoViewRequesterElement) && (requester == other.requester)
+ }
+
+ override fun hashCode(): Int {
+ return requester.hashCode()
+ }
+}
+
/**
* A modifier that holds state and modifier implementations for [bringIntoViewRequester]. It has
- * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier], and uses that parent
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildNode], and uses that parent
* to respond to requests to [bringIntoView].
*/
@ExperimentalFoundationApi
-private class BringIntoViewRequesterModifier(
- defaultParent: BringIntoViewParent
-) : BringIntoViewChildModifier(defaultParent) {
+internal class BringIntoViewRequesterNode(
+ private var requester: BringIntoViewRequester
+) : BringIntoViewChildNode() {
+ init {
+ updateRequester(requester)
+ }
+
+ fun updateRequester(requester: BringIntoViewRequester) {
+ disposeRequester()
+ if (requester is BringIntoViewRequesterImpl) {
+ requester.modifiers += this
+ }
+ this.requester = requester
+ }
+
+ private fun disposeRequester() {
+ if (requester is BringIntoViewRequesterImpl) {
+ (requester as BringIntoViewRequesterImpl).modifiers -= this
+ }
+ }
+
+ override fun onDetach() {
+ disposeRequester()
+ }
/**
* Requests that [rect] (if non-null) or the entire bounds of this modifier's node (if [rect]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
index 3d95cca..9e31be2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
@@ -17,14 +17,12 @@
package androidx.compose.foundation.relocation
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.modifier.modifierLocalMapOf
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -96,40 +94,48 @@
*
* @see BringIntoViewRequester
*/
+@Suppress("ModifierInspectorInfo")
@ExperimentalFoundationApi
fun Modifier.bringIntoViewResponder(
responder: BringIntoViewResponder
-): Modifier = composed(debugInspectorInfo {
- name = "bringIntoViewResponder"
- properties["responder"] = responder
-}) {
- val defaultParent = rememberDefaultBringIntoViewParent()
- val modifier = remember(defaultParent) {
- BringIntoViewResponderModifier(defaultParent)
+): Modifier = this.then(BringIntoViewResponderElement(responder))
+
+@ExperimentalFoundationApi
+private class BringIntoViewResponderElement(
+ private val responder: BringIntoViewResponder
+) : ModifierNodeElement<BringIntoViewResponderNode>() {
+ override fun create(): BringIntoViewResponderNode = BringIntoViewResponderNode(responder)
+
+ override fun update(node: BringIntoViewResponderNode) = node.also {
+ it.responder = responder
}
- modifier.responder = responder
- return@composed modifier
+ override fun equals(other: Any?): Boolean {
+ return (this === other) ||
+ (other is BringIntoViewResponderElement) && (responder == other.responder)
+ }
+
+ override fun hashCode(): Int {
+ return responder.hashCode()
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "bringIntoViewResponder"
+ properties["responder"] = responder
+ }
}
/**
* A modifier that holds state and modifier implementations for [bringIntoViewResponder]. It has
- * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier] and additionally
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildNode] and additionally
* provides itself as the [BringIntoViewParent] for subsequent modifiers. This class is responsible
* for recursively propagating requests up the responder chain.
*/
@OptIn(ExperimentalFoundationApi::class)
-private class BringIntoViewResponderModifier(
- defaultParent: BringIntoViewParent
-) : BringIntoViewChildModifier(defaultParent),
- ModifierLocalProvider<BringIntoViewParent?>,
- BringIntoViewParent {
+private class BringIntoViewResponderNode(
+ var responder: BringIntoViewResponder
+) : BringIntoViewChildNode(), BringIntoViewParent {
- lateinit var responder: BringIntoViewResponder
-
- override val key: ProvidableModifierLocal<BringIntoViewParent?>
- get() = ModifierLocalBringIntoViewParent
- override val value: BringIntoViewParent
- get() = this
+ override val providedValues = modifierLocalMapOf(ModifierLocalBringIntoViewParent to this)
/**
* Responds to a child's request by first converting [boundsProvider] into this node's [LayoutCoordinates]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
new file mode 100644
index 0000000..bb111a3
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.modifiers
+
+import androidx.compose.foundation.text.ceilToIntPx
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Find the constraints to pass to Paragraph based on all the parameters.
+ */
+internal fun finalConstraints(
+ constraints: Constraints,
+ softWrap: Boolean,
+ overflow: TextOverflow,
+ maxIntrinsicWidth: Float
+): Constraints = Constraints(
+ maxWidth = finalMaxWidth(constraints, softWrap, overflow, maxIntrinsicWidth),
+ maxHeight = constraints.maxHeight
+ )
+
+/**
+ * Find the final max width a Paragraph would use based on all parameters.
+ */
+internal fun finalMaxWidth(
+ constraints: Constraints,
+ softWrap: Boolean,
+ overflow: TextOverflow,
+ maxIntrinsicWidth: Float
+): Int {
+ val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+ val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+ constraints.maxWidth
+ } else {
+ Constraints.Infinity
+ }
+
+ // if minWidth == maxWidth the width is fixed.
+ // therefore we can pass that value to our paragraph and use it
+ // if minWidth != maxWidth there is a range
+ // then we should check if the max intrinsic width is in this range to decide the
+ // width to be passed to Paragraph
+ // if max intrinsic width is between minWidth and maxWidth
+ // we can use it to layout
+ // else if max intrinsic width is greater than maxWidth, we can only use maxWidth
+ // else if max intrinsic width is less than minWidth, we should use minWidth
+ return if (constraints.minWidth == maxWidth) {
+ maxWidth
+ } else {
+ maxIntrinsicWidth.ceilToIntPx().coerceIn(constraints.minWidth, maxWidth)
+ }
+}
+
+/**
+ * Find the maxLines to pass to text layout based on all parameters
+ */
+internal fun finalMaxLines(softWrap: Boolean, overflow: TextOverflow, maxLinesIn: Int): Int {
+ // This is a fallback behavior because native text layout doesn't support multiple
+ // ellipsis in one text layout.
+ // When softWrap is turned off and overflow is ellipsis, it's expected that each line
+ // that exceeds maxWidth will be ellipsized.
+ // For example,
+ // input text:
+ // "AAAA\nAAAA"
+ // maxWidth:
+ // 3 * fontSize that only allow 3 characters to be displayed each line.
+ // expected output:
+ // AA…
+ // AA…
+ // Here we assume there won't be any '\n' character when softWrap is false. And make
+ // maxLines 1 to implement the similar behavior.
+ val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+ return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
+}
+
+/**
+ * Assuming we're laying out the same text in two different constraints, see if breaks could change
+ *
+ * If text or other text-layout attributes change, this method will not return accurate results.
+ */
+internal fun canChangeBreaks(
+ canWrap: Boolean,
+ newConstraints: Constraints,
+ oldConstraints: Constraints,
+ maxIntrinsicWidth: Float,
+ softWrap: Boolean,
+ overflow: TextOverflow,
+): Boolean {
+ // no breaks
+ if (!canWrap) return false
+ // we can assume maxIntrinsicWidth is the same, or other invalidate would have happened
+ // earlier (resetting para, etc)
+ val prevMaxWidth = finalMaxWidth(oldConstraints, softWrap, overflow, maxIntrinsicWidth)
+ val newMaxWidth = finalMaxWidth(newConstraints, softWrap, overflow, maxIntrinsicWidth)
+
+ if (prevMaxWidth != newMaxWidth) {
+ if (prevMaxWidth >= maxIntrinsicWidth && newMaxWidth >= maxIntrinsicWidth) {
+ // nothing can change, layout >= text width.
+ return false
+ }
+
+ return if (prevMaxWidth < newMaxWidth) {
+ // we're growing, so return if we could have broken last time
+ prevMaxWidth < maxIntrinsicWidth
+ } else {
+ // we're shrinking, so return if we could have broken this time
+ newMaxWidth < maxIntrinsicWidth
+ }
+ }
+
+ // widths haven't changed, layouts will be same
+ return false
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
index 53b531f..fc4e604 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
@@ -27,7 +27,6 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.resolveDefaults
-import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -64,11 +63,17 @@
internal var density: Density? = null
set(value) {
val localField = field
- if (value == null || localField == null) {
+ if (localField == null) {
field = value
return
}
+ if (value == null) {
+ field = value
+ markDirty()
+ return
+ }
+
if (localField.density != value.density || localField.fontScale != value.fontScale) {
field = value
// none of our results are correct if density changed
@@ -92,12 +97,14 @@
private var layoutCache: TextLayoutResult? = null
/**
- * Last intrinsic height computation
- *
- * - first = input width
- * - second = output height
+ * Input width for the last call to [intrinsicHeight]
*/
- private var cachedIntrinsicHeight: Pair<Int, Int>? = null
+ private var cachedIntrinsicHeightInputWidth: Int = -1
+
+ /**
+ * Output height for last call to [intrinsicHeight] at [cachedIntrinsicHeightInputWidth]
+ */
+ private var cachedIntrinsicHeight: Int = -1
/**
* The last computed TextLayoutResult, or throws if not initialized.
@@ -121,9 +128,6 @@
constraints: Constraints,
layoutDirection: LayoutDirection
): Boolean {
- if (!layoutCache.newLayoutWillBeDifferent(constraints, layoutDirection)) {
- return false
- }
val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
val localMinMax = MinMaxLinesCoercer.from(
minMaxLinesCoercer,
@@ -142,16 +146,28 @@
} else {
constraints
}
+ if (!layoutCache.newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+ if (finalConstraints == layoutCache!!.layoutInput.constraints) return false
+ // we need to regen the input, constraints aren't the same
+ layoutCache = textLayoutResult(
+ layoutDirection = layoutDirection,
+ finalConstraints = finalConstraints,
+ multiParagraph = layoutCache!!.multiParagraph
+ )
+ return true
+ }
val multiParagraph = layoutText(finalConstraints, layoutDirection)
- val size = finalConstraints.constrain(
- IntSize(
- multiParagraph.width.ceilToIntPx(),
- multiParagraph.height.ceilToIntPx()
- )
- )
+ layoutCache = textLayoutResult(layoutDirection, finalConstraints, multiParagraph)
+ return true
+ }
- layoutCache = TextLayoutResult(
+ private fun textLayoutResult(
+ layoutDirection: LayoutDirection,
+ finalConstraints: Constraints,
+ multiParagraph: MultiParagraph
+ ): TextLayoutResult {
+ return TextLayoutResult(
TextLayoutInput(
text,
style,
@@ -165,24 +181,29 @@
finalConstraints
),
multiParagraph,
- size
+ finalConstraints.constrain(
+ IntSize(
+ multiParagraph.width.ceilToIntPx(),
+ multiParagraph.height.ceilToIntPx()
+ )
+ )
)
- return true
}
/**
* The natural height of text at [width] in [layoutDirection]
*/
fun intrinsicHeight(width: Int, layoutDirection: LayoutDirection): Int {
- cachedIntrinsicHeight?.let { (prevWidth, prevHeight) ->
- if (width == prevWidth) return prevHeight
- }
+ val localWidth = cachedIntrinsicHeightInputWidth
+ val localHeght = cachedIntrinsicHeight
+ if (width == localWidth && localWidth != -1) return localHeght
val result = layoutText(
Constraints(0, width, 0, Constraints.Infinity),
layoutDirection
).height.ceilToIntPx()
- cachedIntrinsicHeight = width to result
+ cachedIntrinsicHeightInputWidth = width
+ cachedIntrinsicHeight = result
return result
}
@@ -250,51 +271,16 @@
): MultiParagraph {
val localParagraphIntrinsics = setLayoutDirection(layoutDirection)
- val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
- val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
- constraints.maxWidth
- } else {
- Constraints.Infinity
- }
-
- // This is a fallback behavior because native text layout doesn't support multiple
- // ellipsis in one text layout.
- // When softWrap is turned off and overflow is ellipsis, it's expected that each line
- // that exceeds maxWidth will be ellipsized.
- // For example,
- // input text:
- // "AAAA\nAAAA"
- // maxWidth:
- // 3 * fontSize that only allow 3 characters to be displayed each line.
- // expected output:
- // AA…
- // AA…
- // Here we assume there won't be any '\n' character when softWrap is false. And make
- // maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
- val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
-
- // if minWidth == maxWidth the width is fixed.
- // therefore we can pass that value to our paragraph and use it
- // if minWidth != maxWidth there is a range
- // then we should check if the max intrinsic width is in this range to decide the
- // width to be passed to Paragraph
- // if max intrinsic width is between minWidth and maxWidth
- // we can use it to layout
- // else if max intrinsic width is greater than maxWidth, we can only use maxWidth
- // else if max intrinsic width is less than minWidth, we should use minWidth
- val width = if (minWidth == maxWidth) {
- maxWidth
- } else {
- localParagraphIntrinsics.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
- }
-
return MultiParagraph(
intrinsics = localParagraphIntrinsics,
- constraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
+ constraints = finalConstraints(
+ constraints,
+ softWrap,
+ overflow,
+ localParagraphIntrinsics.maxIntrinsicWidth
+ ),
// This is a fallback behavior for ellipsis. Native
- maxLines = finalMaxLines,
+ maxLines = finalMaxLines(softWrap, overflow, maxLines),
ellipsis = overflow == TextOverflow.Ellipsis
)
}
@@ -319,36 +305,15 @@
// if we were passed identical constraints just skip more work
if (constraints == layoutInput.constraints) return false
- // only be clever if we can predict line break behavior exactly, which is only possible with
- // simple geometry math for the greedy layout case
- if (style.lineBreak != LineBreak.Simple) {
- return true
- }
-
- // see if width would produce the same wraps (greedy wraps only)
- val canWrap = softWrap && maxLines > 1
- if (canWrap && size.width != multiParagraph.maxIntrinsicWidth.ceilToIntPx()) {
- // some soft wrapping happened, check to see if we're between the previous measure and
- // the next wrap
- val prevActualMaxWidth = maxWidth(layoutInput.constraints)
- val newMaxWidth = maxWidth(constraints)
- if (newMaxWidth > prevActualMaxWidth) {
- // we've grown the potential layout area, and may break longer lines
- return true
- }
- if (newMaxWidth <= size.width) {
- // it's possible to shrink this text (possible opt: check minIntrinsicWidth
- return true
- }
- }
-
- // check any constraint width changes for single line text
- if (!canWrap &&
- (constraints.maxWidth != layoutInput.constraints.maxWidth ||
- (constraints.minWidth != layoutInput.constraints.minWidth))) {
- // no soft wrap and width is different, always invalidate
- return true
- }
+ // see if width would produce the same wraps
+ if (canChangeBreaks(
+ canWrap = softWrap && maxLines > 1,
+ newConstraints = constraints,
+ oldConstraints = layoutInput.constraints,
+ maxIntrinsicWidth = this.multiParagraph.intrinsics.maxIntrinsicWidth,
+ softWrap = softWrap,
+ overflow = overflow
+ )) return true
// if we get here width won't change, height may be clipped
if (constraints.maxHeight < multiParagraph.height) {
@@ -365,20 +330,12 @@
*
* Falls back to [paragraphIntrinsics.maxIntrinsicWidth] when not exact constraints.
*/
- private fun maxWidth(constraints: Constraints): Int {
- val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
- val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
- constraints.maxWidth
- } else {
- Constraints.Infinity
- }
- return if (minWidth == maxWidth) {
- maxWidth
- } else {
- paragraphIntrinsics!!.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
- }
- }
+ private fun maxWidth(constraints: Constraints): Int = finalMaxWidth(
+ constraints,
+ softWrap,
+ overflow,
+ paragraphIntrinsics!!.maxIntrinsicWidth
+ )
private fun markDirty() {
paragraphIntrinsics = null
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
index dadc584..b724079 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
@@ -28,7 +28,6 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.resolveDefaults
-import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -158,6 +157,20 @@
constraints
}
if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+ if (finalConstraints != prevConstraints) {
+ // ensure size and overflow is still accurate
+ val localParagraph = paragraph!!
+ val localSize = finalConstraints.constrain(
+ IntSize(
+ localParagraph.width.ceilToIntPx(),
+ localParagraph.height.ceilToIntPx()
+ )
+ )
+ layoutSize = localSize
+ didOverflow = overflow != TextOverflow.Visible &&
+ (localSize.width < localParagraph.width ||
+ localSize.height < localParagraph.height)
+ }
return false
}
paragraph = layoutText(finalConstraints, layoutDirection).also {
@@ -252,52 +265,16 @@
): Paragraph {
val localParagraphIntrinsics = setLayoutDirection(layoutDirection)
- val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
- val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
- constraints.maxWidth
- } else {
- Constraints.Infinity
- }
-
- // This is a fallback behavior because native text layout doesn't support multiple
- // ellipsis in one text layout.
- // When softWrap is turned off and overflow is ellipsis, it's expected that each line
- // that exceeds maxWidth will be ellipsized.
- // For example,
- // input text:
- // "AAAA\nAAAA"
- // maxWidth:
- // 3 * fontSize that only allow 3 characters to be displayed each line.
- // expected output:
- // AA…
- // AA…
- // Here we assume there won't be any '\n' character when softWrap is false. And make
- // maxLines 1 to implement the similar behavior.
- val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
- val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
-
- // if minWidth == maxWidth the width is fixed.
- // therefore we can pass that value to our paragraph and use it
- // if minWidth != maxWidth there is a range
- // then we should check if the max intrinsic width is in this range to decide the
- // width to be passed to Paragraph
- // if max intrinsic width is between minWidth and maxWidth
- // we can use it to layout
- // else if max intrinsic width is greater than maxWidth, we can only use maxWidth
- // else if max intrinsic width is less than minWidth, we should use minWidth
- val width = if (minWidth == maxWidth) {
- maxWidth
- } else {
- localParagraphIntrinsics.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
- }
-
- val finalConstraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight)
return Paragraph(
- paragraphIntrinsics = paragraphIntrinsics!!,
- constraints = finalConstraints,
+ paragraphIntrinsics = localParagraphIntrinsics,
+ constraints = finalConstraints(
+ constraints,
+ softWrap,
+ overflow,
+ localParagraphIntrinsics.maxIntrinsicWidth
+ ),
// This is a fallback behavior for ellipsis. Native
- maxLines = finalMaxLines,
+ maxLines = finalMaxLines(softWrap, overflow, maxLines),
ellipsis = overflow == TextOverflow.Ellipsis
)
}
@@ -310,6 +287,7 @@
constraints: Constraints,
layoutDirection: LayoutDirection
): Boolean {
+ // paragarph and paragraphIntrinsics are from previous run
val localParagraph = paragraph ?: return true
val localParagraphIntrinsics = paragraphIntrinsics ?: return true
// no layout yet
@@ -323,28 +301,15 @@
// if we were passed identical constraints just skip more work
if (constraints == prevConstraints) return false
- // only be clever if we can predict line break behavior exactly, which is only possible with
- // simple geometry math for the greedy layout case
- if (style.lineBreak != LineBreak.Simple) {
- return true
- }
-
- // see if width would produce the same wraps (greedy wraps only)
- val canWrap = softWrap && maxLines > 1
- if (canWrap && layoutSize.width != localParagraph.maxIntrinsicWidth.ceilToIntPx()) {
- // some soft wrapping happened, check to see if we're between the previous measure and
- // the next wrap
- val prevActualMaxWidth = maxWidth(prevConstraints)
- val newMaxWidth = maxWidth(constraints)
- if (newMaxWidth > prevActualMaxWidth) {
- // we've grown the potential layout area, and may break longer lines
- return true
- }
- if (newMaxWidth <= layoutSize.width) {
- // it's possible to shrink this text (possible opt: check minIntrinsicWidth
- return true
- }
- }
+ // see if width would produce the same wraps
+ if (canChangeBreaks(
+ canWrap = softWrap && maxLines > 1,
+ newConstraints = constraints,
+ oldConstraints = prevConstraints,
+ maxIntrinsicWidth = localParagraphIntrinsics.maxIntrinsicWidth,
+ softWrap = softWrap,
+ overflow = overflow
+ )) return true
// if we get here width won't change, height may be clipped
if (constraints.maxHeight < localParagraph.height) {
@@ -356,26 +321,6 @@
return false
}
- /**
- * Compute the maxWidth for text layout from [Constraints]
- *
- * Falls back to [paragraphIntrinsics.maxIntrinsicWidth] when not exact constraints.
- */
- private fun maxWidth(constraints: Constraints): Int {
- val minWidth = constraints.minWidth
- val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
- val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
- constraints.maxWidth
- } else {
- Constraints.Infinity
- }
- return if (minWidth == maxWidth) {
- maxWidth
- } else {
- paragraphIntrinsics!!.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
- }
- }
-
private fun markDirty() {
paragraph = null
paragraphIntrinsics = null
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
index ea02955..736956c 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
@@ -16,17 +16,14 @@
package androidx.compose.foundation.relocation
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Composable
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
/**
* Platform specific internal API to bring a rectangle into view.
*/
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
- return BringIntoViewParent { _, _ ->
+internal actual fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+ BringIntoViewParent =
+ BringIntoViewParent { _, _ ->
// TODO(b/203204124): Implement this if desktop has a
// concept similar to Android's View.requestRectangleOnScreen.
- }
-}
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 5e3f0aa..2ecb589 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -61,8 +61,11 @@
testImplementation(libs.truth)
androidTestImplementation(project(":compose:material3:material3:material3-samples"))
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.4.0-beta02")
+ androidTestImplementation("androidx.compose.ui:ui-test:1.4.0-beta02")
+ androidTestImplementation(project(":compose:test-utils")){
+ transitive = false
+ }
androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation(project(":core:core"))
androidTestImplementation(libs.testRules)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt
new file mode 100644
index 0000000..e336c3e
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.runtime.Stable
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/*** This is an internal copy of androidx.compose.foundation.MutatorMutex with an additional
+ * tryMutate method. Do not modify, except for tryMutate. ***/
+
+expect class InternalAtomicReference<V>(value: V) {
+ fun get(): V
+ fun set(value: V)
+ fun getAndSet(value: V): V
+ fun compareAndSet(expect: V, newValue: V): Boolean
+}
+
+/**
+ * Mutual exclusion for UI state mutation over time.
+ *
+ * [mutate] permits interruptible state mutation over time using a standard [MutatePriority].
+ * A [InternalMutatorMutex] enforces that only a single writer can be active at a time for a particular
+ * state resource. Instead of queueing callers that would acquire the lock like a traditional
+ * [Mutex], new attempts to [mutate] the guarded state will either cancel the current mutator or
+ * if the current mutator has a higher priority, the new caller will throw [CancellationException].
+ *
+ * [InternalMutatorMutex] should be used for implementing hoisted state objects that many mutators may
+ * want to manipulate over time such that those mutators can coordinate with one another. The
+ * [InternalMutatorMutex] instance should be hidden as an implementation detail. For example:
+ *
+ */
+@Stable
+internal class InternalMutatorMutex {
+ private class Mutator(val priority: MutatePriority, val job: Job) {
+ fun canInterrupt(other: Mutator) = priority >= other.priority
+
+ fun cancel() = job.cancel()
+ }
+
+ private val currentMutator = InternalAtomicReference<Mutator?>(null)
+ private val mutex = Mutex()
+
+ private fun tryMutateOrCancel(mutator: Mutator) {
+ while (true) {
+ val oldMutator = currentMutator.get()
+ if (oldMutator == null || mutator.canInterrupt(oldMutator)) {
+ if (currentMutator.compareAndSet(oldMutator, mutator)) {
+ oldMutator?.cancel()
+ break
+ }
+ } else throw CancellationException("Current mutation had a higher priority")
+ }
+ }
+
+ /**
+ * Enforce that only a single caller may be active at a time.
+ *
+ * If [mutate] is called while another call to [mutate] or [mutateWith] is in progress, their
+ * [priority] values are compared. If the new caller has a [priority] equal to or higher than
+ * the call in progress, the call in progress will be cancelled, throwing
+ * [CancellationException] and the new caller's [block] will be invoked. If the call in
+ * progress had a higher [priority] than the new caller, the new caller will throw
+ * [CancellationException] without invoking [block].
+ *
+ * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+ * Higher priority mutations will interrupt lower priority mutations.
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ */
+ suspend fun <R> mutate(
+ priority: MutatePriority = MutatePriority.Default,
+ block: suspend () -> R
+ ) = coroutineScope {
+ val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+ tryMutateOrCancel(mutator)
+
+ mutex.withLock {
+ try {
+ block()
+ } finally {
+ currentMutator.compareAndSet(mutator, null)
+ }
+ }
+ }
+
+ /**
+ * Enforce that only a single caller may be active at a time.
+ *
+ * If [mutateWith] is called while another call to [mutate] or [mutateWith] is in progress,
+ * their [priority] values are compared. If the new caller has a [priority] equal to or
+ * higher than the call in progress, the call in progress will be cancelled, throwing
+ * [CancellationException] and the new caller's [block] will be invoked. If the call in
+ * progress had a higher [priority] than the new caller, the new caller will throw
+ * [CancellationException] without invoking [block].
+ *
+ * This variant of [mutate] calls its [block] with a [receiver], removing the need to create
+ * an additional capturing lambda to invoke it with a receiver object. This can be used to
+ * expose a mutable scope to the provided [block] while leaving the rest of the state object
+ * read-only. For example:
+ *
+ * @param receiver the receiver `this` that [block] will be called with
+ * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+ * Higher priority mutations will interrupt lower priority mutations.
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ */
+ suspend fun <T, R> mutateWith(
+ receiver: T,
+ priority: MutatePriority = MutatePriority.Default,
+ block: suspend T.() -> R
+ ) = coroutineScope {
+ val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+ tryMutateOrCancel(mutator)
+
+ mutex.withLock {
+ try {
+ receiver.block()
+ } finally {
+ currentMutator.compareAndSet(mutator, null)
+ }
+ }
+ }
+
+ /**
+ * Attempt to mutate synchronously if there is no other active caller.
+ * If there is no other active caller, the [block] will be executed in a lock. If there is
+ * another active caller, this method will return false, indicating that the active caller
+ * needs to be cancelled through a [mutate] or [mutateWith] call with an equal or higher
+ * mutation priority.
+ *
+ * Calls to [mutate] and [mutateWith] will suspend until execution of the [block] has finished.
+ *
+ * @param block mutation code to run mutually exclusive with any other call to [mutate],
+ * [mutateWith] or [tryMutate].
+ * @return true if the [block] was executed, false if there was another active caller and the
+ * [block] was not executed.
+ */
+ fun tryMutate(block: () -> Unit): Boolean {
+ val didLock = mutex.tryLock()
+ if (didLock) {
+ try {
+ block()
+ } finally {
+ mutex.unlock()
+ }
+ }
+ return didLock
+ }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index 31776bd..854505f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -285,7 +285,7 @@
bottomPadding: Float,
onDragStopped: CoroutineScope.(velocity: Float) -> Unit,
) = draggable(
- state = sheetState.swipeableState.draggableState,
+ state = sheetState.swipeableState.swipeDraggableState,
orientation = Orientation.Vertical,
enabled = sheetState.isVisible,
startDragImmediately = sheetState.swipeableState.isAnimationRunning,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
index fca009d..7696bfe 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -338,7 +338,7 @@
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.toFloat()
val currentOffset = sheetState.requireOffset()
- return if (toFling < 0 && currentOffset > sheetState.swipeableState.minBound) {
+ return if (toFling < 0 && currentOffset > sheetState.swipeableState.minOffset) {
onFling(toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index d57af0c..77bdf92 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -21,6 +21,8 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.DragScope
import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
@@ -50,6 +52,7 @@
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
@@ -79,7 +82,7 @@
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null
) = draggable(
- state = state.draggableState,
+ state = state.swipeDraggableState,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
@@ -122,7 +125,11 @@
val previousTarget = state.targetValue
val stateRequiresCleanup = state.updateAnchors(newAnchors)
if (stateRequiresCleanup) {
- anchorChangeHandler?.onAnchorsChanged(previousTarget, previousAnchors, newAnchors)
+ anchorChangeHandler?.onAnchorsChanged(
+ previousTarget,
+ previousAnchors,
+ newAnchors
+ )
}
}
},
@@ -165,6 +172,27 @@
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
+ private val swipeMutex = InternalMutatorMutex()
+
+ internal val swipeDraggableState = object : DraggableState {
+ private val dragScope = object : DragScope {
+ override fun dragBy(pixels: Float) {
+ [email protected](pixels)
+ }
+ }
+
+ override suspend fun drag(
+ dragPriority: MutatePriority,
+ block: suspend DragScope.() -> Unit
+ ) {
+ swipe(dragPriority) { dragScope.block() }
+ }
+
+ override fun dispatchRawDelta(delta: Float) {
+ [email protected](delta)
+ }
+ }
+
/**
* The current value of the [SwipeableV2State].
*/
@@ -242,13 +270,19 @@
var lastVelocity: Float by mutableStateOf(0f)
private set
- val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
- val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
+ /**
+ * The minimum offset this state can reach. This will be the smallest anchor, or
+ * [Float.NEGATIVE_INFINITY] if the anchors are not initialized yet.
+ */
+ val minOffset by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
+
+ /**
+ * The maximum offset this state can reach. This will be the biggest anchor, or
+ * [Float.POSITIVE_INFINITY] if the anchors are not initialized yet.
+ */
+ val maxOffset by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
private var animationTarget: T? by mutableStateOf(null)
- internal val draggableState = DraggableState {
- offset = ((offset ?: 0f) + it).coerceIn(minBound, maxBound)
- }
internal var anchors by mutableStateOf(emptyMap<T, Float>())
@@ -267,9 +301,10 @@
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
val initialValueHasAnchor = if (previousAnchorsEmpty) {
- val initialValueAnchor = anchors[currentValue]
+ val initialValue = currentValue
+ val initialValueAnchor = anchors[initialValue]
val initialValueHasAnchor = initialValueAnchor != null
- if (initialValueHasAnchor) offset = initialValueAnchor
+ if (initialValueHasAnchor) trySnapTo(initialValue)
initialValueHasAnchor
} else true
return !initialValueHasAnchor || !previousAnchorsEmpty
@@ -282,6 +317,8 @@
/**
* Snap to a [targetValue] without any animation.
+ * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
+ * [targetValue] without updating the offset.
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
@@ -289,16 +326,7 @@
* @param targetValue The target value of the animation
*/
suspend fun snapTo(targetValue: T) {
- val targetOffset = anchors.requireAnchor(targetValue)
- try {
- draggableState.drag {
- animationTarget = targetValue
- dragBy(targetOffset - requireOffset())
- }
- this.currentValue = targetValue
- } finally {
- animationTarget = null
- }
+ swipe { snap(targetValue) }
}
/**
@@ -319,7 +347,7 @@
val targetOffset = anchors[targetValue]
if (targetOffset != null) {
try {
- draggableState.drag {
+ swipe {
animationTarget = targetValue
var prev = offset ?: 0f
animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
@@ -366,17 +394,17 @@
}
/**
- * Swipe by the [delta], coerce it in the bounds and dispatch it to the [draggableState].
+ * Swipe by the [delta], coerce it in the bounds and dispatch it to the [SwipeableV2State].
*
- * @return The delta the [draggableState] will consume
+ * @return The delta the consumed by the [SwipeableV2State]
*/
fun dispatchRawDelta(delta: Float): Float {
val currentDragPosition = offset ?: 0f
val potentiallyConsumed = currentDragPosition + delta
- val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+ val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
val deltaToConsume = clamped - currentDragPosition
- if (abs(deltaToConsume) > 0) {
- draggableState.dispatchRawDelta(deltaToConsume)
+ if (abs(deltaToConsume) >= 0) {
+ offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
}
return deltaToConsume
}
@@ -428,6 +456,31 @@
"this=$this SwipeableState?"
}
+ private suspend fun swipe(
+ swipePriority: MutatePriority = MutatePriority.Default,
+ action: suspend () -> Unit
+ ): Unit = coroutineScope { swipeMutex.mutate(swipePriority, action) }
+
+ /**
+ * Attempt to snap synchronously. Snapping can happen synchronously when there is no other swipe
+ * transaction like a drag or an animation is progress. If there is another interaction in
+ * progress, the suspending [snapTo] overload needs to be used.
+ *
+ * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
+ */
+ internal fun trySnapTo(targetValue: T): Boolean = swipeMutex.tryMutate { snap(targetValue) }
+
+ private fun snap(targetValue: T) {
+ val targetOffset = anchors[targetValue]
+ if (targetOffset != null) {
+ dispatchRawDelta(targetOffset - (offset ?: 0f))
+ currentValue = targetValue
+ animationTarget = null
+ } else {
+ currentValue = targetValue
+ }
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableV2State].
@@ -636,6 +689,3 @@
private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
- "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
-}
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt b/compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
similarity index 60%
rename from compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt
rename to compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
index 12a9f5a..65a109b 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt
+++ b/compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
@@ -1,5 +1,7 @@
+// ktlint-disable filename
+
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +16,10 @@
* limitations under the License.
*/
-package androidx.compose.ui.text.platform.extensions
+package androidx.compose.material3
-import android.text.style.URLSpan
-import androidx.compose.ui.text.ExperimentalTextApi
-import androidx.compose.ui.text.UrlAnnotation
-
-@ExperimentalTextApi
-fun UrlAnnotation.toSpan(): URLSpan = URLSpan(url)
\ No newline at end of file
+/* Copy of androidx.compose.material.ActualJvm, mirrored from Foundation. This is used for the
+ M2/M3-internal copy of MutatorMutex.
+ */
+internal actual typealias InternalAtomicReference<V> =
+ java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index fb53cf4..11478bf 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -1260,6 +1260,15 @@
}
+package androidx.compose.ui.text.platform {
+
+ @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
+ ctor public URLSpanCache();
+ method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
+ }
+
+}
+
package androidx.compose.ui.text.platform.extensions {
public final class TtsAnnotationExtensions_androidKt {
@@ -1267,10 +1276,6 @@
method public static android.text.style.TtsSpan toSpan(androidx.compose.ui.text.VerbatimTtsAnnotation);
}
- public final class UrlAnnotationExtensions_androidKt {
- method @androidx.compose.ui.text.ExperimentalTextApi public static android.text.style.URLSpan toSpan(androidx.compose.ui.text.UrlAnnotation);
- }
-
}
package androidx.compose.ui.text.style {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index 55fdaed..9c0efcb 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -58,17 +58,18 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(InternalTextApi::class)
+@OptIn(InternalTextApi::class, ExperimentalTextApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class AndroidAccessibilitySpannableStringTest {
private val context = InstrumentationRegistry.getInstrumentation().context
private val density = Density(context)
- private val resourceLoader = UncachedFontFamilyResolver(context)
+ private val fontFamilyResolver = UncachedFontFamilyResolver(context)
+ private val urlSpanCache = URLSpanCache()
@Test
fun toAccessibilitySpannableString_with_locale() {
@@ -80,8 +81,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -102,8 +106,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -123,8 +130,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -144,8 +154,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -164,8 +177,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -184,8 +200,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -205,11 +224,14 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
- Truth.assertThat(
+ assertThat(
spannableString.getSpans(
0,
spannableString.length,
@@ -227,8 +249,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -245,8 +270,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -266,8 +294,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -287,8 +318,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -298,7 +332,6 @@
}
}
- @OptIn(ExperimentalTextApi::class)
@Test
fun toAccessibilitySpannableString_with_verbatimTtsAnnotation() {
val annotatedString = buildAnnotatedString {
@@ -308,8 +341,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -320,7 +356,6 @@
}
}
- @OptIn(ExperimentalTextApi::class)
@Test
fun toAccessibilitySpannableString_with_urlAnnotation() {
val annotatedString = buildAnnotatedString {
@@ -330,8 +365,11 @@
}
}
- val spannableString =
- annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+ val spannableString = annotatedString.toAccessibilitySpannableString(
+ density,
+ fontFamilyResolver,
+ urlSpanCache
+ )
assertThat(spannableString).isInstanceOf(SpannableString::class.java)
assertThat(spannableString).hasSpan(
@@ -341,7 +379,6 @@
}
}
- @OptIn(InternalTextApi::class)
@Test
fun fontsInSpanStyles_areIgnored() {
// b/232238615
@@ -356,9 +393,32 @@
val fontFamilyResolver = createFontFamilyResolver(context)
// see if font span is added
- string.toAccessibilitySpannableString(density, fontFamilyResolver)
+ string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache())
// toAccessibilitySpannableString should _not_ make any font requests
- Truth.assertThat(loader.blockingRequests).isEmpty()
+ assertThat(loader.blockingRequests).isEmpty()
+ }
+
+ /**
+ * A11y services rely on ClickableSpans being the same instance for the same source spans across
+ * multiple calls of [toAccessibilitySpannableString] for the same source string. If this is not
+ * the case, their onClick handler will not be invoked. See b/253292081.
+ */
+ @Test
+ fun urlSpansAreSameInstanceForSameAnnotatedString() {
+ val string = buildAnnotatedString {
+ pushUrlAnnotation(UrlAnnotation("google.com"))
+ append("link")
+ }
+
+ val spannable1 =
+ string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+ val spannable2 =
+ string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+ val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
+ val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
+
+ assertThat(spannable1).isNotSameInstanceAs(spannable2)
+ assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
index 83666c8..f9f160e 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
@@ -32,13 +32,11 @@
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.GenericFontFamily
-import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.font.getAndroidTypefaceStyle
import androidx.compose.ui.text.platform.extensions.setBackground
import androidx.compose.ui.text.platform.extensions.setColor
@@ -49,27 +47,13 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastForEach
-/**
- * Convert an AnnotatedString into SpannableString for Android text to speech support.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@InternalTextApi // used in ui:ui
-fun AnnotatedString.toAccessibilitySpannableString(
- density: Density,
- @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader
-): SpannableString {
- @Suppress("DEPRECATION")
- return toAccessibilitySpannableString(density, createFontFamilyResolver(resourceLoader))
-}
-
@OptIn(ExperimentalTextApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@InternalTextApi // used in ui:ui
fun AnnotatedString.toAccessibilitySpannableString(
density: Density,
- fontFamilyResolver: FontFamily.Resolver
+ fontFamilyResolver: FontFamily.Resolver,
+ urlSpanCache: URLSpanCache
): SpannableString {
val spannableString = SpannableString(text)
spanStylesOrNull?.fastForEach { (style, start, end) ->
@@ -90,7 +74,7 @@
getUrlAnnotations(0, length).fastForEach { (urlAnnotation, start, end) ->
spannableString.setSpan(
- urlAnnotation.toSpan(),
+ urlSpanCache.toURLSpan(urlAnnotation),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt
new file mode 100644
index 0000000..a8444ba
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.platform
+
+import android.text.style.URLSpan
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.UrlAnnotation
+import java.util.WeakHashMap
+
+/**
+ * This class converts [UrlAnnotation]s to [URLSpan]s, ensuring that the same instance of [URLSpan]
+ * will be returned for every instance of [UrlAnnotation]. This is required for [URLSpan]s (and
+ * any ClickableSpan) to be handled correctly by accessibility services, which require every
+ * ClickableSpan to have a stable ID across reads from the accessibility node. A11y services convert
+ * these spans to parcelable ones, then look them up later using their ID. Since the ID is a hidden
+ * property, the only way to satisfy this constraint is to actually use the same [URLSpan] instance
+ * every time.
+ *
+ * See b/253292081.
+ */
+// "URL" violates naming guidelines, but that is intentional to match the platform API.
+@Suppress("AcronymName")
+@OptIn(ExperimentalTextApi::class)
+@InternalTextApi
+class URLSpanCache {
+ private val spansByAnnotation = WeakHashMap<UrlAnnotation, URLSpan>()
+
+ @Suppress("AcronymName")
+ fun toURLSpan(urlAnnotation: UrlAnnotation): URLSpan =
+ spansByAnnotation.getOrPut(urlAnnotation) { URLSpan(urlAnnotation.url) }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index fc1e5aa..cab650d 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1764,6 +1764,11 @@
method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
}
+ public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ }
+
public final class RotaryScrollEvent {
method public float getHorizontalScrollPixels();
method public long getUptimeMillis();
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 5c219f3..3d089cf 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1573,6 +1573,16 @@
method public static int getNativeKeyCode(long);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftKeyboardInterceptionModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onInterceptKeyBeforeSoftKeyboard(android.view.KeyEvent event);
+ method public boolean onPreInterceptKeyBeforeSoftKeyboard(android.view.KeyEvent event);
+ }
+
+ public final class SoftwareKeyboardInterceptionModifierKt {
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier onInterceptKeyBeforeSoftKeyboard(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onInterceptKeyBeforeSoftKeyboard);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier onPreInterceptKeyBeforeSoftKeyboard(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreInterceptKeyBeforeSoftKeyboard);
+ }
+
}
package androidx.compose.ui.input.nestedscroll {
@@ -1910,7 +1920,7 @@
method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 6839e9b..272ecd7 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1764,6 +1764,11 @@
method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
}
+ public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ }
+
public final class RotaryScrollEvent {
method public float getHorizontalScrollPixels();
method public long getUptimeMillis();
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
index 7641c8a..cdb4b23 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
@@ -20,8 +20,11 @@
import android.os.Build
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.AtLeastSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.Padding
@@ -41,24 +44,29 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.padding
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.runOnUiThreadIR
import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.waitAndScreenShot
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -511,6 +519,48 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun resizingIsReflectedInGraphicsLayer() {
+ var sizePx by mutableStateOf(20)
+ rule.runOnUiThread {
+ activity.setContent {
+ Box(
+ modifier = Modifier
+ .background(Color.White)
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(30, 30) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(Constraints.fixed(sizePx, sizePx))
+ layout(
+ constraints.constrainWidth(sizePx),
+ constraints.constrainHeight(sizePx)
+ ) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+ .clipToBounds()
+ .fillColor(Color.Red)
+ )
+ }
+ }
+
+ takeScreenShot(30).apply {
+ assertRect(Color.Red, size = 20, centerX = 10, centerY = 10)
+ }
+
+ drawLatch = CountDownLatch(1)
+ sizePx = 30
+
+ takeScreenShot(30).apply {
+ assertRect(Color.Red, size = 30)
+ }
+ }
+
private fun Modifier.fillColor(color: Color): Modifier {
return drawBehind {
drawRect(
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
index e4256b6..3baf4aa 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -17,8 +17,9 @@
package androidx.compose.ui.input.focus
import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_A
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -26,21 +27,24 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.setFocusableContent
+import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.InterruptedSoftKeyboardInput
import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.KeyInput
import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.RotaryInput
import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyInputInputModifierNodeImpl
-import androidx.compose.ui.input.rotary.RotaryInputModifierNodeImpl
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.input.key.onPreInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.rotary.RotaryScrollEvent
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.input.rotary.onPreRotaryScrollEvent
+import androidx.compose.ui.input.rotary.onRotaryScrollEvent
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.test.performRotaryScrollInput
-import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -59,10 +63,8 @@
val rule = createComposeRule()
private val sentEvent: Any = when (nodeType) {
- KeyInput ->
- KeyEvent(AndroidKeyEvent(AndroidKeyEvent.ACTION_DOWN, AndroidKeyEvent.KEYCODE_A))
- RotaryInput ->
- RotaryScrollEvent(1f, 1f, 0L)
+ KeyInput, InterruptedSoftKeyboardInput -> KeyEvent(AndroidKeyEvent(ACTION_DOWN, KEYCODE_A))
+ RotaryInput -> RotaryScrollEvent(1f, 1f, 0L)
}
private var receivedEvent: Any? = null
private val initialFocus = FocusRequester()
@@ -70,7 +72,7 @@
companion object {
@JvmStatic
@Parameterized.Parameters(name = "node = {0}")
- fun initParameters() = arrayOf(KeyInput, RotaryInput)
+ fun initParameters() = arrayOf(KeyInput, InterruptedSoftKeyboardInput, RotaryInput)
}
@Before
@@ -102,7 +104,8 @@
// Assert.
assertThat(receivedEvent).isNull()
when (nodeType) {
- KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
+ KeyInput, InterruptedSoftKeyboardInput ->
+ assertThat(error!!.message).contains("do not have an active focus target")
RotaryInput -> assertThat(error).isNull()
}
}
@@ -133,14 +136,14 @@
assertThat(receivedEvent).isNull()
when (nodeType) {
KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
- RotaryInput -> assertThat(error).isNull()
+ InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
}
}
@Test
fun onFocusAwareEvent_afterFocusable_isNotTriggered() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.focusable(initiallyFocused = true)
@@ -158,14 +161,14 @@
// Assert.
when (nodeType) {
KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
- RotaryInput -> assertThat(receivedEvent).isNull()
+ InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
}
}
@Test
fun onPreFocusAwareEvent_afterFocusable_isNotTriggered() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.focusable(initiallyFocused = true)
@@ -182,14 +185,14 @@
// Assert.
when (nodeType) {
KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
- RotaryInput -> assertThat(receivedEvent).isNull()
+ InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
}
}
@Test
fun onFocusAwareEvent_isTriggered() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -210,7 +213,7 @@
@Test
fun onPreFocusAwareEvent_triggered() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onPreFocusAwareEvent {
@@ -231,7 +234,7 @@
@Test
fun onFocusAwareEventNotTriggered_ifOnPreFocusAwareEventConsumesEvent_1() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -257,7 +260,7 @@
@Test
fun onFocusAwareEventNotTriggered_ifOnPreFocusAwareEventConsumesEvent_2() {
// Arrange.
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onPreFocusAwareEvent {
@@ -286,7 +289,7 @@
var triggerIndex = 1
var onFocusAwareEventTrigger = 0
var onPreFocusAwareEventTrigger = 0
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -317,7 +320,7 @@
var triggerIndex = 1
var onFocusAwareEventTrigger = 0
var onPreFocusAwareEventTrigger = 0
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onPreFocusAwareEvent {
@@ -350,7 +353,7 @@
var parentOnPreFocusAwareEventTrigger = 0
var childOnFocusAwareEventTrigger = 0
var childOnPreFocusAwareEventTrigger = 0
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -398,7 +401,7 @@
var parentOnPreFocusAwareEventTrigger = 0
var childOnFocusAwareEventTrigger = 0
var childOnPreFocusAwareEventTrigger = 0
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -447,7 +450,7 @@
var parentOnPreFocusAwareEventTrigger = 0
var childOnFocusAwareEventTrigger = 0
var childOnPreFocusAwareEventTrigger = 0
- ContentWithInitialFocus {
+ rule.setContentWithInitialFocus {
Box(
modifier = Modifier
.onFocusAwareEvent {
@@ -508,7 +511,7 @@
private fun SemanticsNodeInteraction.performFocusAwareInput(sentEvent: Any) {
when (nodeType) {
- KeyInput -> {
+ KeyInput, InterruptedSoftKeyboardInput -> {
check(sentEvent is KeyEvent)
performKeyPress(sentEvent)
}
@@ -522,55 +525,24 @@
}
}
- private fun ContentWithInitialFocus(content: @Composable () -> Unit) {
- rule.setContent {
- Box(modifier = Modifier.requiredSize(10.dp, 10.dp)) { content() }
- }
- rule.runOnIdle { initialFocus.requestFocus() }
+ private fun ComposeContentTestRule.setContentWithInitialFocus(content: @Composable () -> Unit) {
+ setFocusableContent(content)
+ runOnIdle { initialFocus.requestFocus() }
}
- private fun Modifier.onFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
- FocusAwareEventElement(onEvent, nodeType, EventType.OnEvent)
- )
-
- private fun Modifier.onPreFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
- FocusAwareEventElement(onEvent, nodeType, EventType.PreEvent)
- )
-
@OptIn(ExperimentalComposeUiApi::class)
- private data class FocusAwareEventElement(
- private val callback: (Any) -> Boolean,
- private val nodeType: NodeType,
- private val eventType: EventType
- ) : ModifierNodeElement<Modifier.Node>() {
- override fun create() = when (nodeType) {
- KeyInput -> KeyInputInputModifierNodeImpl(
- onEvent = callback.takeIf { eventType == EventType.OnEvent },
- onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
- )
- RotaryInput -> RotaryInputModifierNodeImpl(
- onEvent = callback.takeIf { eventType == EventType.OnEvent },
- onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
- )
- }
-
- override fun update(node: Modifier.Node) = when (nodeType) {
- KeyInput -> (node as KeyInputInputModifierNodeImpl).apply {
- onEvent = callback.takeIf { eventType == EventType.OnEvent }
- onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
- }
- RotaryInput -> (node as RotaryInputModifierNodeImpl).apply {
- onEvent = callback.takeIf { eventType == EventType.OnEvent }
- onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
- }
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onEvent"
- properties["onEvent"] = callback
- }
+ private fun Modifier.onFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = when (nodeType) {
+ KeyInput -> onKeyEvent(onEvent)
+ InterruptedSoftKeyboardInput -> onInterceptKeyBeforeSoftKeyboard(onEvent)
+ RotaryInput -> onRotaryScrollEvent(onEvent)
}
- enum class NodeType { KeyInput, RotaryInput }
- enum class EventType { PreEvent, OnEvent }
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun Modifier.onPreFocusAwareEvent(onPreEvent: (Any) -> Boolean) = when (nodeType) {
+ KeyInput -> onPreviewKeyEvent(onPreEvent)
+ InterruptedSoftKeyboardInput -> onPreInterceptKeyBeforeSoftKeyboard(onPreEvent)
+ RotaryInput -> onPreRotaryScrollEvent(onPreEvent)
+ }
+
+ enum class NodeType { KeyInput, InterruptedSoftKeyboardInput, RotaryInput }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
new file mode 100644
index 0000000..5b4c1ba
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.key
+
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.KEYCODE_A as KeyCodeA
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.focus.setFocusableContent
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalComposeUiApi::class)
+class HardwareKeyInputTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ val initialFocus = FocusRequester()
+
+ @Test
+ fun onKeyToSoftKeyboardInterceptedEventTriggered() {
+ // Arrange.
+ var receivedKeyEvent: KeyEvent? = null
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onInterceptKeyBeforeSoftKeyboard {
+ receivedKeyEvent = it
+ true
+ }
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ val keyEvent = checkNotNull(receivedKeyEvent)
+ assertThat(keyEvent.key).isEqualTo(Key.A)
+ assertThat(keyEvent.type).isEqualTo(KeyUp)
+ assertThat(keyConsumed).isTrue()
+ }
+ }
+
+ @Test
+ fun onPreKeyToSoftKeyboardInterceptedEventTriggered() {
+ // Arrange.
+ var receivedKeyEvent: KeyEvent? = null
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ receivedKeyEvent = it
+ true
+ }
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ val keyEvent = checkNotNull(receivedKeyEvent)
+ assertThat(keyEvent.key).isEqualTo(Key.A)
+ assertThat(keyEvent.type).isEqualTo(KeyUp)
+ assertThat(keyConsumed).isTrue()
+ }
+ }
+
+ @Test
+ fun onKeyEventNotTriggered_ifOnPreKeyEventConsumesEvent() {
+ // Arrange.
+ var receivedPreKeyEvent: KeyEvent? = null
+ var receivedKeyEvent: KeyEvent? = null
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onInterceptKeyBeforeSoftKeyboard {
+ receivedKeyEvent = it
+ true
+ }
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ receivedPreKeyEvent = it
+ true
+ }
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ val keyEvent = checkNotNull(receivedPreKeyEvent)
+ assertThat(keyEvent.key).isEqualTo(Key.A)
+ assertThat(keyEvent.type).isEqualTo(KeyUp)
+ assertThat(keyConsumed).isTrue()
+
+ assertThat(receivedKeyEvent).isNull()
+ }
+ }
+
+ @Test
+ fun onKeyEvent_triggeredAfter_onPreviewKeyEvent() {
+ // Arrange.
+ var triggerIndex = 1
+ var onInterceptedKeyEventTrigger = 0
+ var onInterceptedPreKeyEventTrigger = 0
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onInterceptKeyBeforeSoftKeyboard {
+ onInterceptedKeyEventTrigger = triggerIndex++
+ true
+ }
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ onInterceptedPreKeyEventTrigger = triggerIndex++
+ false
+ }
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(onInterceptedPreKeyEventTrigger).isEqualTo(1)
+ assertThat(onInterceptedKeyEventTrigger).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun onKeyEvent_onKeyToSoftKeyboardInterceptedEvent_interaction() {
+ // Arrange.
+ var triggerIndex = 1
+ var onInterceptedKeyEventTrigger = 0
+ var onInterceptedPreKeyEventTrigger = 0
+ var onKeyEventTrigger = 0
+ var onPreviewKeyEventTrigger = 0
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onKeyEvent {
+ onKeyEventTrigger = triggerIndex++
+ false
+ }
+ .onPreviewKeyEvent {
+ onPreviewKeyEventTrigger = triggerIndex++
+ false
+ }
+ .onInterceptKeyBeforeSoftKeyboard {
+ onInterceptedKeyEventTrigger = triggerIndex++
+ false
+ }
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ onInterceptedPreKeyEventTrigger = triggerIndex++
+ false
+ }
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(onInterceptedPreKeyEventTrigger).isEqualTo(1)
+ assertThat(onInterceptedKeyEventTrigger).isEqualTo(2)
+ assertThat(onPreviewKeyEventTrigger).isEqualTo(3)
+ assertThat(onKeyEventTrigger).isEqualTo(4)
+ }
+ }
+
+ @Test
+ fun onKeyEvent_onKeyToSoftKeyboardInterceptedEvent_parentChildInteraction() {
+ // Arrange.
+ var triggerIndex = 1
+ var onInterceptedKeyEventChildTrigger = 0
+ var onInterceptedKeyEventParentTrigger = 0
+ var onPreInterceptedKeyEventChildTrigger = 0
+ var onPreInterceptedKeyEvenParentTrigger = 0
+ var onKeyEventChildTrigger = 0
+ var onKeyEventParentTrigger = 0
+ var onPreKeyEventChildTrigger = 0
+ var onPreKeyEventParentTrigger = 0
+ rule.setContentWithInitialFocus {
+ Box(
+ Modifier
+ .onKeyEvent {
+ onKeyEventParentTrigger = triggerIndex++
+ false
+ }
+ .onPreviewKeyEvent {
+ onPreKeyEventParentTrigger = triggerIndex++
+ false
+ }
+ .onInterceptKeyBeforeSoftKeyboard {
+ onInterceptedKeyEventParentTrigger = triggerIndex++
+ false
+ }
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ onPreInterceptedKeyEvenParentTrigger = triggerIndex++
+ false
+ }
+ ) {
+ Box(
+ Modifier
+ .onKeyEvent {
+ onKeyEventChildTrigger = triggerIndex++
+ false
+ }
+ .onPreviewKeyEvent {
+ onPreKeyEventChildTrigger = triggerIndex++
+ false
+ }
+ .onInterceptKeyBeforeSoftKeyboard {
+ onInterceptedKeyEventChildTrigger = triggerIndex++
+ false
+ }
+ .onPreInterceptKeyBeforeSoftKeyboard {
+ onPreInterceptedKeyEventChildTrigger = triggerIndex++
+ false
+ }
+ .focusRequester(initialFocus)
+ .focusable()
+ )
+ }
+ }
+
+ // Act.
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(onPreInterceptedKeyEvenParentTrigger).isEqualTo(1)
+ assertThat(onPreInterceptedKeyEventChildTrigger).isEqualTo(2)
+ assertThat(onInterceptedKeyEventChildTrigger).isEqualTo(3)
+ assertThat(onInterceptedKeyEventParentTrigger).isEqualTo(4)
+ assertThat(onPreKeyEventParentTrigger).isEqualTo(5)
+ assertThat(onPreKeyEventChildTrigger).isEqualTo(6)
+ assertThat(onKeyEventChildTrigger).isEqualTo(7)
+ assertThat(onKeyEventParentTrigger).isEqualTo(8)
+ }
+ }
+
+ private fun ComposeContentTestRule.setContentWithInitialFocus(content: @Composable () -> Unit) {
+ setFocusableContent {
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
+ runOnIdle {
+ initialFocus.requestFocus()
+ }
+ }
+
+ /**
+ * The [KeyEvent] is usually created by the system. This function creates an instance of
+ * [KeyEvent] that can be used in tests.
+ */
+ private fun keyEvent(keycode: Int, keyEventType: KeyEventType): KeyEvent {
+ val action = when (keyEventType) {
+ KeyDown -> ACTION_DOWN
+ KeyUp -> ACTION_UP
+ else -> error("Unknown key event type")
+ }
+ return KeyEvent(AndroidKeyEvent(0L, 0L, action, keycode, 0, 0))
+ }
+}
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 02081d9..60fe14e 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
@@ -624,22 +624,42 @@
}
}
- override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
- return focusOwner.dispatchKeyEvent(keyEvent)
- }
+ /**
+ * This function is used by the testing framework to send key events.
+ */
+ override fun sendKeyEvent(keyEvent: KeyEvent): Boolean =
+ // First dispatch the key event to mimic the event being intercepted before it is sent to
+ // the soft keyboard.
+ focusOwner.dispatchInterceptedSoftKeyboardEvent(keyEvent) ||
+ // Next, send the key event to the Soft Keyboard.
+ // TODO(b/272600716): Send the key event to the IME.
- override fun dispatchKeyEvent(event: AndroidKeyEvent) =
+ // Finally, dispatch the key event to onPreKeyEvent/onKeyEvent listeners.
+ focusOwner.dispatchKeyEvent(keyEvent)
+
+ override fun dispatchKeyEvent(event: AndroidKeyEvent): Boolean {
if (isFocused) {
// Focus lies within the Compose hierarchy, so we dispatch the key event to the
// appropriate place.
_windowInfo.keyboardModifiers = PointerKeyboardModifiers(event.metaState)
- sendKeyEvent(KeyEvent(event))
+ // If the event is not consumed, use the default implementation.
+ return focusOwner.dispatchKeyEvent(KeyEvent(event)) || super.dispatchKeyEvent(event)
} else {
- // This Owner has a focused child view, which is a view interop use case,
+ // This Owner has a focused child view, which is a view interoperability use case,
// so we use the default ViewGroup behavior which will route tke key event to the
// focused view.
- super.dispatchKeyEvent(event)
+ return super.dispatchKeyEvent(event)
}
+ }
+
+ override fun dispatchKeyEventPreIme(event: AndroidKeyEvent): Boolean {
+ return (isFocused && focusOwner.dispatchInterceptedSoftKeyboardEvent(KeyEvent(event))) ||
+ // If this view is not focused, and it received a key event, it means this is a view
+ // interoperability use case and we need to route the event to the embedded child view.
+ // Also, if this event wasn't consumed by the compose hierarchy, we need to send it back
+ // to the parent view. Both these cases are handles by the default view implementation.
+ super.dispatchKeyEventPreIme(event)
+ }
override fun onAttach(node: LayoutNode) {
}
@@ -1378,9 +1398,8 @@
eventTime: Long,
forceHover: Boolean = true
) {
- val oldAction = motionEvent.actionMasked
// don't send any events for pointers that are "up" unless they support hover
- val upIndex = when (oldAction) {
+ val upIndex = when (motionEvent.actionMasked) {
ACTION_UP -> if (action == ACTION_HOVER_ENTER || action == ACTION_HOVER_EXIT) -1 else 0
ACTION_POINTER_UP -> motionEvent.actionIndex
else -> -1
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 773cc2e..ee20288 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -52,7 +52,6 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.HitTestResult
import androidx.compose.ui.node.LayoutNode
@@ -83,6 +82,7 @@
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.platform.URLSpanCache
import androidx.compose.ui.text.platform.toAccessibilitySpannableString
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
@@ -190,6 +190,7 @@
return null
}
+@OptIn(InternalTextApi::class)
internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidComposeView) :
AccessibilityDelegateCompat() {
companion object {
@@ -393,6 +394,8 @@
private val EXTRA_DATA_TEST_TRAVERSALAFTER_VAL =
"android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALAFTER_VAL"
+ private val urlSpanCache = URLSpanCache()
+
/**
* A snapshot of the semantics node. The children here is fixed and are taken from the time
* this node is constructed. While a SemanticsNode always contains the up-to-date children.
@@ -1290,7 +1293,6 @@
}
}
- @OptIn(InternalTextApi::class)
private fun setText(
node: SemanticsNode,
info: AccessibilityNodeInfoCompat,
@@ -1298,13 +1300,21 @@
val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
val editableTextToAssign = trimToSize(
node.unmergedConfig.getTextForTextField()
- ?.toAccessibilitySpannableString(density = view.density, fontFamilyResolver),
+ ?.toAccessibilitySpannableString(
+ density = view.density,
+ fontFamilyResolver,
+ urlSpanCache
+ ),
ParcelSafeTextLength
)
val textToAssign = trimToSize(
node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
- ?.toAccessibilitySpannableString(density = view.density, fontFamilyResolver),
+ ?.toAccessibilitySpannableString(
+ density = view.density,
+ fontFamilyResolver,
+ urlSpanCache
+ ),
ParcelSafeTextLength
)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index 9566714..04671af 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.KeyEvent
@@ -80,6 +79,11 @@
fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean
/**
+ * Dispatches an intercepted soft keyboard key event through the compose hierarchy.
+ */
+ fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean
+
+ /**
* Dispatches a rotary scroll event through the compose hierarchy.
*/
fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean
@@ -87,18 +91,15 @@
/**
* Schedule a FocusTarget node to be invalidated after onApplyChanges.
*/
- @OptIn(ExperimentalComposeUiApi::class)
fun scheduleInvalidation(node: FocusTargetModifierNode)
/**
* Schedule a FocusEvent node to be invalidated after onApplyChanges.
*/
- @OptIn(ExperimentalComposeUiApi::class)
fun scheduleInvalidation(node: FocusEventModifierNode)
/**
* Schedule a FocusProperties node to be invalidated after onApplyChanges.
*/
- @OptIn(ExperimentalComposeUiApi::class)
fun scheduleInvalidation(node: FocusPropertiesModifierNode)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 7a08381..638594b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -192,14 +192,25 @@
onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
onVisit = { if (it.onKeyEvent(keyEvent)) return true }
)
+ return false
+ }
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean {
+ val focusedSoftKeyboardInterceptionNode = rootFocusNode.findActiveFocusNode()
+ ?.nearestAncestor(Nodes.SoftKeyboardKeyInput)
+
+ focusedSoftKeyboardInterceptionNode?.traverseAncestors(
+ type = Nodes.SoftKeyboardKeyInput,
+ onPreVisit = { if (it.onPreInterceptKeyBeforeSoftKeyboard(keyEvent)) return true },
+ onVisit = { if (it.onInterceptKeyBeforeSoftKeyboard(keyEvent)) return true }
+ )
return false
}
/**
* Dispatches a rotary scroll event through the compose hierarchy.
*/
- @OptIn(ExperimentalComposeUiApi::class)
override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
?.nearestAncestor(Nodes.RotaryInput)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index 4d620d4..b823385 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.input.key
import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -30,7 +32,7 @@
*/
fun Modifier.onKeyEvent(
onKeyEvent: (KeyEvent) -> Boolean
-): Modifier = this then OnKeyEventElement(onKeyEvent)
+): Modifier = this then KeyInputElement(onKeyEvent = onKeyEvent, onPreKeyEvent = null)
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -46,4 +48,35 @@
*/
fun Modifier.onPreviewKeyEvent(
onPreviewKeyEvent: (KeyEvent) -> Boolean
-): Modifier = this then OnPreviewKeyEvent(onPreviewKeyEvent)
\ No newline at end of file
+): Modifier = this then KeyInputElement(onKeyEvent = null, onPreKeyEvent = onPreviewKeyEvent)
+
+private data class KeyInputElement(
+ val onKeyEvent: ((KeyEvent) -> Boolean)?,
+ val onPreKeyEvent: ((KeyEvent) -> Boolean)?
+) : ModifierNodeElement<KeyInputModifierNodeImpl>() {
+ override fun create() = KeyInputModifierNodeImpl(onKeyEvent, onPreKeyEvent)
+
+ override fun update(node: KeyInputModifierNodeImpl) = node.apply {
+ onEvent = onKeyEvent
+ onPreEvent = onPreKeyEvent
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ onKeyEvent?.let {
+ name = "onKeyEvent"
+ properties["onKeyEvent"] = it
+ }
+ onPreKeyEvent?.let {
+ name = "onPreviewKeyEvent"
+ properties["onPreviewKeyEvent"] = it
+ }
+ }
+}
+
+private class KeyInputModifierNodeImpl(
+ var onEvent: ((KeyEvent) -> Boolean)?,
+ var onPreEvent: ((KeyEvent) -> Boolean)?
+) : KeyInputModifierNode, Modifier.Node() {
+ override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
+ override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
index 8521dec..550c5c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
@@ -18,8 +18,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
/**
* Implement this interface to create a [Modifier.Node] that can intercept hardware Key events.
@@ -47,48 +45,3 @@
*/
fun onPreKeyEvent(event: KeyEvent): Boolean
}
-
-internal data class OnKeyEventElement(
- val onKeyEvent: (KeyEvent) -> Boolean
-) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
- override fun create() = KeyInputInputModifierNodeImpl(
- onEvent = onKeyEvent,
- onPreEvent = null
- )
-
- override fun update(node: KeyInputInputModifierNodeImpl) = node.apply {
- onEvent = onKeyEvent
- onPreEvent = null
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onKeyEvent"
- properties["onKeyEvent"] = onKeyEvent
- }
-}
-
-internal data class OnPreviewKeyEvent(
- val onPreviewKeyEvent: (KeyEvent) -> Boolean
-) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
- override fun create(): KeyInputInputModifierNodeImpl {
- return KeyInputInputModifierNodeImpl(onEvent = null, onPreEvent = onPreviewKeyEvent)
- }
-
- override fun update(node: KeyInputInputModifierNodeImpl) = node.apply {
- onPreEvent = onPreviewKeyEvent
- onEvent = null
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onPreviewKeyEvent"
- properties["onPreviewKeyEvent"] = onPreviewKeyEvent
- }
-}
-
-internal class KeyInputInputModifierNodeImpl(
- var onEvent: ((KeyEvent) -> Boolean)?,
- var onPreEvent: ((KeyEvent) -> Boolean)?
-) : KeyInputModifierNode, Modifier.Node() {
- override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
- override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt
new file mode 100644
index 0000000..31eb488
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.key
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatableNode
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events
+ * before they are sent to the software keyboard.
+ *
+ * The event is routed to the focused item. Before reaching the focused item,
+ * [onPreInterceptKeyBeforeSoftKeyboard] is called for parents of the focused item.
+ * If the parents don't consume the event, [onPreSoftwareKeyboardKeyEvent]() is
+ * called for the focused item. If the event is still not consumed,
+ * [onInterceptKeyBeforeSoftKeyboard] is called on the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface SoftKeyboardInterceptionModifierNode : DelegatableNode {
+ /**
+ * This function is called when a [KeyEvent] is received by this node during the upward
+ * pass. While implementing this callback, return true to stop propagation of this event.
+ * If you return false, the key event will be sent to this
+ * [SoftKeyboardInterceptionModifierNode]'s parent.
+ */
+ fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean
+
+ /**
+ * This function is called when a [KeyEvent] is received by this node during the
+ * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+ * Return true to stop propagation of this event. If you return false, the event will be sent
+ * to this [SoftKeyboardInterceptionModifierNode]'s child. If none of the children consume
+ * the event, it will be sent back up to the root using the [onPreInterceptKeyBeforeSoftKeyboard]
+ * function.
+ */
+ fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt
new file mode 100644
index 0000000..88fc93a
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.key
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events before they are sent to the software keyboard.
+ *
+ * @param onInterceptKeyBeforeSoftKeyboard This callback is invoked when the user interacts with
+ * the hardware keyboard. While implementing this callback, return true to stop propagation of this
+ * event. If you return false, the key event will be sent to this
+ * [SoftKeyboardInterceptionModifierNode]'s parent, and ultimately to the software keyboard.
+ *
+ * @sample androidx.compose.ui.samples.KeyEventSample
+ */
+@ExperimentalComposeUiApi
+fun Modifier.onInterceptKeyBeforeSoftKeyboard(
+ onInterceptKeyBeforeSoftKeyboard: (KeyEvent) -> Boolean
+): Modifier = this then SoftKeyboardInterceptionElement(
+ onKeyEvent = onInterceptKeyBeforeSoftKeyboard,
+ onPreKeyEvent = null
+)
+
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events before they are sent to the software keyboard. This
+ * modifier is similar to [onInterceptKeyBeforeSoftKeyboard], but allows a parent composable to
+ * intercept the hardware key event before any child.
+ *
+ * @param onPreInterceptKeyBeforeSoftKeyboard This callback is invoked when the user interacts
+ * with the hardware keyboard. It gives ancestors of a focused component the chance to intercept a
+ * [KeyEvent]. Return true to stop propagation of this event. If you return false, the key event
+ * will be sent to this [SoftKeyboardInterceptionModifierNode]'s child. If none of the children
+ * consume the event, it will be sent back up to the root [KeyInputModifierNode] using the
+ * onKeyEvent callback, and ultimately to the software keyboard.
+ *
+ * @sample androidx.compose.ui.samples.KeyEventSample
+ */
+@ExperimentalComposeUiApi
+fun Modifier.onPreInterceptKeyBeforeSoftKeyboard(
+ onPreInterceptKeyBeforeSoftKeyboard: (KeyEvent) -> Boolean,
+): Modifier = this then SoftKeyboardInterceptionElement(
+ onKeyEvent = null,
+ onPreKeyEvent = onPreInterceptKeyBeforeSoftKeyboard
+)
+
+private data class SoftKeyboardInterceptionElement(
+ val onKeyEvent: ((KeyEvent) -> Boolean)?,
+ val onPreKeyEvent: ((KeyEvent) -> Boolean)?
+) : ModifierNodeElement<InterceptedKeyInputModifierNodeImpl>() {
+ override fun create() = InterceptedKeyInputModifierNodeImpl(
+ onEvent = onKeyEvent,
+ onPreEvent = onPreKeyEvent
+ )
+
+ override fun update(node: InterceptedKeyInputModifierNodeImpl) = node.apply {
+ onEvent = onKeyEvent
+ onPreEvent = onPreKeyEvent
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ onKeyEvent?.let {
+ name = "onKeyToSoftKeyboardInterceptedEvent"
+ properties["onKeyToSoftKeyboardInterceptedEvent"] = it
+ }
+ onPreKeyEvent?.let {
+ name = "onPreKeyToSoftKeyboardInterceptedEvent"
+ properties["onPreKeyToSoftKeyboardInterceptedEvent"] = it
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private class InterceptedKeyInputModifierNodeImpl(
+ var onEvent: ((KeyEvent) -> Boolean)?,
+ var onPreEvent: ((KeyEvent) -> Boolean)?
+) : SoftKeyboardInterceptionModifierNode, Modifier.Node() {
+ override fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean =
+ onEvent?.invoke(event) ?: false
+ override fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean =
+ onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
index 076a965..6b2351c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.input.rotary
import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -39,7 +41,10 @@
*/
fun Modifier.onRotaryScrollEvent(
onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = this then OnRotaryScrollEventElement(onRotaryScrollEvent)
+): Modifier = this then OnRotaryScrollEventElement(
+ onRotaryScrollEvent = onRotaryScrollEvent,
+ onPreRotaryScrollEvent = null
+)
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -64,4 +69,43 @@
*/
fun Modifier.onPreRotaryScrollEvent(
onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = this then OnPreRotaryScrollEventElement(onPreRotaryScrollEvent)
+): Modifier = this then OnRotaryScrollEventElement(
+ onRotaryScrollEvent = null,
+ onPreRotaryScrollEvent = onPreRotaryScrollEvent
+)
+
+private data class OnRotaryScrollEventElement(
+ val onRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?,
+ val onPreRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?
+) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
+ override fun create() = RotaryInputModifierNodeImpl(
+ onEvent = onRotaryScrollEvent,
+ onPreEvent = onPreRotaryScrollEvent
+ )
+
+ override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
+ onEvent = onRotaryScrollEvent
+ onPreEvent = onPreRotaryScrollEvent
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ onRotaryScrollEvent?.let {
+ name = "onRotaryScrollEvent"
+ properties["onRotaryScrollEvent"] = it
+ }
+ onPreRotaryScrollEvent?.let {
+ name = "onPreRotaryScrollEvent"
+ properties["onPreRotaryScrollEvent"] = it
+ }
+ }
+}
+
+private class RotaryInputModifierNodeImpl(
+ var onEvent: ((RotaryScrollEvent) -> Boolean)?,
+ var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
+) : RotaryInputModifierNode, Modifier.Node() {
+ override fun onRotaryScrollEvent(event: RotaryScrollEvent) =
+ onEvent?.invoke(event) ?: false
+ override fun onPreRotaryScrollEvent(event: RotaryScrollEvent) =
+ onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
index 05f8d64..d59566c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
@@ -16,11 +16,8 @@
package androidx.compose.ui.input.rotary
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
/**
* Implement this interface to create a [Modifier.Node] that can intercept rotary scroll events.
@@ -30,7 +27,6 @@
* consume the event, [onPreRotaryScrollEvent]() is called for the focused item. If the event is
* still not consumed, [onRotaryScrollEvent]() is called on the focused item's parents.
*/
-@ExperimentalComposeUiApi
interface RotaryInputModifierNode : DelegatableNode {
/**
* This function is called when a [RotaryScrollEvent] is received by this node during the upward
@@ -48,56 +44,3 @@
*/
fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean
}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal data class OnRotaryScrollEventElement(
- val onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
- override fun create() = RotaryInputModifierNodeImpl(
- onEvent = onRotaryScrollEvent,
- onPreEvent = null
- )
-
- override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
- onEvent = onRotaryScrollEvent
- onPreEvent = null
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onRotaryScrollEvent"
- properties["onRotaryScrollEvent"] = onRotaryScrollEvent
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal data class OnPreRotaryScrollEventElement(
- val onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
- override fun create() = RotaryInputModifierNodeImpl(
- onEvent = null,
- onPreEvent = onPreRotaryScrollEvent
- )
-
- override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
- onPreEvent = onPreRotaryScrollEvent
- onEvent = null
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "onPreRotaryScrollEvent"
- properties["onPreRotaryScrollEvent"] = onPreRotaryScrollEvent
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class RotaryInputModifierNodeImpl(
- var onEvent: ((RotaryScrollEvent) -> Boolean)?,
- var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
-) : RotaryInputModifierNode, Modifier.Node() {
- override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
- return onEvent?.invoke(event) ?: false
- }
- override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
- return onPreEvent?.invoke(event) ?: false
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index b2e69f4..51e1fb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -106,7 +106,7 @@
measure(layoutNode.childMeasurables, constraints)
}
onMeasured()
- return this
+ this
}
override fun minIntrinsicWidth(height: Int) =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index cceff4d..89da2e1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -281,12 +281,10 @@
protected inline fun performingMeasure(
constraints: Constraints,
- block: () -> Placeable
+ crossinline block: () -> Placeable
): Placeable {
measurementConstraints = constraints
- val result = block()
- layer?.resize(measuredSize)
- return result
+ return block()
}
fun onMeasured() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index b451d40..2d857bd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.focus.FocusPropertiesModifierNode
import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.key.SoftKeyboardInterceptionModifierNode
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.pointer.SuspendPointerInputModifierNode
import androidx.compose.ui.input.rotary.RotaryInputModifierNode
@@ -55,7 +56,6 @@
// its own measureNode if the measureNode happens to implement LayoutAware. If the measureNode
// implements any other node interfaces, such as draw, those should be visited by the coordinator
// below them.
-@OptIn(ExperimentalComposeUiApi::class)
internal val NodeKind<*>.includeSelfInTraversal: Boolean
get() = mask and Nodes.LayoutAware.mask != 0
@@ -99,10 +99,12 @@
get() = NodeKind<CompositionLocalConsumerModifierNode>(0b1 shl 15)
@JvmStatic
inline val SuspendPointerInput get() = NodeKind<SuspendPointerInputModifierNode>(0b1 shl 16)
+ @JvmStatic
+ inline val SoftKeyboardKeyInput
+ get() = NodeKind<SoftKeyboardInterceptionModifierNode>(0b1 shl 17)
// ...
}
-@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(element: Modifier.Element): Int {
var mask = Nodes.Any.mask
if (element is LayoutModifier) {
@@ -195,7 +197,9 @@
if (node is SuspendPointerInputModifierNode) {
mask = mask or SuspendPointerInput
}
-
+ if (node is SoftKeyboardInterceptionModifierNode) {
+ mask = mask or Nodes.SoftKeyboardKeyInput
+ }
return mask
}
@@ -203,16 +207,12 @@
private const val Inserted = 1
private const val Removed = 2
-@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)
-@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)
-@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
-@OptIn(ExperimentalComposeUiApi::class)
private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
check(node.isAttached)
if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
diff --git a/core/core-ktx/build.gradle b/core/core-ktx/build.gradle
index 21bb1d2..b8c9576 100644
--- a/core/core-ktx/build.gradle
+++ b/core/core-ktx/build.gradle
@@ -7,7 +7,7 @@
}
dependencies {
- // Atomic group
+ // Atomically versioned.
constraints {
implementation(project(":core:core"))
}
diff --git a/core/core/build.gradle b/core/core/build.gradle
index b9743ef..ecf22c1 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -8,7 +8,7 @@
}
dependencies {
- // Atomic group
+ // Atomically versioned.
constraints {
implementation(project(":core:core-ktx"))
}
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 9c1379f..95d0b38 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -36,7 +36,7 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.espressoCore)
androidTestImplementation('com.google.android.gms:play-services-base:18.0.1')
- androidTestImplementation('com.google.android.gms:play-services-nearby:18.3.0', {
+ androidTestImplementation('com.google.android.gms:play-services-nearby:18.5.0', {
exclude group: "androidx.core"
})
androidTestImplementation(libs.multidex)
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
index 2862833..522e730 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
@@ -46,7 +46,7 @@
private var startedRanging = false
companion object {
val rangingPosition = RangingPosition(
- RangingMeasurement(1, 1.0F), null, null, 20)
+ RangingMeasurement(1, 1.0F), null, null, 20, -50)
}
override fun getApiKey(): ApiKey<zze> {
TODO("Not yet implemented")
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
index a46f50e..6c2a4fb 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
@@ -63,7 +63,6 @@
.setSessionId(defaultRangingParameters.sessionId)
.setUwbConfigId(configId)
.setRangingUpdateRate(updateRate)
- .setSessionKeyInfo(defaultRangingParameters.sessionKeyInfo)
parametersBuilder.addPeerDevice(UwbDevice.createForAddress(uwbDevice.address.address))
val callback =
object : RangingSessionCallback {
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
index 74d6b6e..d4769cc 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
@@ -51,7 +51,8 @@
val localAddress = com.google.android.gms.nearby.uwb.UwbAddress(DEVICE_ADDRESS)
val rangingCapabilities =
- com.google.android.gms.nearby.uwb.RangingCapabilities(true, false, false, 200)
+ com.google.android.gms.nearby.uwb.RangingCapabilities(true, false, false, 200,
+ listOf(9), listOf(1, 2, 3), 2F)
val uwbClient = TestUwbClient(complexChannel, localAddress, rangingCapabilities, true)
return if (isController) {
TestUwbControllerSessionScope(
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 1c781ab..8a23173 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -30,7 +30,7 @@
implementation(libs.guavaAndroid)
implementation('com.google.android.gms:play-services-base:18.0.1')
implementation(libs.kotlinCoroutinesPlayServices)
- implementation('com.google.android.gms:play-services-nearby:18.3.0', {
+ implementation('com.google.android.gms:play-services-nearby:18.5.0', {
exclude group: "androidx.core"
})
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
index ce0dab8..3290b14 100644
--- a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
@@ -29,7 +29,8 @@
.setChannel(10)
.build()
val LOCAL_ADDRESS = UwbAddress(byteArrayOf(0xB0.toByte()))
- val RANGING_CAPABILITIES = RangingCapabilities(true, false, false, 200)
+ val RANGING_CAPABILITIES = RangingCapabilities(true, false, false,
+ 200, listOf(9), listOf(1, 2, 3), 2F)
val NEIGHBOR_1 = byteArrayOf(0xA1.toByte())
val NEIGHBOR_2 = byteArrayOf(0xA5.toByte())
val UWB_DEVICE = UwbDevice.createForAddress(NEIGHBOR_1)
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
index c3f90ad..db0ff5b 100644
--- a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
@@ -48,7 +48,7 @@
private var startedRanging = false
companion object {
val rangingPosition = RangingPosition(
- RangingMeasurement(1, 1.0F), null, null, 20)
+ RangingMeasurement(1, 1.0F), null, null, 20, -50)
}
override fun getApiKey(): ApiKey<zze> {
TODO("Not yet implemented")
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
index fa19f31..86c1559 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
@@ -79,7 +79,6 @@
.setSessionId(parameters.sessionId)
.setUwbConfigId(configId)
.setRangingUpdateRate(updateRate)
- .setSessionKeyInfo(parameters.sessionKeyInfo)
.setComplexChannel(
parameters.complexChannel?.let {
UwbComplexChannel.Builder()
@@ -87,6 +86,9 @@
.setPreambleIndex(it.preambleIndex)
.build()
})
+ if (parameters.sessionKeyInfo != null) {
+ parametersBuilder.setSessionKeyInfo(parameters.sessionKeyInfo)
+ }
for (peer in parameters.peerDevices) {
parametersBuilder.addPeerDevice(UwbDevice.createForAddress(peer.address.address))
}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index e099b3b..7978fe2 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -181,6 +181,7 @@
WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//Stepper/\#kotlin\.Float\#kotlin\.Function[0-9]+\[kotlin\.Float,kotlin\.Unit\]\#kotlin\.Int\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#kotlin\.ranges\.ClosedFloatingPointRange\[kotlin\.Float\]\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.BoxScope,kotlin\.Unit\]/PointingToDeclaration/
WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//Stepper/\#kotlin\.Int\#kotlin\.Function[0-9]+\[kotlin\.Int,kotlin\.Unit\]\#kotlin\.ranges\.IntProgression\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.BoxScope,kotlin\.Unit\]/PointingToDeclaration/
WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//TitleCard/\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.RowScope,kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#kotlin\.Boolean\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.RowScope,kotlin\.Unit\]\?\#androidx\.compose\.ui\.graphics\.painter\.Painter\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.ColumnScope,kotlin\.Unit\]/PointingToDeclaration/
+WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material[0-9]+//MaterialTheme/\#androidx\.wear\.compose\.material[0-9]+\.ColorScheme\#androidx\.wear\.compose\.material[0-9]+\.Typography\#androidx\.wear\.compose\.material[0-9]+\.Shapes\#kotlin\.Function[0-9]+\[kotlin\.Unit\]/PointingToDeclaration/
WARN: Unable to find what is referred to by
# > Task :docs-tip-of-tree:dackkaDocs
WARN\: Failed to resolve \`\@see SplitAttributes\.shouldExpandSecondaryContainer\`\!
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 67b8d16..3fb9fc1 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -30,24 +30,24 @@
docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
docs("androidx.autofill:autofill:1.2.0-beta01")
- docs("androidx.benchmark:benchmark-common:1.2.0-alpha11")
- docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha11")
- docs("androidx.benchmark:benchmark-macro:1.2.0-alpha11")
- docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha11")
+ docs("androidx.benchmark:benchmark-common:1.2.0-alpha12")
+ docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha12")
+ docs("androidx.benchmark:benchmark-macro:1.2.0-alpha12")
+ docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha12")
docs("androidx.biometric:biometric:1.2.0-alpha05")
docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
docs("androidx.browser:browser:1.5.0")
- docs("androidx.camera:camera-camera2:1.3.0-alpha04")
- docs("androidx.camera:camera-core:1.3.0-alpha04")
- docs("androidx.camera:camera-extensions:1.3.0-alpha04")
+ docs("androidx.camera:camera-camera2:1.3.0-alpha05")
+ docs("androidx.camera:camera-core:1.3.0-alpha05")
+ docs("androidx.camera:camera-extensions:1.3.0-alpha05")
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
- docs("androidx.camera:camera-lifecycle:1.3.0-alpha04")
- docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha04")
+ docs("androidx.camera:camera-lifecycle:1.3.0-alpha05")
+ docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha05")
docs("androidx.camera:camera-previewview:1.1.0-beta02")
- docs("androidx.camera:camera-video:1.3.0-alpha04")
- docs("androidx.camera:camera-view:1.3.0-alpha04")
- docs("androidx.camera:camera-viewfinder:1.3.0-alpha04")
+ docs("androidx.camera:camera-video:1.3.0-alpha05")
+ docs("androidx.camera:camera-view:1.3.0-alpha05")
+ docs("androidx.camera:camera-viewfinder:1.3.0-alpha05")
docs("androidx.car.app:app:1.4.0-alpha01")
docs("androidx.car.app:app-automotive:1.4.0-alpha01")
docs("androidx.car.app:app-projected:1.4.0-alpha01")
@@ -55,60 +55,60 @@
docs("androidx.cardview:cardview:1.0.0")
docs("androidx.collection:collection:1.3.0-alpha02")
docs("androidx.collection:collection-ktx:1.3.0-alpha02")
- docs("androidx.compose.animation:animation:1.4.0-rc01")
- docs("androidx.compose.animation:animation-core:1.4.0-rc01")
- docs("androidx.compose.animation:animation-graphics:1.4.0-rc01")
- samples("androidx.compose.animation:animation-samples:1.4.0-rc01")
- samples("androidx.compose.animation:animation-core-samples:1.4.0-rc01")
- samples("androidx.compose.animation:animation-graphics-samples:1.4.0-rc01")
- docs("androidx.compose.foundation:foundation:1.4.0-rc01")
- docs("androidx.compose.foundation:foundation-layout:1.4.0-rc01")
- samples("androidx.compose.foundation:foundation-layout-samples:1.4.0-rc01")
- samples("androidx.compose.foundation:foundation-samples:1.4.0-rc01")
- docs("androidx.compose.material3:material3:1.1.0-alpha08")
- samples("androidx.compose.material3:material3-samples:1.1.0-alpha08")
- docs("androidx.compose.material3:material3-window-size-class:1.1.0-alpha08")
- samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-alpha08")
- docs("androidx.compose.material:material:1.4.0-rc01")
- docs("androidx.compose.material:material-icons-core:1.4.0-rc01")
- samples("androidx.compose.material:material-icons-core-samples:1.4.0-rc01")
- docs("androidx.compose.material:material-ripple:1.4.0-rc01")
- samples("androidx.compose.material:material-samples:1.4.0-rc01")
- docs("androidx.compose.runtime:runtime:1.4.0-rc01")
- docs("androidx.compose.runtime:runtime-livedata:1.4.0-rc01")
- samples("androidx.compose.runtime:runtime-livedata-samples:1.4.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava2:1.4.0-rc01")
- samples("androidx.compose.runtime:runtime-rxjava2-samples:1.4.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava3:1.4.0-rc01")
- samples("androidx.compose.runtime:runtime-rxjava3-samples:1.4.0-rc01")
- docs("androidx.compose.runtime:runtime-saveable:1.4.0-rc01")
- samples("androidx.compose.runtime:runtime-saveable-samples:1.4.0-rc01")
- samples("androidx.compose.runtime:runtime-samples:1.4.0-rc01")
+ docs("androidx.compose.animation:animation:1.5.0-alpha01")
+ docs("androidx.compose.animation:animation-core:1.5.0-alpha01")
+ docs("androidx.compose.animation:animation-graphics:1.5.0-alpha01")
+ samples("androidx.compose.animation:animation-samples:1.5.0-alpha01")
+ samples("androidx.compose.animation:animation-core-samples:1.5.0-alpha01")
+ samples("androidx.compose.animation:animation-graphics-samples:1.5.0-alpha01")
+ docs("androidx.compose.foundation:foundation:1.5.0-alpha01")
+ docs("androidx.compose.foundation:foundation-layout:1.5.0-alpha01")
+ samples("androidx.compose.foundation:foundation-layout-samples:1.5.0-alpha01")
+ samples("androidx.compose.foundation:foundation-samples:1.5.0-alpha01")
+ docs("androidx.compose.material3:material3:1.1.0-beta01")
+ samples("androidx.compose.material3:material3-samples:1.1.0-beta01")
+ docs("androidx.compose.material3:material3-window-size-class:1.1.0-beta01")
+ samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-beta01")
+ docs("androidx.compose.material:material:1.5.0-alpha01")
+ docs("androidx.compose.material:material-icons-core:1.5.0-alpha01")
+ samples("androidx.compose.material:material-icons-core-samples:1.5.0-alpha01")
+ docs("androidx.compose.material:material-ripple:1.5.0-alpha01")
+ samples("androidx.compose.material:material-samples:1.5.0-alpha01")
+ docs("androidx.compose.runtime:runtime:1.5.0-alpha01")
+ docs("androidx.compose.runtime:runtime-livedata:1.5.0-alpha01")
+ samples("androidx.compose.runtime:runtime-livedata-samples:1.5.0-alpha01")
+ docs("androidx.compose.runtime:runtime-rxjava2:1.5.0-alpha01")
+ samples("androidx.compose.runtime:runtime-rxjava2-samples:1.5.0-alpha01")
+ docs("androidx.compose.runtime:runtime-rxjava3:1.5.0-alpha01")
+ samples("androidx.compose.runtime:runtime-rxjava3-samples:1.5.0-alpha01")
+ docs("androidx.compose.runtime:runtime-saveable:1.5.0-alpha01")
+ samples("androidx.compose.runtime:runtime-saveable-samples:1.5.0-alpha01")
+ samples("androidx.compose.runtime:runtime-samples:1.5.0-alpha01")
docs("androidx.compose.runtime:runtime-tracing:1.0.0-alpha03")
- docs("androidx.compose.ui:ui:1.4.0-rc01")
- docs("androidx.compose.ui:ui-geometry:1.4.0-rc01")
- docs("androidx.compose.ui:ui-graphics:1.4.0-rc01")
- samples("androidx.compose.ui:ui-graphics-samples:1.4.0-rc01")
- docs("androidx.compose.ui:ui-test:1.4.0-rc01")
- docs("androidx.compose.ui:ui-test-junit4:1.4.0-rc01")
- samples("androidx.compose.ui:ui-test-samples:1.4.0-rc01")
- docs("androidx.compose.ui:ui-text:1.4.0-rc01")
- docs("androidx.compose.ui:ui-text-google-fonts:1.4.0-rc01")
- samples("androidx.compose.ui:ui-text-samples:1.4.0-rc01")
- docs("androidx.compose.ui:ui-tooling:1.4.0-rc01")
- docs("androidx.compose.ui:ui-tooling-data:1.4.0-rc01")
- docs("androidx.compose.ui:ui-tooling-preview:1.4.0-rc01")
- docs("androidx.compose.ui:ui-unit:1.4.0-rc01")
- samples("androidx.compose.ui:ui-unit-samples:1.4.0-rc01")
- docs("androidx.compose.ui:ui-util:1.4.0-rc01")
- docs("androidx.compose.ui:ui-viewbinding:1.4.0-rc01")
- samples("androidx.compose.ui:ui-viewbinding-samples:1.4.0-rc01")
- samples("androidx.compose.ui:ui-samples:1.4.0-rc01")
+ docs("androidx.compose.ui:ui:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-geometry:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-graphics:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-graphics-samples:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-test:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-test-junit4:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-test-samples:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-text:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-text-google-fonts:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-text-samples:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-tooling:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-tooling-data:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-tooling-preview:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-unit:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-unit-samples:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-util:1.5.0-alpha01")
+ docs("androidx.compose.ui:ui-viewbinding:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-viewbinding-samples:1.5.0-alpha01")
+ samples("androidx.compose.ui:ui-samples:1.5.0-alpha01")
docs("androidx.concurrent:concurrent-futures:1.2.0-alpha01")
docs("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha01")
- docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha08")
- docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha08")
- docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha08")
+ docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha09")
+ docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha09")
+ docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha09")
docs("androidx.contentpager:contentpager:1.0.0")
docs("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
docs("androidx.core.uwb:uwb:1.0.0-alpha04")
@@ -140,23 +140,23 @@
docs("androidx.datastore:datastore-rxjava3:1.1.0-alpha01")
docs("androidx.documentfile:documentfile:1.1.0-alpha01")
docs("androidx.draganddrop:draganddrop:1.0.0")
- docs("androidx.drawerlayout:drawerlayout:1.2.0-rc01")
+ docs("androidx.drawerlayout:drawerlayout:1.2.0")
docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
docs("androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03")
- docs("androidx.emoji2:emoji2:1.3.0-rc01")
- docs("androidx.emoji2:emoji2-bundled:1.3.0-rc01")
+ docs("androidx.emoji2:emoji2:1.4.0-alpha01")
+ docs("androidx.emoji2:emoji2-bundled:1.4.0-alpha01")
docs("androidx.emoji2:emoji2-emojipicker:1.0.0-alpha03")
- docs("androidx.emoji2:emoji2-views:1.3.0-rc01")
- docs("androidx.emoji2:emoji2-views-helper:1.3.0-rc01")
+ docs("androidx.emoji2:emoji2-views:1.4.0-alpha01")
+ docs("androidx.emoji2:emoji2-views-helper:1.4.0-alpha01")
docs("androidx.emoji:emoji:1.2.0-alpha03")
docs("androidx.emoji:emoji-appcompat:1.2.0-alpha03")
docs("androidx.emoji:emoji-bundled:1.2.0-alpha03")
docs("androidx.enterprise:enterprise-feedback:1.1.0")
docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
docs("androidx.exifinterface:exifinterface:1.3.6")
- docs("androidx.fragment:fragment:1.6.0-alpha07")
- docs("androidx.fragment:fragment-ktx:1.6.0-alpha07")
- docs("androidx.fragment:fragment-testing:1.6.0-alpha07")
+ docs("androidx.fragment:fragment:1.6.0-alpha08")
+ docs("androidx.fragment:fragment-ktx:1.6.0-alpha08")
+ docs("androidx.fragment:fragment-testing:1.6.0-alpha08")
docs("androidx.glance:glance:1.0.0-alpha05")
docs("androidx.glance:glance-appwidget:1.0.0-alpha05")
docs("androidx.glance:glance-appwidget-preview:1.0.0-alpha05")
@@ -164,8 +164,8 @@
docs("androidx.glance:glance-preview:1.0.0-alpha05")
docs("androidx.glance:glance-wear-tiles:1.0.0-alpha05")
docs("androidx.glance:glance-wear-tiles-preview:1.0.0-alpha05")
- docs("androidx.graphics:graphics-core:1.0.0-alpha02")
- docs("androidx.gridlayout:gridlayout:1.0.0")
+ docs("androidx.graphics:graphics-core:1.0.0-alpha03")
+ docs("androidx.gridlayout:gridlayout:1.1.0-alpha01")
docs("androidx.health.connect:connect-client:1.0.0-alpha11")
samples("androidx.health.connect:connect-client-samples:1.0.0-alpha11")
docs("androidx.health:health-services-client:1.0.0-beta02")
@@ -176,7 +176,7 @@
samples("androidx.hilt:hilt-navigation-compose-samples:1.1.0-alpha01")
docs("androidx.hilt:hilt-navigation-fragment:1.0.0-beta01")
docs("androidx.hilt:hilt-work:1.0.0-beta01")
- docs("androidx.input:input-motionprediction:1.0.0-alpha02")
+ docs("androidx.input:input-motionprediction:1.0.0-beta01")
docs("androidx.interpolator:interpolator:1.0.0")
docs("androidx.javascriptengine:javascriptengine:1.0.0-alpha04")
docs("androidx.leanback:leanback:1.2.0-alpha02")
@@ -238,19 +238,19 @@
docs("androidx.mediarouter:mediarouter:1.6.0-alpha02")
docs("androidx.mediarouter:mediarouter-testing:1.6.0-alpha02")
docs("androidx.metrics:metrics-performance:1.0.0-alpha03")
- docs("androidx.navigation:navigation-common:2.6.0-alpha07")
- docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha07")
- docs("androidx.navigation:navigation-compose:2.6.0-alpha07")
- samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha07")
- docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha07")
- docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha07")
- docs("androidx.navigation:navigation-fragment:2.6.0-alpha07")
- docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha07")
- docs("androidx.navigation:navigation-runtime:2.6.0-alpha07")
- docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha07")
- docs("androidx.navigation:navigation-testing:2.6.0-alpha07")
- docs("androidx.navigation:navigation-ui:2.6.0-alpha07")
- docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha07")
+ docs("androidx.navigation:navigation-common:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-compose:2.6.0-alpha08")
+ samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-fragment:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-runtime:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-testing:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-ui:2.6.0-alpha08")
+ docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha08")
docs("androidx.paging:paging-common:3.2.0-alpha04")
docs("androidx.paging:paging-common-ktx:3.2.0-alpha04")
docs("androidx.paging:paging-compose:1.0.0-alpha18")
@@ -269,15 +269,15 @@
docs("androidx.preference:preference:1.2.0")
docs("androidx.preference:preference-ktx:1.2.0")
docs("androidx.print:print:1.1.0-beta01")
- docs("androidx.privacysandbox.ads:ads-adservices:1.0.0-beta01")
- docs("androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta01")
+ docs("androidx.privacysandbox.ads:ads-adservices:1.0.0-beta02")
+ docs("androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta02")
docs("androidx.privacysandbox.tools:tools:1.0.0-alpha03")
docs("androidx.privacysandbox.tools:tools-apigenerator:1.0.0-alpha03")
docs("androidx.privacysandbox.tools:tools-apipackager:1.0.0-alpha03")
docs("androidx.privacysandbox.tools:tools-core:1.0.0-alpha03")
- docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha01")
- docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha01")
- docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha01")
+ docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha02")
+ docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha02")
+ docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha02")
docs("androidx.profileinstaller:profileinstaller:1.3.0")
docs("androidx.recommendation:recommendation:1.0.0")
docs("androidx.recyclerview:recyclerview:1.3.0")
@@ -285,18 +285,18 @@
docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
docs("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
docs("androidx.resourceinspection:resourceinspection-processor:1.0.1")
- docs("androidx.room:room-common:2.5.0")
- docs("androidx.room:room-guava:2.5.0")
- docs("androidx.room:room-ktx:2.5.0")
- docs("androidx.room:room-migration:2.5.0")
- docs("androidx.room:room-paging:2.5.0")
- docs("androidx.room:room-paging-guava:2.5.0")
- docs("androidx.room:room-paging-rxjava2:2.5.0")
- docs("androidx.room:room-paging-rxjava3:2.5.0")
- docs("androidx.room:room-runtime:2.5.0")
- docs("androidx.room:room-rxjava2:2.5.0")
- docs("androidx.room:room-rxjava3:2.5.0")
- docs("androidx.room:room-testing:2.5.0")
+ docs("androidx.room:room-common:2.6.0-alpha01")
+ docs("androidx.room:room-guava:2.6.0-alpha01")
+ docs("androidx.room:room-ktx:2.6.0-alpha01")
+ docs("androidx.room:room-migration:2.6.0-alpha01")
+ docs("androidx.room:room-paging:2.6.0-alpha01")
+ docs("androidx.room:room-paging-guava:2.6.0-alpha01")
+ docs("androidx.room:room-paging-rxjava2:2.6.0-alpha01")
+ docs("androidx.room:room-paging-rxjava3:2.6.0-alpha01")
+ docs("androidx.room:room-runtime:2.6.0-alpha01")
+ docs("androidx.room:room-rxjava2:2.6.0-alpha01")
+ docs("androidx.room:room-rxjava3:2.6.0-alpha01")
+ docs("androidx.room:room-testing:2.6.0-alpha01")
docs("androidx.savedstate:savedstate:1.2.1")
docs("androidx.savedstate:savedstate-ktx:1.2.1")
docs("androidx.security:security-app-authenticator:1.0.0-alpha02")
@@ -310,9 +310,9 @@
docs("androidx.slice:slice-core:1.1.0-alpha02")
docs("androidx.slice:slice-view:1.1.0-alpha02")
docs("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
- docs("androidx.sqlite:sqlite:2.3.0")
- docs("androidx.sqlite:sqlite-framework:2.3.0")
- docs("androidx.sqlite:sqlite-ktx:2.3.0")
+ docs("androidx.sqlite:sqlite:2.4.0-alpha01")
+ docs("androidx.sqlite:sqlite-framework:2.4.0-alpha01")
+ docs("androidx.sqlite:sqlite-ktx:2.4.0-alpha01")
docs("androidx.startup:startup-runtime:1.2.0-alpha02")
docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
docs("androidx.test:core:1.5.0")
@@ -336,15 +336,15 @@
docs("androidx.test.services:storage:1.4.2")
docs("androidx.test.uiautomator:uiautomator:2.3.0-alpha02")
docs("androidx.textclassifier:textclassifier:1.0.0-alpha04")
- docs("androidx.tracing:tracing:1.2.0-beta01")
- docs("androidx.tracing:tracing-ktx:1.2.0-beta01")
- docs("androidx.tracing:tracing-perfetto:1.0.0-alpha12")
- docs("androidx.tracing:tracing-perfetto-common:1.0.0-alpha12")
+ docs("androidx.tracing:tracing:1.2.0-beta02")
+ docs("androidx.tracing:tracing-ktx:1.2.0-beta02")
+ docs("androidx.tracing:tracing-perfetto:1.0.0-alpha13")
+ docs("androidx.tracing:tracing-perfetto-common:1.0.0-alpha13")
docs("androidx.transition:transition:1.4.1")
docs("androidx.transition:transition-ktx:1.4.1")
- docs("androidx.tv:tv-foundation:1.0.0-alpha04")
- docs("androidx.tv:tv-material:1.0.0-alpha04")
- samples("androidx.tv:tv-samples:1.0.0-alpha04")
+ docs("androidx.tv:tv-foundation:1.0.0-alpha05")
+ docs("androidx.tv:tv-material:1.0.0-alpha05")
+ samples("androidx.tv:tv-samples:1.0.0-alpha05")
docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
docs("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -352,25 +352,27 @@
docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
docs("androidx.viewpager2:viewpager2:1.1.0-beta01")
docs("androidx.viewpager:viewpager:1.1.0-alpha01")
- docs("androidx.wear.compose:compose-foundation:1.2.0-alpha06")
- samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha06")
- docs("androidx.wear.compose:compose-material:1.2.0-alpha06")
- docs("androidx.wear.compose:compose-material-core:1.2.0-alpha06")
- samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha06")
+ docs("androidx.wear.compose:compose-foundation:1.2.0-alpha07")
+ samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha07")
+ docs("androidx.wear.compose:compose-material:1.2.0-alpha07")
+ docs("androidx.wear.compose:compose-material-core:1.2.0-alpha07")
+ samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha07")
docs("androidx.wear.compose:compose-material3:1.0.0-alpha01")
- samples("androidx.wear.compose:compose-material3-samples:1.2.0-alpha06")
- docs("androidx.wear.compose:compose-navigation:1.2.0-alpha06")
- samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha06")
- docs("androidx.wear.protolayout:protolayout:1.0.0-alpha05")
- docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha05")
- docs("androidx.wear.protolayout:protolayout-material:1.0.0-alpha05")
- docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha05")
- docs("androidx.wear.protolayout:protolayout-renderer:1.0.0-alpha05")
- docs("androidx.wear.tiles:tiles:1.2.0-alpha01")
- docs("androidx.wear.tiles:tiles-material:1.2.0-alpha01")
- docs("androidx.wear.tiles:tiles-proto:1.2.0-alpha01")
- docs("androidx.wear.tiles:tiles-renderer:1.2.0-alpha01")
- docs("androidx.wear.tiles:tiles-testing:1.2.0-alpha01")
+ samples("androidx.wear.compose:compose-material3-samples:1.2.0-alpha07")
+ docs("androidx.wear.compose:compose-navigation:1.2.0-alpha07")
+ samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha07")
+ docs("androidx.wear.compose:compose-ui-tooling:1.2.0-alpha07")
+ docs("androidx.wear.protolayout:protolayout:1.0.0-alpha06")
+ docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha06")
+ docs("androidx.wear.protolayout:protolayout-material:1.0.0-alpha06")
+ docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha06")
+ docs("androidx.wear.protolayout:protolayout-renderer:1.0.0-alpha06")
+ docs("androidx.wear.tiles:tiles:1.2.0-alpha02")
+ docs("androidx.wear.tiles:tiles-material:1.2.0-alpha02")
+ docs("androidx.wear.tiles:tiles-proto:1.2.0-alpha02")
+ docs("androidx.wear.tiles:tiles-renderer:1.2.0-alpha02")
+ docs("androidx.wear.tiles:tiles-testing:1.2.0-alpha02")
+ docs("androidx.wear.tiles:tiles-tooling:1.2.0-alpha02")
docs("androidx.wear.watchface:watchface:1.2.0-alpha07")
docs("androidx.wear.watchface:watchface-client:1.2.0-alpha07")
docs("androidx.wear.watchface:watchface-client-guava:1.2.0-alpha07")
@@ -395,7 +397,7 @@
docs("androidx.wear:wear-input:1.2.0-alpha02")
samples("androidx.wear:wear-input-samples:1.2.0-alpha01")
docs("androidx.wear:wear-input-testing:1.2.0-alpha02")
- docs("androidx.webkit:webkit:1.7.0-alpha03")
+ docs("androidx.webkit:webkit:1.7.0-beta01")
docs("androidx.window.extensions.core:core:1.0.0-beta01")
docs("androidx.window:window:1.1.0-beta01")
stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
@@ -406,11 +408,11 @@
docs("androidx.window:window-rxjava3:1.1.0-beta01")
samples("androidx.window:window-samples:1.1.0-beta01")
docs("androidx.window:window-testing:1.1.0-beta01")
- docs("androidx.work:work-gcm:2.8.0")
- docs("androidx.work:work-multiprocess:2.8.0")
- docs("androidx.work:work-runtime:2.8.0")
- docs("androidx.work:work-runtime-ktx:2.8.0")
- docs("androidx.work:work-rxjava2:2.8.0")
- docs("androidx.work:work-rxjava3:2.8.0")
- docs("androidx.work:work-testing:2.8.0")
+ docs("androidx.work:work-gcm:2.8.1")
+ docs("androidx.work:work-multiprocess:2.8.1")
+ docs("androidx.work:work-runtime:2.8.1")
+ docs("androidx.work:work-runtime-ktx:2.8.1")
+ docs("androidx.work:work-rxjava2:2.8.1")
+ docs("androidx.work:work-rxjava3:2.8.1")
+ docs("androidx.work:work-testing:2.8.1")
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
index 5e1993d..4dac647 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
@@ -58,10 +58,10 @@
R.id.emoji_picker_header_icon
).apply {
setImageDrawable(context.getDrawable(emojiPickerItems.getHeaderIconId(i)))
- setOnClickListener { onHeaderIconClicked(i) }
isSelected = isItemSelected
contentDescription = emojiPickerItems.getHeaderIconDescription(i)
}
+ viewHolder.itemView.setOnClickListener { onHeaderIconClicked(i) }
if (isItemSelected) {
headerIcon.post {
headerIcon.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
index 2989592003..8a75049 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
@@ -26,6 +26,7 @@
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.text.style.TextAppearanceSpan
+import android.text.style.TypefaceSpan
import android.text.style.UnderlineSpan
import android.util.Log
import android.util.TypedValue
@@ -115,6 +116,9 @@
}
spans.add(TextAppearanceSpan(translationContext.context, textAppearance))
}
+ style.fontFamily?.let { family ->
+ spans.add(TypefaceSpan(family.family))
+ }
style.textAlign?.let { align ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
TextTranslatorApi31Impl.setTextViewGravity(
@@ -140,6 +144,7 @@
setTextColor(resId, colorProvider.getColor(translationContext.context).toArgb())
}
}
+
is DayNightColorProvider -> {
if (Build.VERSION.SDK_INT >= 31) {
setTextViewTextColor(
@@ -151,6 +156,7 @@
setTextColor(resId, colorProvider.getColor(translationContext.context).toArgb())
}
}
+
else -> Log.w(GlanceAppWidgetTag, "Unexpected text color: $colorProvider")
}
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
index b877f04..1b4731c 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
@@ -26,6 +26,7 @@
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.text.style.TextAppearanceSpan
+import android.text.style.TypefaceSpan
import android.text.style.UnderlineSpan
import android.view.Gravity
import android.widget.LinearLayout
@@ -47,6 +48,7 @@
import androidx.glance.layout.fillMaxWidth
import androidx.glance.semantics.contentDescription
import androidx.glance.semantics.semantics
+import androidx.glance.text.FontFamily
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
@@ -56,6 +58,7 @@
import androidx.glance.unit.ColorProvider
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -64,7 +67,6 @@
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
-import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -119,6 +121,96 @@
}
@Test
+ fun canTranslateText_withMonoFontFamily() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ "test",
+ style = TextStyle(fontFamily = FontFamily.Monospace),
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ val content = view.text as SpannedString
+ assertThat(content.toString()).isEqualTo("test")
+ content.checkSingleSpan<TypefaceSpan> { span ->
+ assertThat(span.family).isEqualTo("monospace")
+ }
+ }
+
+ @Test
+ fun canTranslateText_withMonoSerifFamily() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ "test",
+ style = TextStyle(fontFamily = FontFamily.Serif),
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ val content = view.text as SpannedString
+ assertThat(content.toString()).isEqualTo("test")
+ content.checkSingleSpan<TypefaceSpan> { span ->
+ assertThat(span.family).isEqualTo("serif")
+ }
+ }
+
+ @Test
+ fun canTranslateText_withSansFontFamily() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ "test",
+ style = TextStyle(fontFamily = FontFamily.SansSerif),
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ val content = view.text as SpannedString
+ assertThat(content.toString()).isEqualTo("test")
+ content.checkSingleSpan<TypefaceSpan> { span ->
+ assertThat(span.family).isEqualTo("sans-serif")
+ }
+ }
+
+ @Test
+ fun canTranslateText_withCursiveFontFamily() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ "test",
+ style = TextStyle(fontFamily = FontFamily.Cursive),
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ val content = view.text as SpannedString
+ assertThat(content.toString()).isEqualTo("test")
+ content.checkSingleSpan<TypefaceSpan> { span ->
+ assertThat(span.family).isEqualTo("cursive")
+ }
+ }
+
+ @Test
+ fun canTranslateText_withCustomFontFamily() = fakeCoroutineScope.runTest {
+ val rv = context.runAndTranslate {
+ Text(
+ "test",
+ style = TextStyle(fontFamily = FontFamily("casual")),
+ )
+ }
+ val view = context.applyRemoteViews(rv)
+
+ assertIs<TextView>(view)
+ val content = view.text as SpannedString
+ assertThat(content.toString()).isEqualTo("test")
+ content.checkSingleSpan<TypefaceSpan> { span ->
+ assertThat(span.family).isEqualTo("casual")
+ }
+ }
+
+ @Test
fun canTranslateText_withStyleStrikeThrough() = fakeCoroutineScope.runTest {
val rv = context.runAndTranslate {
Text("test", style = TextStyle(textDecoration = TextDecoration.LineThrough))
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -666,6 +666,24 @@
package androidx.glance.text {
+ public final class FontFamily {
+ ctor public FontFamily(String family);
+ method public String getFamily();
+ property public final String family;
+ field public static final androidx.glance.text.FontFamily.Companion Companion;
+ }
+
+ public static final class FontFamily.Companion {
+ method public androidx.glance.text.FontFamily getCursive();
+ method public androidx.glance.text.FontFamily getMonospace();
+ method public androidx.glance.text.FontFamily getSansSerif();
+ method public androidx.glance.text.FontFamily getSerif();
+ property public final androidx.glance.text.FontFamily Cursive;
+ property public final androidx.glance.text.FontFamily Monospace;
+ property public final androidx.glance.text.FontFamily SansSerif;
+ property public final androidx.glance.text.FontFamily Serif;
+ }
+
@kotlin.jvm.JvmInline public final value class FontStyle {
field public static final androidx.glance.text.FontStyle.Companion Companion;
}
@@ -739,15 +757,17 @@
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
method public androidx.glance.unit.ColorProvider getColor();
+ method public androidx.glance.text.FontFamily? getFontFamily();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
property public final androidx.glance.unit.ColorProvider color;
+ property public final androidx.glance.text.FontFamily? fontFamily;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -666,6 +666,24 @@
package androidx.glance.text {
+ public final class FontFamily {
+ ctor public FontFamily(String family);
+ method public String getFamily();
+ property public final String family;
+ field public static final androidx.glance.text.FontFamily.Companion Companion;
+ }
+
+ public static final class FontFamily.Companion {
+ method public androidx.glance.text.FontFamily getCursive();
+ method public androidx.glance.text.FontFamily getMonospace();
+ method public androidx.glance.text.FontFamily getSansSerif();
+ method public androidx.glance.text.FontFamily getSerif();
+ property public final androidx.glance.text.FontFamily Cursive;
+ property public final androidx.glance.text.FontFamily Monospace;
+ property public final androidx.glance.text.FontFamily SansSerif;
+ property public final androidx.glance.text.FontFamily Serif;
+ }
+
@kotlin.jvm.JvmInline public final value class FontStyle {
field public static final androidx.glance.text.FontStyle.Companion Companion;
}
@@ -739,15 +757,17 @@
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
method public androidx.glance.unit.ColorProvider getColor();
+ method public androidx.glance.text.FontFamily? getFontFamily();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
property public final androidx.glance.unit.ColorProvider color;
+ property public final androidx.glance.text.FontFamily? fontFamily;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -666,6 +666,24 @@
package androidx.glance.text {
+ public final class FontFamily {
+ ctor public FontFamily(String family);
+ method public String getFamily();
+ property public final String family;
+ field public static final androidx.glance.text.FontFamily.Companion Companion;
+ }
+
+ public static final class FontFamily.Companion {
+ method public androidx.glance.text.FontFamily getCursive();
+ method public androidx.glance.text.FontFamily getMonospace();
+ method public androidx.glance.text.FontFamily getSansSerif();
+ method public androidx.glance.text.FontFamily getSerif();
+ property public final androidx.glance.text.FontFamily Cursive;
+ property public final androidx.glance.text.FontFamily Monospace;
+ property public final androidx.glance.text.FontFamily SansSerif;
+ property public final androidx.glance.text.FontFamily Serif;
+ }
+
@kotlin.jvm.JvmInline public final value class FontStyle {
field public static final androidx.glance.text.FontStyle.Companion Companion;
}
@@ -739,15 +757,17 @@
}
@androidx.compose.runtime.Immutable public final class TextStyle {
- ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
- method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+ ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+ method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
method public androidx.glance.unit.ColorProvider getColor();
+ method public androidx.glance.text.FontFamily? getFontFamily();
method public androidx.compose.ui.unit.TextUnit? getFontSize();
method public androidx.glance.text.FontStyle? getFontStyle();
method public androidx.glance.text.FontWeight? getFontWeight();
method public androidx.glance.text.TextAlign? getTextAlign();
method public androidx.glance.text.TextDecoration? getTextDecoration();
property public final androidx.glance.unit.ColorProvider color;
+ property public final androidx.glance.text.FontFamily? fontFamily;
property public final androidx.compose.ui.unit.TextUnit? fontSize;
property public final androidx.glance.text.FontStyle? fontStyle;
property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt
new file mode 100644
index 0000000..39b8e52
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.text
+
+/**
+ * Describes the family of the font.
+ * Defaults are provided, but it is also possible to supply a custom family. If this is found on
+ * the system it will be used, otherwise it will fallback to a system default.
+ */
+class FontFamily constructor(val family: String) {
+ companion object {
+ /**
+ * The formal text style for scripts.
+ */
+ val Serif = FontFamily("serif")
+
+ /**
+ * Font family with low contrast and plain stroke endings.
+ */
+ val SansSerif = FontFamily("sans-serif")
+
+ /**
+ * Font family where glyphs have the same fixed width.
+ */
+ val Monospace = FontFamily("monospace")
+
+ /**
+ * Cursive, hand-written like font family.
+ */
+ val Cursive = FontFamily("cursive")
+ }
+
+ override fun toString(): String {
+ return family
+ }
+}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
index f8c01f9..1c1ad1f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
@@ -31,6 +31,7 @@
val fontStyle: FontStyle? = null,
val textAlign: TextAlign? = null,
val textDecoration: TextDecoration? = null,
+ val fontFamily: FontFamily? = null,
) {
fun copy(
color: ColorProvider = this.color,
@@ -38,14 +39,16 @@
fontWeight: FontWeight? = this.fontWeight,
fontStyle: FontStyle? = this.fontStyle,
textAlign: TextAlign? = this.textAlign,
- textDecoration: TextDecoration? = this.textDecoration
+ textDecoration: TextDecoration? = this.textDecoration,
+ fontFamily: FontFamily? = this.fontFamily,
) = TextStyle(
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontStyle = fontStyle,
textAlign = textAlign,
- textDecoration = textDecoration
+ textDecoration = textDecoration,
+ fontFamily = fontFamily,
)
override fun equals(other: Any?): Boolean {
@@ -57,6 +60,7 @@
if (fontStyle != other.fontStyle) return false
if (textDecoration != other.textDecoration) return false
if (textAlign != other.textAlign) return false
+ if (fontFamily != other.fontFamily) return false
return true
}
@@ -67,10 +71,12 @@
result = 31 * result + fontStyle.hashCode()
result = 31 * result + textDecoration.hashCode()
result = 31 * result + textAlign.hashCode()
+ result = 31 * result + fontFamily.hashCode()
return result
}
override fun toString() =
"TextStyle(color=$color, fontSize=$fontSize, fontWeight=$fontWeight, " +
- "fontStyle=$fontStyle, textDecoration=$textDecoration, textAlign=$textAlign)"
+ "fontStyle=$fontStyle, textDecoration=$textDecoration, textAlign=$textAlign, " +
+ "fontFamily=$fontFamily)"
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 3d658fb..c8b1b2e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -8,7 +8,7 @@
APPSEARCH = "1.1.0-alpha03"
ARCH_CORE = "2.3.0-alpha01"
ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
-AUTOFILL = "1.2.0-beta02"
+AUTOFILL = "1.3.0-alpha01"
BENCHMARK = "1.2.0-alpha12"
BIOMETRIC = "1.2.0-alpha06"
BLUETOOTH = "1.0.0-alpha01"
diff --git a/palette/palette-ktx/build.gradle b/palette/palette-ktx/build.gradle
index 20b494b..4eef22b 100644
--- a/palette/palette-ktx/build.gradle
+++ b/palette/palette-ktx/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":palette:palette"))
- }
-
api(project(":palette:palette"))
api(libs.kotlinStdlib)
androidTestImplementation(libs.junit)
diff --git a/palette/palette/build.gradle b/palette/palette/build.gradle
index bb524c8..90bdee7 100644
--- a/palette/palette/build.gradle
+++ b/palette/palette/build.gradle
@@ -6,11 +6,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":palette:palette-ktx"))
- }
-
api("androidx.core:core:1.1.0")
implementation("androidx.collection:collection:1.1.0")
diff --git a/preference/preference-ktx/build.gradle b/preference/preference-ktx/build.gradle
index 19e8159..ebef733 100644
--- a/preference/preference-ktx/build.gradle
+++ b/preference/preference-ktx/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":preference:preference"))
- }
-
api(project(":preference:preference"))
api("androidx.core:core-ktx:1.1.0") {
because "Mirror preference dependency graph for -ktx artifacts"
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 475c906..366ccdc 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":preference:preference-ktx"))
- }
-
api("androidx.annotation:annotation:1.2.0")
api("androidx.appcompat:appcompat:1.1.0")
// Use the latest version of core library for verifying insets visibility
diff --git a/savedstate/savedstate-ktx/build.gradle b/savedstate/savedstate-ktx/build.gradle
index da99fd8..0f3f346 100644
--- a/savedstate/savedstate-ktx/build.gradle
+++ b/savedstate/savedstate-ktx/build.gradle
@@ -23,11 +23,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":savedstate:savedstate"))
- }
-
api(project(":savedstate:savedstate"))
api(libs.kotlinStdlib)
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index 23eb488..a102643 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -14,11 +14,6 @@
}
dependencies {
- // Atomic group
- constraints {
- implementation(project(":savedstate:savedstate-ktx"))
- }
-
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.arch.core:core-common:2.1.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index e36b8b2..50ef0aa 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -7,8 +7,8 @@
}
public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index e36b8b2..50ef0aa 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -7,8 +7,8 @@
}
public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index e32ed48..0b89ad0 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -7,8 +7,8 @@
}
public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
- ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+ ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index cfe4d48..fd2b9f9 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -141,15 +141,15 @@
*/
public DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
- @Nullable SensorGateway sensorGateway,
- @NonNull ObservableStateStore stateStore) {
+ @NonNull ObservableStateStore stateStore,
+ @Nullable SensorGateway sensorGateway) {
// Build pipeline with quota that doesn't allow any animations.
this(
platformDataSourcesInitiallyEnabled,
- sensorGateway,
stateStore,
/* enableAnimations= */ false,
- DISABLED_ANIMATIONS_QUOTA_MANAGER);
+ DISABLED_ANIMATIONS_QUOTA_MANAGER,
+ sensorGateway);
}
/**
@@ -170,15 +170,15 @@
*/
public DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
- @Nullable SensorGateway sensorGateway,
@NonNull ObservableStateStore stateStore,
- @NonNull QuotaManager animationQuotaManager) {
+ @NonNull QuotaManager animationQuotaManager,
+ @Nullable SensorGateway sensorGateway) {
this(
platformDataSourcesInitiallyEnabled,
- sensorGateway,
stateStore,
/* enableAnimations= */ true,
- animationQuotaManager);
+ animationQuotaManager,
+ sensorGateway);
}
/**
@@ -196,10 +196,10 @@
*/
private DynamicTypeEvaluator(
boolean platformDataSourcesInitiallyEnabled,
- @Nullable SensorGateway sensorGateway,
@NonNull ObservableStateStore stateStore,
boolean enableAnimations,
- @NonNull QuotaManager animationQuotaManager) {
+ @NonNull QuotaManager animationQuotaManager,
+ @Nullable SensorGateway sensorGateway) {
this.mSensorGateway = sensorGateway;
Handler uiHandler = new Handler(Looper.getMainLooper());
MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
index 0bcba4e..630218e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
@@ -273,9 +273,9 @@
DynamicTypeEvaluator evaluator =
new DynamicTypeEvaluator(
/* platformDataSourcesInitiallyEnabled= */ true,
- /* sensorGateway= */ null,
stateStore,
- new FixedQuotaManagerImpl(MAX_VALUE));
+ new FixedQuotaManagerImpl(MAX_VALUE),
+ /* sensorGateway= */ null);
mTestCase.runTest(evaluator);
}
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index 3421707..536215b 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -35,7 +35,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation("androidx.core:core:1.7.0")
androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(project(":wear:tiles:tiles-renderer"))
+ androidTestImplementation(project(":wear:protolayout:protolayout-renderer"))
androidTestRuntimeOnly(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
androidTestImplementation(libs.protobuf)
@@ -56,8 +56,7 @@
}
sourceSets {
- // TODO(b/268312818): Update to protolayout location and copy images.
- androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-material"
+ androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-protolayout-material"
}
defaultConfig {
diff --git a/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml b/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..fc13b63
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application
+ android:label="Golden Tests"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:taskAffinity="">
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name="androidx.wear.protolayout.material.testapp.GoldenTestActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
new file mode 100644
index 0000000..13d9252
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenTest {
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+ public MaterialGoldenTest(String expected, LayoutElement layoutElement) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ float scale = displayMetrics.density;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // Not important for components.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ Map<String, LayoutElement> testCases = generateTestCases(context, deviceParameters, "");
+
+ return testCases.entrySet().stream()
+ .map(test -> new Object[] {test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
new file mode 100644
index 0000000..7e03861
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenXLTest {
+ /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+ tests together, first all parametrization (data()) methods are called, and then individual
+ tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+ each test. */
+ private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+ private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+ private static final float FONT_SCALE_XXXL = 1.24f;
+
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+ public MaterialGoldenXLTest(String expected, LayoutElement layoutElement) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ currentDisplayMetrics.setTo(displayMetrics);
+ displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+ float scale = displayMetrics.density;
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // Not important for components.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ Map<String, LayoutElement> testCases =
+ generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+ // Restore state before this method, so other test have correct context.
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+
+ return testCases.entrySet().stream()
+ .map(test -> new Object[] {test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @Parameterized.BeforeParam
+ public static void restoreBefore() {
+ OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+ getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+ }
+
+ @Parameterized.AfterParam
+ public static void restoreAfter() {
+ getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+ }
+
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
new file mode 100644
index 0000000..1033504
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.test.screenshot.matchers.MSSIMMatcher;
+import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.material.testapp.GoldenTestActivity;
+
+public class RunnerUtils {
+ // This isn't totally ideal right now. The screenshot tests run on a phone, so emulate some
+ // watch dimensions here.
+ public static final int SCREEN_WIDTH = 390;
+ public static final int SCREEN_HEIGHT = 390;
+
+ private RunnerUtils() {}
+
+ public static void runSingleScreenshotTest(
+ @NonNull AndroidXScreenshotTestRule rule,
+ @NonNull LayoutElementBuilders.LayoutElement layoutElement,
+ @NonNull String expected) {
+ LayoutElementBuilders.Layout layout =
+ LayoutElementBuilders.Layout.fromLayoutElement(layoutElement);
+ byte[] layoutPayload = layout.toByteArray();
+
+ Intent startIntent =
+ new Intent(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ GoldenTestActivity.class);
+ startIntent.putExtra("layout", layoutPayload);
+
+ ActivityScenario<GoldenTestActivity> scenario = ActivityScenario.launch(startIntent);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ try {
+ // Wait 1s after launching the activity. This allows for the old white layout in the
+ // bootstrap activity to fully go away before proceeding.
+ Thread.sleep(1000);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e("MaterialGoldenTest", "Error sleeping", ex);
+ }
+
+ Bitmap bitmap =
+ Bitmap.createBitmap(
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .takeScreenshot(),
+ 0,
+ 0,
+ SCREEN_WIDTH,
+ SCREEN_HEIGHT);
+ rule.assertBitmapAgainstGolden(bitmap, expected, new MSSIMMatcher());
+
+ // There's a weird bug (related to b/159805732) where, when calling .close() on
+ // ActivityScenario or calling finish() and immediately exiting the test, the test can hang
+ // on a white screen for 45s. Closing the activity here and waiting for 1s seems to fix
+ // this.
+ scenario.onActivity(Activity::finish);
+
+ try {
+ Thread.sleep(1000);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ Log.e("MaterialGoldenTest", "Error sleeping", ex);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
new file mode 100644
index 0000000..cb43b9b
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestCasesGenerator {
+ private TestCasesGenerator() {}
+
+ public static final String NORMAL_SCALE_SUFFIX = "";
+ public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+ private static final String ICON_ID = "tile_icon";
+ private static final String AVATAR = "avatar_image";
+
+ /**
+ * This function will append goldenSuffix on the name of the golden images that should be
+ * different for different user font sizes. Note that some of the golden will have the same name
+ * as it should point on the same size independent image.
+ */
+ @NonNull
+ static Map<String, LayoutElement> generateTestCases(
+ @NonNull Context context,
+ @NonNull DeviceParametersBuilders.DeviceParameters deviceParameters,
+ @NonNull String goldenSuffix) {
+ Clickable clickable =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ String mainText = "Primary label";
+ String labelText = "Secondary label";
+ String largeChipText = "Action";
+ HashMap<String, LayoutElement> testCases = new HashMap<>();
+
+ testCases.put(
+ "default_icon_button_golden" + NORMAL_SCALE_SUFFIX,
+ new Button.Builder(context, clickable).setIconContent(ICON_ID).build());
+ testCases.put(
+ "extralarge_secondary_icon_after_button_golden" + NORMAL_SCALE_SUFFIX,
+ new Button.Builder(context, clickable)
+ .setButtonColors(ButtonDefaults.SECONDARY_COLORS)
+ .setIconContent(ICON_ID)
+ .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+ .build());
+ testCases.put(
+ "large_secondary_icon_40size_button_golden" + NORMAL_SCALE_SUFFIX,
+ new Button.Builder(context, clickable)
+ .setSize(ButtonDefaults.LARGE_SIZE)
+ .setButtonColors(ButtonDefaults.SECONDARY_COLORS)
+ .setIconContent(ICON_ID, dp(40))
+ .build());
+ testCases.put(
+ "extralarge_custom_text_custom_sizefont_button_golden" + goldenSuffix,
+ new Button.Builder(context, clickable)
+ .setButtonColors(new ButtonColors(Color.YELLOW, Color.GREEN))
+ .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+ .setCustomContent(
+ new Text.Builder(context, "ABC")
+ .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+ .setItalic(true)
+ .setColor(argb(Color.GREEN))
+ .build())
+ .build());
+ testCases.put(
+ "default_text_button_golden" + goldenSuffix,
+ new Button.Builder(context, clickable).setTextContent("ABC").build());
+ testCases.put(
+ "default_image_button_golden" + NORMAL_SCALE_SUFFIX,
+ new Button.Builder(context, clickable).setImageContent(AVATAR).build());
+
+ testCases.put(
+ "default_chip_maintext_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryLabelContent(mainText)
+ .build());
+ testCases.put(
+ "default_chip_maintextlabeltext_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryLabelContent(mainText)
+ .setSecondaryLabelContent(labelText)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .build());
+ testCases.put(
+ "default_chip_maintexticon_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryLabelContent(mainText)
+ .setIconContent(ICON_ID)
+ .build());
+ testCases.put(
+ "secondary_chip_maintext_centered_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setChipColors(ChipDefaults.SECONDARY_COLORS)
+ .setPrimaryLabelContent(mainText)
+ .build());
+ testCases.put(
+ "custom_chip_all_overflows_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setWidth(130)
+ .setPrimaryLabelContent(mainText)
+ .setSecondaryLabelContent(labelText)
+ .setIconContent(ICON_ID)
+ .setChipColors(
+ new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+ .build());
+ testCases.put(
+ "default_chip_all_centered_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setPrimaryLabelContent(mainText)
+ .setSecondaryLabelContent(labelText)
+ .setIconContent(ICON_ID)
+ .build());
+ testCases.put(
+ "default_chip_all_rigthalign_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_END)
+ .setPrimaryLabelContent(mainText)
+ .setSecondaryLabelContent(labelText)
+ .setIconContent(ICON_ID)
+ .build());
+ testCases.put(
+ "custom_chip_icon_primary_overflows_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryLabelContent(mainText)
+ .setIconContent(ICON_ID)
+ .setWidth(150)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setChipColors(
+ new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+ .build());
+ testCases.put(
+ "chip_custom_content_centered_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setCustomContent(
+ new Box.Builder()
+ .addContent(
+ new Text.Builder(context, "random text")
+ .setTypography(Typography.TYPOGRAPHY_TITLE3)
+ .setItalic(true)
+ .build())
+ .build())
+ .build());
+ testCases.put(
+ "chip_custom_content_leftaligned_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setChipColors(ChipDefaults.SECONDARY_COLORS)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setCustomContent(
+ new Row.Builder()
+ .addContent(
+ new Text.Builder(context, "text1")
+ .setTypography(Typography.TYPOGRAPHY_TITLE3)
+ .setItalic(true)
+ .setColor(argb(Color.WHITE))
+ .build())
+ .addContent(
+ new Text.Builder(context, "text2")
+ .setTypography(Typography.TYPOGRAPHY_TITLE2)
+ .setColor(argb(Color.YELLOW))
+ .build())
+ .build())
+ .setWidth(150)
+ .build());
+ testCases.put(
+ "chip_2lines_primary_overflows_golden" + goldenSuffix,
+ new Chip.Builder(context, clickable, deviceParameters)
+ .setPrimaryLabelContent("abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde")
+ .build());
+
+ // Different text lengths to test expanding the width based on the size of text. If it's
+ // more than 9, the rest will be deleted.
+ testCases.put(
+ "compactchip_default_len2_golden" + goldenSuffix,
+ new CompactChip.Builder(context, "Ab", clickable, deviceParameters).build());
+ testCases.put(
+ "compactchip_default_len5_golden" + goldenSuffix,
+ new CompactChip.Builder(context, "Abcde", clickable, deviceParameters).build());
+ testCases.put(
+ "compactchip_default_len9_golden" + goldenSuffix,
+ new CompactChip.Builder(context, "Abcdefghi", clickable, deviceParameters).build());
+ testCases.put(
+ "compactchip_default_toolong_golden" + goldenSuffix,
+ new CompactChip.Builder(
+ context, "AbcdefghiEXTRAEXTRAEXTRA", clickable, deviceParameters)
+ .build());
+ testCases.put(
+ "compactchip_custom_default_golden" + goldenSuffix,
+ new CompactChip.Builder(context, "Action", clickable, deviceParameters)
+ .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
+ .build());
+
+ testCases.put(
+ "titlechip_default_golden" + goldenSuffix,
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters).build());
+ testCases.put(
+ "titlechip_default_texttoolong_golden" + goldenSuffix,
+ new TitleChip.Builder(context, "abcdeabcdeabcdeEXTRA", clickable, deviceParameters)
+ .build());
+ testCases.put(
+ "titlechip_leftalign_secondary_default_golden" + goldenSuffix,
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setChipColors(ChipDefaults.TITLE_SECONDARY_COLORS)
+ .build());
+ testCases.put(
+ "titlechip_centered_custom_150_secondary_default_golden" + goldenSuffix,
+ new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setChipColors(new ChipColors(Color.YELLOW, Color.BLUE))
+ .setWidth(150)
+ .build());
+
+ testCases.put(
+ "default_full_circularprogressindicator",
+ new CircularProgressIndicator.Builder().build());
+ testCases.put(
+ "default_gap_circularprogressindicator",
+ new CircularProgressIndicator.Builder()
+ .setStartAngle(ProgressIndicatorDefaults.GAP_START_ANGLE)
+ .setEndAngle(ProgressIndicatorDefaults.GAP_END_ANGLE)
+ .build());
+ testCases.put(
+ "default_full_90_circularprogressindicator",
+ new CircularProgressIndicator.Builder().setProgress(0.25f).build());
+ testCases.put(
+ "default_gap_90_circularprogressindicator",
+ new CircularProgressIndicator.Builder()
+ .setProgress(0.25f)
+ .setStartAngle(ProgressIndicatorDefaults.GAP_START_ANGLE)
+ .setEndAngle(ProgressIndicatorDefaults.GAP_END_ANGLE)
+ .build());
+ testCases.put(
+ "custom_gap_45_circularprogressindicator",
+ new CircularProgressIndicator.Builder()
+ .setStartAngle(45)
+ .setEndAngle(270)
+ .setProgress(0.2f)
+ .setStrokeWidth(12)
+ .setCircularProgressIndicatorColors(
+ new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
+ .build());
+
+ testCases.put(
+ "default_text_golden" + goldenSuffix, new Text.Builder(context, "Testing").build());
+ testCases.put(
+ "custom_text_golden" + goldenSuffix,
+ new Text.Builder(context, "Testing text.")
+ .setItalic(true)
+ .setColor(argb(Color.YELLOW))
+ .setWeight(LayoutElementBuilders.FONT_WEIGHT_BOLD)
+ .setTypography(Typography.TYPOGRAPHY_BODY2)
+ .build());
+ testCases.put(
+ "overflow_text_golden" + goldenSuffix,
+ new Text.Builder(context, "abcdeabcdeabcde").build());
+
+ return testCases;
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
new file mode 100644
index 0000000..e39d6fa
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class LayoutsGoldenTest {
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+ public LayoutsGoldenTest(String expected, LayoutElement layoutElement) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ float scale = displayMetrics.density;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // TODO(b/231543947): Add test cases for round screen.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ Map<String, LayoutElement> testCases = generateTestCases(context, deviceParameters, "");
+
+ return testCases.entrySet().stream()
+ .map(test -> new Object[] {test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
new file mode 100644
index 0000000..0089ecd
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class LayoutsGoldenXLTest {
+ /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+ tests together, first all parametrization (data()) methods are called, and then individual
+ tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+ each test. */
+ private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+ private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+ private static final float FONT_SCALE_XXXL = 1.24f;
+
+ private final LayoutElement mLayoutElement;
+ private final String mExpected;
+
+ @Rule
+ public AndroidXScreenshotTestRule mScreenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+ public LayoutsGoldenXLTest(String expected, LayoutElement layoutElement) {
+ mLayoutElement = layoutElement;
+ mExpected = expected;
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ currentDisplayMetrics.setTo(displayMetrics);
+ displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(displayMetrics);
+
+ DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+ float scale = displayMetrics.density;
+ DeviceParameters deviceParameters =
+ new DeviceParameters.Builder()
+ .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+ .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+ .setScreenDensity(displayMetrics.density)
+ // TODO(b/231543947): Add test cases for round screen.
+ .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+ .build();
+
+ Map<String, LayoutElement> testCases =
+ generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+ // Restore state before this method, so other test have correct context. This is needed here
+ // too, besides in restoreBefore and restoreAfter as the test cases builder uses the context
+ // to apply font scaling, so we need that display metrics passed in. However, after
+ // generating cases we need to restore the state as other data() methods in this package can
+ // work correctly with the default state, as when the tests are run, first all data() static
+ // methods are called, and then parameterized test cases.
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+ InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getResources()
+ .getDisplayMetrics()
+ .setTo(currentDisplayMetrics);
+
+ return testCases.entrySet().stream()
+ .map(test -> new Object[] {test.getKey(), test.getValue()})
+ .collect(Collectors.toList());
+ }
+
+ @Parameterized.BeforeParam
+ public static void restoreBefore() {
+ // Set the state as it was in data() method when we generated test cases. This was
+ // overridden by other static data() methods, so we need to restore it.
+ OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+ getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+ }
+
+ @Parameterized.AfterParam
+ public static void restoreAfter() {
+ // Restore the state to default, so the other tests and emulator have the correct starter
+ // state.
+ getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+ }
+
+ @SdkSuppress(maxSdkVersion = 32) // b/271486183
+ @Test
+ public void test() {
+ runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java
new file mode 100644
index 0000000..e95c9da
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.DimensionBuilders.expand;
+import static androidx.wear.protolayout.DimensionBuilders.wrap;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.NonNull;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Spacer;
+import androidx.wear.protolayout.ModifiersBuilders.Background;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.Button;
+import androidx.wear.protolayout.material.ButtonDefaults;
+import androidx.wear.protolayout.material.Chip;
+import androidx.wear.protolayout.material.ChipColors;
+import androidx.wear.protolayout.material.CircularProgressIndicator;
+import androidx.wear.protolayout.material.Colors;
+import androidx.wear.protolayout.material.CompactChip;
+import androidx.wear.protolayout.material.ProgressIndicatorColors;
+import androidx.wear.protolayout.material.ProgressIndicatorDefaults;
+import androidx.wear.protolayout.material.Text;
+import androidx.wear.protolayout.material.TitleChip;
+import androidx.wear.protolayout.material.Typography;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestCasesGenerator {
+ private TestCasesGenerator() {}
+
+ public static final String NORMAL_SCALE_SUFFIX = "";
+ public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+
+ /**
+ * This function will append goldenSuffix on the name of the golden images that should be
+ * different for different user font sizes. Note that some of the golden will have the same name
+ * as it should point on the same size independent image.
+ */
+ @NonNull
+ static Map<String, LayoutElement> generateTestCases(
+ @NonNull Context context,
+ @NonNull DeviceParameters deviceParameters,
+ @NonNull String goldenSuffix) {
+ Clickable clickable =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ HashMap<String, LayoutElement> testCases = new HashMap<>();
+
+ TitleChip content =
+ new TitleChip.Builder(context, "Action", clickable, deviceParameters).build();
+ CompactChip.Builder primaryChipBuilder =
+ new CompactChip.Builder(context, "Action", clickable, deviceParameters);
+
+ testCases.put(
+ "default_empty_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "default_longtext_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(
+ new CompactChip.Builder(
+ context,
+ "Too_long_textToo_long_textToo_long_text",
+ clickable,
+ deviceParameters)
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_primarylabel_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(buildColoredBoxPLL(Color.YELLOW))
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "coloredbox_primarylabel_secondarylabel_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(buildColoredBoxPLL(Color.YELLOW))
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+ testCases.put(
+ "coloredbox_secondarylabel_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(buildColoredBoxPLL(Color.YELLOW))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+
+ testCases.put(
+ "custom_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(content)
+ .setPrimaryChipContent(
+ primaryChipBuilder
+ .setChipColors(new ChipColors(Color.YELLOW, Color.GREEN))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(buildColoredBoxPLL(Color.YELLOW))
+ .build());
+ testCases.put(
+ "two_chips_content_primarychiplayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new Column.Builder()
+ .setWidth(expand())
+ .setHeight(wrap())
+ .addContent(
+ new Chip.Builder(
+ context,
+ clickable,
+ deviceParameters)
+ .setPrimaryLabelContent("First chip")
+ .setWidth(expand())
+ .build())
+ .addContent(new Spacer.Builder().setHeight(dp(4)).build())
+ .addContent(
+ new Chip.Builder(
+ context,
+ clickable,
+ deviceParameters)
+ .setPrimaryLabelContent("Second chip")
+ .setWidth(expand())
+ .build())
+ .build())
+ .build());
+
+ primaryChipBuilder =
+ new CompactChip.Builder(context, "Action", clickable, deviceParameters);
+ testCases.put(
+ "coloredbox_1_chip_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_2_chip_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_3_chip_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_2_chip_primary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "coloredbox_2_chip_primary_secondary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+ testCases.put(
+ "coloredbox_2_columnslayout_golden" + NORMAL_SCALE_SUFFIX,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_2_primary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "coloredbox_2_primary_secondary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+ testCases.put(
+ "coloredbox_3_chip_primary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "coloredbox_3_chip_primary_secondary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+ testCases.put(
+ "coloredbox_3_columnslayout_golden" + NORMAL_SCALE_SUFFIX,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_3_primary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "coloredbox_3_primary_secondary_columnslayout_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .build());
+ testCases.put(
+ "custom_spacer_coloredbox_3_chip_primary_secondary_columnslayout_golden"
+ + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setContent(
+ new MultiSlotLayout.Builder()
+ .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+ .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+ .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+ .setHorizontalSpacerWidth(2)
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+ .setVerticalSpacerHeight(1)
+ .build());
+
+ CircularProgressIndicator.Builder progressIndicatorBuilder =
+ new CircularProgressIndicator.Builder().setProgress(0.3f);
+ Text textContent =
+ new Text.Builder(context, "Text")
+ .setColor(argb(Color.WHITE))
+ .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+ .build();
+ testCases.put(
+ "default_text_progressindicatorlayout_golden" + goldenSuffix,
+ new EdgeContentLayout.Builder(deviceParameters)
+ .setEdgeContent(progressIndicatorBuilder.build())
+ .setPrimaryLabelTextContent(
+ new Text.Builder(context, "Primary label")
+ .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+ .setColor(argb(Colors.PRIMARY))
+ .build())
+ .setContent(textContent)
+ .setSecondaryLabelTextContent(
+ new Text.Builder(context, "Secondary label")
+ .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+ .setColor(argb(Colors.ON_SURFACE))
+ .build())
+ .build());
+ testCases.put(
+ "default_empty_progressindicatorlayout_golden" + NORMAL_SCALE_SUFFIX,
+ new EdgeContentLayout.Builder(deviceParameters)
+ .setEdgeContent(progressIndicatorBuilder.build())
+ .build());
+ testCases.put(
+ "custom_progressindicatorlayout_golden" + goldenSuffix,
+ new EdgeContentLayout.Builder(deviceParameters)
+ .setContent(textContent)
+ .setEdgeContent(
+ progressIndicatorBuilder
+ .setCircularProgressIndicatorColors(
+ new ProgressIndicatorColors(
+ Color.YELLOW, Color.GREEN))
+ .build())
+ .build());
+ testCases.put(
+ "coloredbox_progressindicatorlayout_golden" + NORMAL_SCALE_SUFFIX,
+ new EdgeContentLayout.Builder(deviceParameters)
+ .setEdgeContent(
+ progressIndicatorBuilder
+ .setCircularProgressIndicatorColors(
+ ProgressIndicatorDefaults.DEFAULT_COLORS)
+ .build())
+ .setContent(
+ new Box.Builder()
+ .setWidth(dp(500))
+ .setHeight(dp(500))
+ .setModifiers(
+ new Modifiers.Builder()
+ .setBackground(
+ new Background.Builder()
+ .setColor(
+ argb(Color.YELLOW))
+ .build())
+ .build())
+ .build())
+ .build());
+
+ Button button1 = new Button.Builder(context, clickable).setTextContent("1").build();
+ Button button2 = new Button.Builder(context, clickable).setTextContent("2").build();
+ Button button3 = new Button.Builder(context, clickable).setTextContent("3").build();
+ Button button4 = new Button.Builder(context, clickable).setTextContent("4").build();
+ Button button5 = new Button.Builder(context, clickable).setTextContent("5").build();
+ Button button6 = new Button.Builder(context, clickable).setTextContent("6").build();
+ Button button7 = new Button.Builder(context, clickable).setTextContent("7").build();
+ Button largeButton1 =
+ new Button.Builder(context, clickable)
+ .setTextContent("1")
+ .setSize(ButtonDefaults.LARGE_SIZE)
+ .build();
+ Button largeButton2 =
+ new Button.Builder(context, clickable)
+ .setTextContent("2")
+ .setSize(ButtonDefaults.LARGE_SIZE)
+ .build();
+ Button extraLargeButton =
+ new Button.Builder(context, clickable)
+ .setTextContent("1")
+ .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+ .build();
+ testCases.put(
+ "multibutton_layout_1button_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(extraLargeButton)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_1button_chip_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(extraLargeButton)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_2button_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(largeButton1)
+ .addButtonContent(largeButton2)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_2button_chip_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(largeButton1)
+ .addButtonContent(largeButton2)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_2button_primarylabel_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(largeButton1)
+ .addButtonContent(largeButton2)
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "multibutton_layout_2button_chip_primarylabel_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(largeButton1)
+ .addButtonContent(largeButton2)
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_3button_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_3button_chip_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_3button_primarylabel_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "multibutton_layout_3button_chip_primarylabel_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+ .build());
+ testCases.put(
+ "multibutton_layout_4button_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_4button_chip_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_5button_top_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .addButtonContent(button5)
+ .setFiveButtonDistribution(
+ MultiButtonLayout
+ .FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_5button_bottom_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .addButtonContent(button5)
+ .setFiveButtonDistribution(
+ MultiButtonLayout
+ .FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_5button_bottom_chip_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .addButtonContent(button5)
+ .setFiveButtonDistribution(
+ MultiButtonLayout
+ .FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY)
+ .build())
+ .setPrimaryChipContent(primaryChipBuilder.build())
+ .build());
+ testCases.put(
+ "multibutton_layout_6button_golden" + goldenSuffix,
+ new PrimaryLayout.Builder(deviceParameters)
+ .setContent(
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .addButtonContent(button5)
+ .addButtonContent(button6)
+ .build())
+ .build());
+ testCases.put(
+ "multibutton_layout_7button_golden" + goldenSuffix,
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .addButtonContent(button3)
+ .addButtonContent(button4)
+ .addButtonContent(button5)
+ .addButtonContent(button6)
+ .addButtonContent(button7)
+ .build());
+
+ return testCases;
+ }
+
+ @NonNull
+ private static Text buildTextLabel(@NonNull Context context, @NonNull String text) {
+ return new Text.Builder(context, text)
+ .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+ .setColor(argb(Color.WHITE))
+ .build();
+ }
+
+ @NonNull
+ private static Box buildColoredBoxMSL(int color) {
+ return new Box.Builder()
+ .setWidth(dp(60))
+ .setHeight(dp(60))
+ .setModifiers(
+ new Modifiers.Builder()
+ .setBackground(
+ new Background.Builder().setColor(argb(color)).build())
+ .build())
+ .build();
+ }
+
+ @NonNull
+ private static Box buildColoredBoxPLL(int color) {
+ return new Box.Builder()
+ .setWidth(expand())
+ .setHeight(dp(60))
+ .setModifiers(
+ new Modifiers.Builder()
+ .setBackground(
+ new Background.Builder().setColor(argb(color)).build())
+ .build())
+ .build();
+ }
+
+ @Dimension(unit = Dimension.DP)
+ static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java
new file mode 100644
index 0000000..53fc7d0
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.testapp;
+
+import static androidx.wear.protolayout.material.Helper.checkNotNull;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.wear.protolayout.LayoutElementBuilders.Layout;
+import androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId;
+import androidx.wear.protolayout.ResourceBuilders.ImageResource;
+import androidx.wear.protolayout.ResourceBuilders.Resources;
+import androidx.wear.protolayout.material.R;
+import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+@SuppressWarnings("deprecation")
+public class GoldenTestActivity extends Activity {
+ private static final String ICON_ID = "icon";
+ private static final String AVATAR = "avatar_image";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ byte[] layoutPayload = getIntent().getExtras().getByteArray("layout");
+ Layout layout = Layout.fromByteArray(layoutPayload);
+
+ Context appContext = getApplicationContext();
+ FrameLayout root = new FrameLayout(appContext);
+ root.setBackgroundColor(Color.BLACK);
+ root.setLayoutParams(new LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+
+ ListeningExecutorService mainExecutor = MoreExecutors.newDirectExecutorService();
+ Resources resources = generateResources();
+
+ ProtoLayoutViewInstance instance =
+ new ProtoLayoutViewInstance(
+ new ProtoLayoutViewInstance.Config.Builder(
+ appContext,
+ mainExecutor,
+ mainExecutor,
+ "androidx.wear.tiles.extra.CLICKABLE_ID")
+ .setIsViewFullyVisible(true)
+ .build());
+
+ instance.renderAndAttach(checkNotNull(layout).toProto(), resources.toProto(), root);
+
+ View firstChild = root.getChildAt(0);
+
+ // Simulate what the thing outside the renderer should do. Center the contents.
+ LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
+ layoutParams.gravity = Gravity.CENTER;
+
+ // Set the activity to be full screen so when we crop the Bitmap we don't get time bar etc.
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow()
+ .setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ setContentView(root, new ViewGroup.LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+ super.onCreate(savedInstanceState);
+ }
+
+ private static Resources generateResources() {
+ return new Resources.Builder()
+ .addIdToImageMapping(
+ ICON_ID,
+ new ImageResource.Builder()
+ .setAndroidResourceByResId(
+ new AndroidImageResourceByResId.Builder()
+ .setResourceId(R.drawable.icon)
+ .build())
+ .build())
+ .addIdToImageMapping(
+ AVATAR,
+ new ImageResource.Builder()
+ .setAndroidResourceByResId(
+ new AndroidImageResourceByResId.Builder()
+ .setResourceId(R.drawable.avatar)
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png b/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png
new file mode 100644
index 0000000..a6da988
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png
Binary files differ
diff --git a/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml b/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml
new file mode 100644
index 0000000..21eb853
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#000"
+ android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/types.proto b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
index c9ddacc..5d06b7c 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/types.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
@@ -20,8 +20,11 @@
// A string type.
message StringProp {
- // The value.
- string value = 1;
+
+ oneof optional_value {
+ // The value.
+ string value = 1;
+ }
// The dynamic value.
androidx.wear.protolayout.expression.proto.DynamicString dynamic_value = 2;
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
index 3a01156..11ff17f 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
@@ -141,10 +141,10 @@
if (enableAnimations) {
this.mEvaluator =
new DynamicTypeEvaluator(
- canUpdateGateways, sensorGateway, stateStore, animationQuotaManager);
+ canUpdateGateways, stateStore, animationQuotaManager, sensorGateway);
} else {
this.mEvaluator =
- new DynamicTypeEvaluator(canUpdateGateways, sensorGateway, stateStore);
+ new DynamicTypeEvaluator(canUpdateGateways, stateStore, sensorGateway);
}
}
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index d4dbbd8..2a5ba9d 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1095,13 +1095,27 @@
method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
}
+ public static final class TypeBuilders.StringLayoutConstraint {
+ method public int getAlignment();
+ method public String getPatternForLayout();
+ }
+
+ public static final class TypeBuilders.StringLayoutConstraint.Builder {
+ ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+ }
+
public static final class TypeBuilders.StringProp {
+ method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
method public String getValue();
}
public static final class TypeBuilders.StringProp.Builder {
- ctor public TypeBuilders.StringProp.Builder();
+ ctor @Deprecated public TypeBuilders.StringProp.Builder();
+ ctor public TypeBuilders.StringProp.Builder(String);
method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+ method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
}
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index 41ff5b2..6de2652 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -1243,13 +1243,27 @@
method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
}
+ public static final class TypeBuilders.StringLayoutConstraint {
+ method public int getAlignment();
+ method public String getPatternForLayout();
+ }
+
+ public static final class TypeBuilders.StringLayoutConstraint.Builder {
+ ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+ }
+
public static final class TypeBuilders.StringProp {
+ method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
method public String getValue();
}
public static final class TypeBuilders.StringProp.Builder {
- ctor public TypeBuilders.StringProp.Builder();
+ ctor @Deprecated public TypeBuilders.StringProp.Builder();
+ ctor public TypeBuilders.StringProp.Builder(String);
method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+ method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
}
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index d4dbbd8..2a5ba9d 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1095,13 +1095,27 @@
method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
}
+ public static final class TypeBuilders.StringLayoutConstraint {
+ method public int getAlignment();
+ method public String getPatternForLayout();
+ }
+
+ public static final class TypeBuilders.StringLayoutConstraint.Builder {
+ ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+ method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+ }
+
public static final class TypeBuilders.StringProp {
+ method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
method public String getValue();
}
public static final class TypeBuilders.StringProp.Builder {
- ctor public TypeBuilders.StringProp.Builder();
+ ctor @Deprecated public TypeBuilders.StringProp.Builder();
+ ctor public TypeBuilders.StringProp.Builder(String);
method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+ method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
index 0bcbc72..fd275b0 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
@@ -16,13 +16,17 @@
package androidx.wear.protolayout;
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.DynamicBuilders;
import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.AlignmentProto;
import androidx.wear.protolayout.proto.TypesProto;
/** Builders for extensible primitive types used by layout elements. */
@@ -111,7 +115,7 @@
}
/**
- * Gets the value.
+ * Gets the static value.
*
* @since 1.0
*/
@@ -120,6 +124,20 @@
return mImpl.getValue();
}
+ /**
+ * Gets the dynamic value.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicBuilders.DynamicString getDynamicValue() {
+ if (mImpl.hasDynamicValue()) {
+ return DynamicBuilders.dynamicStringFromProto(mImpl.getDynamicValue());
+ } else {
+ return null;
+ }
+ }
+
/** Get the fingerprint for this object, or null if unknown. */
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
@@ -142,10 +160,26 @@
private final TypesProto.StringProp.Builder mImpl = TypesProto.StringProp.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(327834307);
+ /**
+ * Creates an instance of {@link Builder}.
+ *
+ * @deprecated use {@link Builder(String)}
+ */
+ @Deprecated
public Builder() {}
/**
- * Sets the value.
+ * Creates an instance of {@link Builder}.
+ *
+ * @param staticValue the static value.
+ */
+ public Builder(@NonNull String staticValue) {
+ setValue(staticValue);
+ }
+
+ /**
+ * Sets the static value. If a dynamic value is also set and the renderer supports
+ * dynamic values for the corresponding field, this static value will be ignored.
*
* @since 1.0
*/
@@ -156,15 +190,137 @@
return this;
}
+ /**
+ * Sets the dynamic value. Note that when setting this value, the static value is still
+ * required to be set to support older renderers that only read the static value.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setDynamicValue(@NonNull DynamicBuilders.DynamicString dynamicValue) {
+ mImpl.setDynamicValue(dynamicValue.toDynamicStringProto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(dynamicValue.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
/** Builds an instance from accumulated values. */
@NonNull
public StringProp build() {
+ if (mImpl.hasDynamicValue() && !mImpl.hasValue()) {
+ throw new IllegalStateException("Static value is missing.");
+ }
return new StringProp(mImpl.build(), mFingerprint);
}
}
}
/**
+ * A type for specifying layout constraints when using {@link StringProp} on a data bindable
+ * layout element.
+ *
+ * @since 1.2
+ */
+ public static final class StringLayoutConstraint {
+ private final TypesProto.StringProp mImpl;
+ @Nullable private final Fingerprint mFingerprint;
+
+ StringLayoutConstraint(TypesProto.StringProp impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the text string to use as the pattern for the largest text that can be laid out.
+ * Used to ensure that the layout is of a known size during the layout pass. If
+ * not set defaults to the static value of the associated {@link StringProp} field.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public String getPatternForLayout() {
+ return mImpl.getValueForLayout();
+ }
+
+ /**
+ * Gets angular alignment of the actual content within the space reserved by value.
+ *
+ * @since 1.2
+ */
+ @LayoutElementBuilders.TextAlignment
+ public int getAlignment() {
+ return mImpl.getTextAlignmentForLayoutValue();
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public TypesProto.StringProp toProto() {
+ return mImpl;
+ }
+
+ @NonNull
+ static StringLayoutConstraint fromProto(@NonNull TypesProto.StringProp proto) {
+ return new StringLayoutConstraint(proto, null);
+ }
+
+ /** Builder for {@link StringLayoutConstraint}. */
+ public static final class Builder {
+ private final TypesProto.StringProp.Builder mImpl = TypesProto.StringProp.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1927567664);
+
+ /**
+ * Creates a new builder for {@link StringLayoutConstraint}.
+ *
+ * @param patternForLayout Sets the text string to use as the pattern for the largest
+ * text that can be laid out. Used to ensure that the layout
+ * is of a known size during the layout pass.
+ * @since 1.2
+ */
+ public Builder(@NonNull String patternForLayout) {
+ setValue(patternForLayout);
+ }
+
+ /**
+ * Sets the text string to use as the pattern for the largest text that can be laid out.
+ * Used to ensure that the layout is of a known size during the layout pass.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ private Builder setValue(@NonNull String patternForLayout) {
+ mImpl.setValueForLayout(patternForLayout);
+ mFingerprint.recordPropertyUpdate(3, patternForLayout.hashCode());
+ return this;
+ }
+
+ /**
+ * Sets alignment of the actual text within the space reserved by patternForMeasurement.
+ * If not specified, defaults to center alignment.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public Builder setAlignment(@LayoutElementBuilders.TextAlignment int alignment) {
+ mImpl.setTextAlignmentForLayout(AlignmentProto.TextAlignment.forNumber(alignment));
+ mFingerprint.recordPropertyUpdate(4, alignment);
+ return this;
+ }
+
+ /** Builds an instance of {@link StringLayoutConstraint}. */
+ @NonNull
+ public StringLayoutConstraint build() {
+ return new StringLayoutConstraint(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* A float type.
*
* @since 1.0
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java
new file mode 100644
index 0000000..21cac51
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.proto.TypesProto;
+
+import org.junit.Test;
+
+public class TypeBuildersTest {
+ private static final String STATE_KEY = "state-key";
+ private static final TypeBuilders.StringProp STRING_PROP =
+ new TypeBuilders.StringProp.Builder("string")
+ .setDynamicValue(DynamicBuilders.DynamicString.fromState(STATE_KEY))
+ .build();
+
+ @SuppressWarnings("deprecation")
+ private static final TypeBuilders.StringProp.Builder STRING_PROP_BUILDER_WITHOUT_STATIC_VALUE =
+ new TypeBuilders.StringProp.Builder()
+ .setDynamicValue(DynamicBuilders.DynamicString.fromState(STATE_KEY));
+
+ @Test
+ public void stringPropSupportsDynamicString() {
+ TypesProto.StringProp stringPropProto = STRING_PROP.toProto();
+
+ assertThat(stringPropProto.getValue()).isEqualTo(STRING_PROP.getValue());
+ assertThat(stringPropProto.getDynamicValue().getStateSource().getSourceKey())
+ .isEqualTo(STATE_KEY);
+ }
+
+ @Test
+ public void stringProp_withoutStaticValue_throws() {
+ assertThrows(IllegalStateException.class, STRING_PROP_BUILDER_WITHOUT_STATIC_VALUE::build);
+ }
+}
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index 7270552..1d8a312 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -36,6 +36,7 @@
implementation "androidx.wear:wear:1.2.0"
implementation(project(":wear:protolayout:protolayout"))
+ implementation(project(":wear:protolayout:protolayout-expression-pipeline"))
implementation(project(":wear:protolayout:protolayout-renderer"))
implementation(project(":wear:tiles:tiles"))
implementation(project(":wear:tiles:tiles-proto"))
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 1432c9e..0cc77dc 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -26,6 +26,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.expression.pipeline.ObservableStateStore;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.ResourceProto;
import androidx.wear.protolayout.proto.StateProto;
@@ -34,6 +35,7 @@
import androidx.wear.tiles.StateBuilders;
import androidx.wear.tiles.TileService;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@@ -115,6 +117,9 @@
ProtoLayoutViewInstance.Config.Builder config =
new ProtoLayoutViewInstance.Config.Builder(uiContext, mUiExecutor, mUiExecutor,
TileService.EXTRA_CLICKABLE_ID)
+ .setAnimationEnabled(true)
+ .setIsViewFullyVisible(true)
+ .setStateStore(new ObservableStateStore(ImmutableMap.of()))
.setLoadActionListener(instanceListener);
this.mInstance = new ProtoLayoutViewInstance(config.build());
}
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index fbe14a8..12d6cc7 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -47,8 +47,8 @@
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener
import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -274,7 +274,7 @@
*/
public abstract class ComplicationDataSourceService : Service() {
private var wrapper: IComplicationProviderWrapper? = null
- private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null
+ private var lastExpressionEvaluationJob: Job? = null
internal val mainThreadHandler by lazy { createMainThreadHandler() }
internal val mainThreadCoroutineScope by lazy {
CoroutineScope(mainThreadHandler.asCoroutineDispatcher())
@@ -307,7 +307,7 @@
override fun onDestroy() {
super.onDestroy()
- lastExpressionEvaluator?.close()
+ lastExpressionEvaluationJob?.cancel()
}
/**
@@ -554,7 +554,8 @@
}
private fun WireComplicationData?.evaluateAndUpdateManager() {
- lastExpressionEvaluator?.close() // Cancelling any previous evaluation.
+ // Cancelling any previous evaluation.
+ lastExpressionEvaluationJob?.cancel()
if (
// Will be evaluated by the platform.
// TODO(b/257422920): Set this to the exact platform version.
@@ -568,11 +569,14 @@
)
return
}
- lastExpressionEvaluator =
- ComplicationDataExpressionEvaluator(this).apply {
- listenAndUpdateManager(
- iComplicationManager,
+ lastExpressionEvaluationJob =
+ mainThreadCoroutineScope.launch {
+ iComplicationManager.updateComplicationData(
complicationInstanceId,
+ // Doing one-off evaluation, the service will be re-invoked.
+ ComplicationDataExpressionEvaluator()
+ .evaluate(this@evaluateAndUpdateManager)
+ .first(),
)
}
}
@@ -581,22 +585,6 @@
}
}
- private fun ComplicationDataExpressionEvaluator.listenAndUpdateManager(
- iComplicationManager: IComplicationManager,
- complicationInstanceId: Int,
- ) {
- mainThreadCoroutineScope.launch {
- use {
- init()
- // Doing one-off evaluation, the service will be re-invoked.
- iComplicationManager.updateComplicationData(
- complicationInstanceId,
- data.filterNotNull().first()
- )
- }
- }
- }
-
@SuppressLint("SyntheticAccessor")
override fun onComplicationDeactivated(complicationInstanceId: Int) {
mainThreadHandler.post {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index f496e85..f4d51da 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -20,7 +20,6 @@
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationText as WireComplicationText
import androidx.annotation.RestrictTo
-import androidx.core.util.Consumer
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.pipeline.BoundDynamicType
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
@@ -29,16 +28,15 @@
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway
import java.util.concurrent.Executor
import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.update
/**
@@ -49,211 +47,104 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
- val unevaluatedData: WireComplicationData,
private val stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
private val sensorGateway: SensorGateway? = null,
private val keepExpression: Boolean = false,
-) : AutoCloseable {
+) {
/**
- * Java compatibility class for [ComplicationDataExpressionEvaluator].
+ * Returns a [Flow] that provides the evaluated [WireComplicationData].
*
- * Unlike [data], [listener] is not invoked until there is a value (until [data] is non-null).
+ * The expression is evaluated _separately_ on each flow collection.
*/
- class Compat
- internal constructor(
- val unevaluatedData: WireComplicationData,
- private val listener: Consumer<WireComplicationData>,
- stateStore: ObservableStateStore,
- sensorGateway: SensorGateway?,
- keepExpression: Boolean,
- ) : AutoCloseable {
- private val evaluator =
- ComplicationDataExpressionEvaluator(
- unevaluatedData,
- stateStore,
- sensorGateway,
- keepExpression,
+ fun evaluate(unevaluatedData: WireComplicationData) =
+ flow<WireComplicationData> {
+ val state: MutableStateFlow<State> = unevaluatedData.buildState()
+ state.value.use {
+ val evaluatedData: Flow<WireComplicationData> =
+ state
+ .mapNotNull {
+ when {
+ // Emitting INVALID_DATA if there's an invalid receiver.
+ it.invalidReceivers.isNotEmpty() -> INVALID_DATA
+ // Emitting the data if all pending receivers are done and all
+ // pre-updates are satisfied.
+ it.pendingReceivers.isEmpty() && it.preUpdateCount == 0 -> it.data
+ // Skipping states that are not ready for be emitted.
+ else -> null
+ }
+ }
+ .distinctUntilChanged()
+ emitAll(evaluatedData)
+ }
+ }
+
+ private suspend fun WireComplicationData.buildState() =
+ MutableStateFlow(State(this)).apply {
+ if (hasRangedValueExpression()) {
+ addReceiver(
+ rangedValueExpression,
+ expressionTrimmer = { setRangedValueExpression(null) },
+ setter = { setRangedValue(it) },
+ )
+ }
+ if (hasLongText()) addReceiver(longText) { setLongText(it) }
+ if (hasLongTitle()) addReceiver(longTitle) { setLongTitle(it) }
+ if (hasShortText()) addReceiver(shortText) { setShortText(it) }
+ if (hasShortTitle()) addReceiver(shortTitle) { setShortTitle(it) }
+ if (hasContentDescription()) {
+ addReceiver(contentDescription) { setContentDescription(it) }
+ }
+ // Add all the receivers before we start binding them because binding can synchronously
+ // trigger the receiver, which would update the data before all the fields are
+ // evaluated.
+ value.initEvaluation()
+ }
+
+ private suspend fun MutableStateFlow<State>.addReceiver(
+ expression: DynamicFloat?,
+ expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+ setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
+ ) {
+ expression ?: return
+ val executor = currentCoroutineContext().asExecutor()
+ update { state ->
+ state.withPendingReceiver(
+ ComplicationEvaluationResultReceiver<Float>(
+ this,
+ setter = { value ->
+ if (!keepExpression) expressionTrimmer(this)
+ setter(this, value)
+ },
+ binder = { receiver -> value.evaluator.bind(expression, executor, receiver) },
+ )
)
-
- /** Builder for [ComplicationDataExpressionEvaluator.Compat]. */
- class Builder(
- private val unevaluatedData: WireComplicationData,
- private val listener: Consumer<WireComplicationData>,
- ) {
- private var stateStore: ObservableStateStore = ObservableStateStore(emptyMap())
- private var sensorGateway: SensorGateway? = null
- private var keepExpression: Boolean = false
-
- fun setStateStore(value: ObservableStateStore) = apply { stateStore = value }
- fun setSensorGateway(value: SensorGateway?) = apply { sensorGateway = value }
- fun setKeepExpression(value: Boolean) = apply { keepExpression = value }
-
- fun build() =
- Compat(unevaluatedData, listener, stateStore, sensorGateway, keepExpression)
- }
-
- /**
- * @see ComplicationDataExpressionEvaluator.init, [executor] is used in place of
- * `coroutineScope`, and defaults to an immediate main-thread [Executor].
- */
- @JvmOverloads
- fun init(executor: Executor = CoroutineScope(Dispatchers.Main.immediate).asExecutor()) {
- evaluator.init(CoroutineScope(executor.asCoroutineDispatcher()))
- evaluator.data
- .filterNotNull()
- .onEach(listener::accept)
- .launchIn(CoroutineScope(executor.asCoroutineDispatcher()))
- }
-
- /** @see ComplicationDataExpressionEvaluator.close */
- override fun close() {
- evaluator.close()
}
}
- private val _data = MutableStateFlow<WireComplicationData?>(null)
-
- /**
- * The evaluated data, or `null` if it wasn't evaluated yet, or [NoDataComplicationData] if it
- * wasn't possible to evaluate the [unevaluatedData].
- */
- val data: StateFlow<WireComplicationData?> = _data.asStateFlow()
-
- @Volatile // In case init() and close() are called on different threads.
- private lateinit var evaluator: DynamicTypeEvaluator
- private val state = MutableStateFlow(State(unevaluatedData))
-
- /**
- * Parses the expression and starts blocking evaluation.
- *
- * This needs to be called exactly once.
- *
- * @param coroutineScope used for background evaluation, must be single threaded
- */
- fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)) {
- // Add all the receivers before we start binding them because binding can synchronously
- // trigger the receiver, which would update the data before all the fields are evaluated.
- Initializer(coroutineScope).init()
- }
-
- /**
- * Stops evaluation.
- *
- * [data] will not change after this is called.
- */
- override fun close() {
- for (receiver in state.value.all) receiver.close()
- if (this::evaluator.isInitialized) evaluator.close()
- }
-
- private inner class Initializer(val coroutineScope: CoroutineScope) {
- fun init() {
- initStateReceivers()
- initEvaluator()
- monitorState()
- }
-
- /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
- private fun initStateReceivers() {
- val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
-
- if (unevaluatedData.hasRangedValueExpression()) {
- unevaluatedData.rangedValueExpression
- ?.buildReceiver(
- expressionTrimmer = { setRangedValueExpression(null) },
- setter = { setRangedValue(it) },
- )
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasLongText()) {
- unevaluatedData.longText?.buildReceiver { setLongText(it) }?.let { receivers += it }
- }
- if (unevaluatedData.hasLongTitle()) {
- unevaluatedData.longTitle
- ?.buildReceiver { setLongTitle(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasShortText()) {
- unevaluatedData.shortText
- ?.buildReceiver { setShortText(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasShortTitle()) {
- unevaluatedData.shortTitle
- ?.buildReceiver { setShortTitle(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasContentDescription()) {
- unevaluatedData.contentDescription
- ?.buildReceiver { setContentDescription(it) }
- ?.let { receivers += it }
- }
-
- state.value = State(unevaluatedData, receivers)
- }
-
- private fun DynamicFloat.buildReceiver(
- expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
- setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
- ) =
- ComplicationEvaluationResultReceiver(
- setter = {
- if (!keepExpression) expressionTrimmer(this)
- setter(this, it)
- },
- binder = { receiver ->
- evaluator.bind(this@buildReceiver, coroutineScope.asExecutor(), receiver)
- },
- )
-
- private fun WireComplicationText.buildReceiver(
- setter:
- WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
- ) =
- expression?.let { expression ->
+ private suspend fun MutableStateFlow<State>.addReceiver(
+ text: WireComplicationText?,
+ setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
+ ) {
+ val expression = text?.expression ?: return
+ val executor = currentCoroutineContext().asExecutor()
+ update {
+ it.withPendingReceiver(
ComplicationEvaluationResultReceiver<String>(
- setter = {
+ this,
+ setter = { value ->
setter(
if (keepExpression) {
- WireComplicationText(it, expression)
+ WireComplicationText(value, expression)
} else {
- WireComplicationText(it)
+ WireComplicationText(value)
}
)
},
binder = { receiver ->
- evaluator.bind(
- expression,
- ULocale.getDefault(),
- coroutineScope.asExecutor(),
- receiver
- )
+ value.evaluator.bind(expression, ULocale.getDefault(), executor, receiver)
},
)
- }
-
- /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
- private fun initEvaluator() {
- if (state.value.pending.isEmpty()) return
- evaluator =
- DynamicTypeEvaluator(
- /* platformDataSourcesInitiallyEnabled = */ true,
- sensorGateway,
- stateStore,
- )
- for (receiver in state.value.pending) receiver.bind()
- for (receiver in state.value.pending) receiver.startEvaluation()
- evaluator.enablePlatformDataSources()
- }
-
- /** Monitors [state] changes and updates [data]. */
- private fun monitorState() {
- state
- .onEach {
- if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
- else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
- }
- .launchIn(coroutineScope)
+ )
}
}
@@ -261,14 +152,14 @@
* Holds the state of the continuously evaluated [WireComplicationData] and the various
* [ComplicationEvaluationResultReceiver] that are evaluating it.
*/
- private class State(
+ private inner class State(
val data: WireComplicationData,
- val pending: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
- val invalid: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
- val complete: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+ val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+ val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+ val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
val preUpdateCount: Int = 0,
- ) {
- val all = pending + invalid + complete
+ ) : AutoCloseable {
+ lateinit var evaluator: DynamicTypeEvaluator
init {
require(preUpdateCount >= 0) {
@@ -276,38 +167,78 @@
}
}
- fun withPreUpdate() =
- State(
- data,
- pending = pending,
- invalid = invalid,
- complete = complete,
- preUpdateCount + 1,
+ fun withPendingReceiver(receiver: ComplicationEvaluationResultReceiver<out Any>) =
+ copy(pendingReceivers = pendingReceivers + receiver)
+
+ fun withPreUpdate() = copy(preUpdateCount = preUpdateCount + 1)
+
+ fun withInvalidReceiver(receiver: ComplicationEvaluationResultReceiver<out Any>) =
+ copy(
+ pendingReceivers = pendingReceivers - receiver,
+ invalidReceivers = invalidReceivers + receiver,
+ completeReceivers = completeReceivers - receiver,
+ preUpdateCount = preUpdateCount - 1,
)
- fun withInvalid(receiver: ComplicationEvaluationResultReceiver<out Any>) =
- State(
- data,
- pending = pending - receiver,
- invalid = invalid + receiver,
- complete = complete - receiver,
- preUpdateCount - 1,
- )
-
- fun withUpdate(
+ fun withUpdatedData(
data: WireComplicationData,
receiver: ComplicationEvaluationResultReceiver<out Any>,
) =
- State(
+ copy(
data,
- pending = pending - receiver,
- invalid = invalid - receiver,
- complete = complete + receiver,
- preUpdateCount - 1,
+ pendingReceivers = pendingReceivers - receiver,
+ invalidReceivers = invalidReceivers - receiver,
+ completeReceivers = completeReceivers + receiver,
+ preUpdateCount = preUpdateCount - 1,
+ )
+
+ /**
+ * Initializes the internal [DynamicTypeEvaluator] if there are pending receivers.
+ *
+ * Should be called after all receivers were added.
+ */
+ fun initEvaluation() {
+ if (pendingReceivers.isEmpty()) return
+ require(!this::evaluator.isInitialized) { "initEvaluator must be called exactly once." }
+ evaluator =
+ DynamicTypeEvaluator(
+ /* platformDataSourcesInitiallyEnabled = */ true,
+ stateStore,
+ sensorGateway,
+ )
+ for (receiver in pendingReceivers) receiver.bind()
+ for (receiver in pendingReceivers) receiver.startEvaluation()
+ evaluator.enablePlatformDataSources()
+ }
+
+ override fun close() {
+ for (receiver in pendingReceivers + invalidReceivers + completeReceivers) {
+ receiver.close()
+ }
+ if (this::evaluator.isInitialized) evaluator.close()
+ }
+
+ private fun copy(
+ data: WireComplicationData = this.data,
+ pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+ this.pendingReceivers,
+ invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+ this.invalidReceivers,
+ completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+ this.completeReceivers,
+ preUpdateCount: Int = this.preUpdateCount,
+ ) =
+ State(
+ data = data,
+ pendingReceivers = pendingReceivers,
+ invalidReceivers = invalidReceivers,
+ completeReceivers = completeReceivers,
+ preUpdateCount = preUpdateCount,
)
}
private inner class ComplicationEvaluationResultReceiver<T : Any>(
+ private val state: MutableStateFlow<State>,
private val setter: WireComplicationData.Builder.(T) -> WireComplicationData.Builder,
private val binder: (ComplicationEvaluationResultReceiver<T>) -> BoundDynamicType,
) : DynamicTypeValueReceiver<T>, AutoCloseable {
@@ -332,12 +263,15 @@
override fun onData(newData: T) {
state.update {
- it.withUpdate(setter(WireComplicationData.Builder(it.data), newData).build(), this)
+ it.withUpdatedData(
+ setter(WireComplicationData.Builder(it.data), newData).build(),
+ this
+ )
}
}
override fun onInvalidated() {
- state.update { it.withInvalid(this) }
+ state.update { it.withInvalidReceiver(this) }
}
}
@@ -360,10 +294,10 @@
* Replacement for CoroutineDispatcher.asExecutor extension due to
* https://github.com/Kotlin/kotlinx.coroutines/pull/3683.
*/
-internal fun CoroutineScope.asExecutor() = Executor { runnable ->
- val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
- if (dispatcher.isDispatchNeeded(coroutineContext)) {
- dispatcher.dispatch(coroutineContext, runnable)
+internal fun CoroutineContext.asExecutor() = Executor { runnable ->
+ val dispatcher = this[ContinuationInterceptor] as CoroutineDispatcher
+ if (dispatcher.isDispatchNeeded(this)) {
+ dispatcher.dispatch(this, runnable)
} else {
runnable.run()
}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 1ee050f..dbb6d56 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -16,12 +16,9 @@
package androidx.wear.watchface.complications.data
-import android.os.Handler
-import android.os.Looper
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.util.Log
-import androidx.core.util.Consumer
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue
@@ -30,48 +27,33 @@
import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
import org.robolectric.shadows.ShadowLog
@RunWith(SharedRobolectricTestRunner::class)
class ComplicationDataExpressionEvaluatorTest {
@get:Rule val expect = Expect.create()
- private val listener = mock<Consumer<WireComplicationData>>()
-
@Before
fun setup() {
ShadowLog.setLoggable("ComplicationData", Log.DEBUG)
}
@Test
- fun data_notInitialized_setToNull() {
- ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
- assertThat(evaluator.data.value).isNull()
- }
- }
+ fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
+ val evaluator = ComplicationDataExpressionEvaluator()
- @Test
- fun data_noExpression_setToUnevaluated() {
- ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
- evaluator.init()
-
- assertThat(evaluator.data.value).isEqualTo(DATA_WITH_NO_EXPRESSION)
- }
+ assertThat(evaluator.evaluate(DATA_WITH_NO_EXPRESSION).firstOrNull())
+ .isEqualTo(DATA_WITH_NO_EXPRESSION)
}
/**
@@ -208,47 +190,34 @@
}
@Test
- fun data_withExpression_setToEvaluated() {
+ fun evaluate_withExpression_returnsEvaluated() = runBlocking {
for (scenario in DataWithExpressionScenario.values()) {
// Defensive copy due to in-place evaluation.
val expressed = WireComplicationData.Builder(scenario.expressed).build()
val stateStore = ObservableStateStore(mapOf())
- ComplicationDataExpressionEvaluator(expressed, stateStore).use { evaluator ->
- val allEvaluations =
- evaluator.data
- .filterNotNull()
- .shareIn(
- CoroutineScope(Dispatchers.Main.immediate),
- SharingStarted.Eagerly,
- replay = 10,
- )
- evaluator.init()
+ val evaluator = ComplicationDataExpressionEvaluator(stateStore)
+ val allEvaluations =
+ evaluator
+ .evaluate(expressed)
+ .shareIn(
+ CoroutineScope(Dispatchers.Main.immediate),
+ SharingStarted.Eagerly,
+ replay = 10,
+ )
- for (state in scenario.states) {
- stateStore.setStateEntryValues(state)
- }
-
- expect
- .withMessage(scenario.name)
- .that(allEvaluations.replayCache)
- .isEqualTo(scenario.evaluated)
+ for (state in scenario.states) {
+ stateStore.setStateEntryValues(state)
}
+
+ expect
+ .withMessage(scenario.name)
+ .that(allEvaluations.replayCache)
+ .isEqualTo(scenario.evaluated)
}
}
@Test
- fun data_keepExpression_doesNotTrimUnevaluatedExpression() {
- class MainExecutor : Executor {
- private val handler = Handler(Looper.getMainLooper())
-
- override fun execute(runnable: Runnable) {
- if (handler.looper == Looper.myLooper()) {
- runnable.run()
- } else {
- handler.post(runnable)
- }
- }
- }
+ fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
val expressed =
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
@@ -258,38 +227,30 @@
.setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
.setContentDescription(WireComplicationText(DynamicString.constant("Description")))
.build()
- ComplicationDataExpressionEvaluator(expressed, keepExpression = true).use { evaluator ->
- evaluator.init()
+ val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
- assertThat(evaluator.data.value)
- .isEqualTo(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
- .setRangedValue(1f)
- .setRangedValueExpression(DynamicFloat.constant(1f))
- .setLongText(
- WireComplicationText("Long Text", DynamicString.constant("Long Text"))
- )
- .setLongTitle(
- WireComplicationText("Long Title", DynamicString.constant("Long Title"))
- )
- .setShortText(
- WireComplicationText("Short Text", DynamicString.constant("Short Text"))
- )
- .setShortTitle(
- WireComplicationText(
- "Short Title",
- DynamicString.constant("Short Title")
- )
- )
- .setContentDescription(
- WireComplicationText(
- "Description",
- DynamicString.constant("Description")
- )
- )
- .build()
- )
- }
+ assertThat(evaluator.evaluate(expressed).firstOrNull())
+ .isEqualTo(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValue(1f)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .setLongText(
+ WireComplicationText("Long Text", DynamicString.constant("Long Text"))
+ )
+ .setLongTitle(
+ WireComplicationText("Long Title", DynamicString.constant("Long Title"))
+ )
+ .setShortText(
+ WireComplicationText("Short Text", DynamicString.constant("Short Text"))
+ )
+ .setShortTitle(
+ WireComplicationText("Short Title", DynamicString.constant("Short Title"))
+ )
+ .setContentDescription(
+ WireComplicationText("Description", DynamicString.constant("Description"))
+ )
+ .build()
+ )
}
enum class HasExpressionDataWithExpressionScenario(val data: WireComplicationData) {
@@ -337,43 +298,6 @@
assertThat(hasExpression(DATA_WITH_NO_EXPRESSION)).isFalse()
}
- @Test
- fun compat_notInitialized_listenerNotInvoked() {
- ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
- .build()
- .use { verify(listener, never()).accept(any()) }
- }
-
- @Test
- fun compat_noExpression_listenerInvokedWithData() {
- ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
- .build()
- .use { evaluator ->
- evaluator.init()
-
- verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
- }
- }
-
- @Test
- fun compat_expression_listenerInvokedWithEvaluatedData() {
- val data =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
- .setRangedValueExpression(DynamicFloat.constant(1f))
- .build()
- ComplicationDataExpressionEvaluator.Compat.Builder(data, listener).build().use { evaluator
- ->
- evaluator.init()
-
- verify(listener, times(1))
- .accept(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
- .setRangedValue(1f)
- .build()
- )
- }
- }
-
private companion object {
val DATA_WITH_NO_EXPRESSION =
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index a196c02..4122a7d 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1078,6 +1078,9 @@
iWatchFaceService = IWatchFaceService.Stub.asInterface(binder)
+ // A ParameterlessEngine doesn't exist in WSL flow.
+ InteractiveInstanceManager.setParameterlessEngine(null)
+
try {
// Note if the implementation doesn't support getVersion this will return zero
// rather than throwing an exception.
@@ -1346,7 +1349,7 @@
return
}
- val pendingWallpaperInstance =
+ var pendingWallpaperInstance =
InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
// In a direct boot scenario attempt to load the previously serialized parameters.
@@ -1376,16 +1379,32 @@
?.let {
Log.e(
TAG,
- "takePendingWallpaperInteractiveWatchFaceInstance failed"
+ "takePendingWallpaperInteractiveWatchFaceInstance failed",
+ e
)
it.callback.onInteractiveWatchFaceCrashed(CrashInfoParcel(e))
}
} finally {
asyncTraceEvent.close()
}
+
+ return
}
}
+ if (pendingWallpaperInstance == null) {
+ // In this case we don't have any watchface parameters, probably because a WSL
+ // watchface has been upgraded to an AndroidX one. The system has either just
+ // racily attempted to connect (in which case we should carry on normally) or it
+ // probably will connect at a later time. In the latter case we should
+ // register a parameterless engine to allow the subsequent connection to
+ // succeed.
+ pendingWallpaperInstance = InteractiveInstanceManager
+ .setParameterlessEngineOrTakePendingWallpaperInteractiveWatchFaceInstance(
+ this
+ )
+ }
+
// If there's a pending WallpaperInteractiveWatchFaceInstance then create it.
if (pendingWallpaperInstance != null) {
val asyncTraceEvent =
@@ -1402,7 +1421,7 @@
)
instance
} catch (e: Exception) {
- Log.e(TAG, "createInteractiveInstance failed")
+ Log.e(TAG, "createInteractiveInstance failed", e)
pendingWallpaperInstance.callback.onInteractiveWatchFaceCrashed(
CrashInfoParcel(e)
)
@@ -1434,6 +1453,29 @@
}
}
+ /** Attaches to a parameterlessEngine if we're completely uninitialized. */
+ @SuppressWarnings("NewApi")
+ internal fun attachToParameterlessEngine(
+ pendingWallpaperInstance:
+ InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance
+ ) {
+ uiThreadCoroutineScope.launch {
+ try {
+ pendingWallpaperInstance.callback.onInteractiveWatchFaceCreated(
+ createInteractiveInstance(
+ pendingWallpaperInstance.params,
+ "attachToParameterlessEngine"
+ )
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "attachToParameterlessEngine failed", e)
+ pendingWallpaperInstance.callback.onInteractiveWatchFaceCrashed(
+ CrashInfoParcel(e)
+ )
+ }
+ }
+ }
+
@UiThread
internal fun ambientTickUpdate(): Unit =
TraceEvent("EngineWrapper.ambientTickUpdate").use {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
index bcbafd1..1dbbc9a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
@@ -20,6 +20,7 @@
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.wear.watchface.IndentingPrintWriter
+import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.utility.TraceEvent
@@ -53,6 +54,7 @@
private var pendingWallpaperInteractiveWatchFaceInstance:
PendingWallpaperInteractiveWatchFaceInstance? =
null
+ private var parameterlessEngine: WatchFaceService.EngineWrapper? = null
@VisibleForTesting
fun getInstances() =
@@ -70,6 +72,54 @@
}
}
+ /**
+ * We either return the pendingWallpaperInteractiveWatchFaceInstance if there is one or
+ * set parameterlessEngine. A parameterless engine, is one that's been created without any
+ * start up params. Typically this can only happen if a WSL watchface is upgraded to an
+ * androidx one, so WallpaperManager knows about it but WearServices/WSL does not.
+ */
+ @SuppressLint("SyntheticAccessor")
+ fun setParameterlessEngineOrTakePendingWallpaperInteractiveWatchFaceInstance(
+ parameterlessEngine: WatchFaceService.EngineWrapper?
+ ): PendingWallpaperInteractiveWatchFaceInstance? {
+ synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+ require(this.parameterlessEngine == null || parameterlessEngine == null) {
+ "Already have a parameterlessEngine registered"
+ }
+
+ if (pendingWallpaperInteractiveWatchFaceInstance == null) {
+ this.parameterlessEngine = parameterlessEngine
+ return null
+ } else {
+ val returnValue = pendingWallpaperInteractiveWatchFaceInstance
+ pendingWallpaperInteractiveWatchFaceInstance = null
+ return returnValue
+ }
+ }
+ }
+
+ /**
+ * A parameterless engine, is one that's been created without any start up params. Typically
+ * this can only happen if a WSL watchface is upgraded to an androidx one, so
+ * WallpaperManager knows about it but WearServices/WSL does not.
+ */
+ @SuppressLint("SyntheticAccessor")
+ fun setParameterlessEngine(parameterlessEngine: WatchFaceService.EngineWrapper?) {
+ synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+ require(this.parameterlessEngine == null || parameterlessEngine == null) {
+ "Already have a parameterlessEngine registered"
+ }
+ this.parameterlessEngine = parameterlessEngine
+ }
+ }
+
+ @SuppressLint("SyntheticAccessor")
+ fun getParameterlessEngine(): WatchFaceService.EngineWrapper? {
+ synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+ return parameterlessEngine
+ }
+ }
+
@SuppressLint("SyntheticAccessor")
fun getAndRetainInstance(instanceId: String): InteractiveWatchFaceImpl? {
synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
@@ -121,12 +171,22 @@
val impl =
synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
val instance = instances[value.params.instanceId]
+
if (instance == null) {
+ parameterlessEngine?.let {
+ parameterlessEngine = null
+ it.attachToParameterlessEngine(value)
+ return null
+ }
+
TraceEvent("Set pendingWallpaperInteractiveWatchFaceInstance").use {
pendingWallpaperInteractiveWatchFaceInstance = value
}
return null
}
+ if (instance.impl.engine == parameterlessEngine) {
+ parameterlessEngine = null
+ }
instance.impl
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index a48ba9c..01c3acb 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -183,6 +183,7 @@
@After
fun tearDown() {
+ assertThat(InteractiveInstanceManager.getParameterlessEngine()).isNull()
InteractiveInstanceManager.releaseInstance(initParams.instanceId)
assertThat(InteractiveInstanceManager.getInstances()).isEmpty()
}
@@ -228,6 +229,9 @@
runPostedTasksFor(0)
assertThat(pendingException.message).startsWith("WatchFace already exists!")
+
+ // Tidy up.
+ InteractiveInstanceManager.setParameterlessEngine(null)
}
@Test
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 140549e..d6870d7 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -751,6 +751,7 @@
}
assertThat(InteractiveInstanceManager.getInstances()).isEmpty()
+ assertThat(InteractiveInstanceManager.getParameterlessEngine()).isNull()
validateMockitoUsage()
}
@@ -3756,6 +3757,21 @@
@Config(sdk = [Build.VERSION_CODES.R])
public fun directBoot() {
val instanceId = "DirectBootInstance"
+ val params = WallpaperInteractiveWatchFaceInstanceParams(
+ instanceId,
+ DeviceConfig(false, false, 0, 0),
+ WatchUiState(false, 0),
+ UserStyle(
+ hashMapOf(
+ colorStyleSetting to blueStyleOption,
+ watchHandStyleSetting to gothicStyleOption
+ )
+ )
+ .toWireFormat(),
+ null,
+ null,
+ null
+ )
testWatchFaceService =
TestWatchFaceService(
WatchFaceType.ANALOG,
@@ -3772,21 +3788,7 @@
watchState,
handler,
null,
- WallpaperInteractiveWatchFaceInstanceParams(
- instanceId,
- DeviceConfig(false, false, 0, 0),
- WatchUiState(false, 0),
- UserStyle(
- hashMapOf(
- colorStyleSetting to blueStyleOption,
- watchHandStyleSetting to gothicStyleOption
- )
- )
- .toWireFormat(),
- null,
- null,
- null
- ),
+ params,
choreographer
)
@@ -3794,15 +3796,34 @@
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
// This increments refcount to 2
- val instance = InteractiveInstanceManager.getAndRetainInstance(instanceId)
+ val instance =
+ InteractiveInstanceManager
+ .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
+ InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
+ params,
+ object : IPendingInteractiveWatchFace.Stub() {
+ override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
+ ) {
+ fail("Shouldn't get called")
+ }
+
+ override fun onInteractiveWatchFaceCrashed(
+ exception: CrashInfoParcel?
+ ) {
+ fail("WatchFace crashed: $exception")
+ }
+ }
+ )
+ )
assertThat(instance).isNotNull()
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
val userStyle = watchFaceImpl.currentUserStyleRepository.userStyle.value
assertThat(userStyle[colorStyleSetting]).isEqualTo(blueStyleOption)
assertThat(userStyle[watchHandStyleSetting]).isEqualTo(gothicStyleOption)
instance!!.release()
-
- InteractiveInstanceManager.releaseInstance(instanceId)
}
@Test
@@ -6523,6 +6544,78 @@
assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
}
+ @Test
+ public fun attachToExistingParameterlessEngine() {
+ // Construct a parameterless engine.
+ testWatchFaceService =
+ TestWatchFaceService(
+ WatchFaceType.DIGITAL,
+ emptyList(),
+ { _, currentUserStyleRepository, watchState ->
+ renderer =
+ TestRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ INTERACTIVE_UPDATE_RATE_MS
+ )
+ renderer
+ },
+ UserStyleSchema(emptyList()),
+ watchState,
+ handler,
+ null,
+ null,
+ choreographer,
+ mockSystemTimeMillis = looperTimeMillis,
+ complicationCache = null
+ )
+
+ engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onCreate(surfaceHolder)
+ engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
+ engineWrapper.onVisibilityChanged(true)
+
+ val callback = object : IPendingInteractiveWatchFace.Stub() {
+ override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
+ ) {
+ interactiveWatchFaceInstance = iInteractiveWatchFace
+ }
+
+ override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel?) {
+ fail("WatchFace crashed: $exception")
+ }
+ }
+
+ InteractiveInstanceManager
+ .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
+ InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(false, false, 0, 0),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null,
+ null
+ ),
+ callback
+ )
+ )
+
+ runBlocking {
+ watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
+ engineWrapper.deferredValidation.awaitWithTimeout()
+ }
+
+ assertThat(this::interactiveWatchFaceInstance.isInitialized).isTrue()
+ assertThat(watchFaceImpl.renderer.watchState.watchFaceInstanceId.value)
+ .isEqualTo(INTERACTIVE_INSTANCE_ID)
+ }
+
private fun getLeftShortTextComplicationDataText(): CharSequence {
val complication =
complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value