Merge "Shadow / jarjar kotlinx-metadata-jvm in room-compiler-processing." into androidx-main
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
new file mode 100644
index 0000000..d97aee4
--- /dev/null
+++ b/activity/activity/lint-baseline.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/activity/ComponentActivity.java"/>
+ </issue>
+
+</issues>
diff --git a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
index f3cb73c..8fdbdc0 100644
--- a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
+++ b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
@@ -34,7 +34,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME: String = "actions.intent.CREATE_CALL"
@@ -43,7 +42,7 @@
class CreateCall private constructor() {
internal enum class PropertyMapStrings(val key: String) {
CALL_FORMAT("call.callFormat"),
- PARTICIPANT("call.participant"),
+ PARTICIPANT("call.participant")
}
class CapabilityBuilder :
@@ -67,7 +66,7 @@
class Arguments
internal constructor(
val callFormat: Call.CanonicalValue.CallFormat?,
- val participantList: List<ParticipantValue>,
+ val participantList: List<ParticipantValue>
) {
override fun toString(): String {
return "Arguments(callFormat=$callFormat, participantList=$participantList)"
@@ -183,13 +182,11 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"call.callFormat",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.CALL_FORMAT.key]
- as Property<Call.CanonicalValue.CallFormat>
- )
+ properties[PropertyMapStrings.CALL_FORMAT.key]
+ as? Property<Call.CanonicalValue.CallFormat>
},
Arguments.Builder::setCallFormat,
TypeConverters.CALL_FORMAT_PARAM_VALUE_CONVERTER,
@@ -198,21 +195,18 @@
.bindRepeatedParameter(
"call.participant",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.PARTICIPANT.key]
- as Property<Participant>
- )
+ properties[PropertyMapStrings.PARTICIPANT.key] as? Property<Participant>
},
Arguments.Builder::setParticipantList,
ParticipantValue.PARAM_VALUE_CONVERTER,
EntityConverter.of(PARTICIPANT_TYPE_SPEC)
)
- .bindOptionalOutput(
+ .bindOutput(
"call",
Output::call,
ParamValueConverter.of(CALL_TYPE_SPEC)::toParamValue
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
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 f7318ef..b559a71 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
@@ -35,7 +35,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME: String = "actions.intent.CREATE_MESSAGE"
@@ -44,7 +43,7 @@
class CreateMessage private constructor() {
internal enum class PropertyMapStrings(val key: String) {
MESSAGE_TEXT("message.text"),
- RECIPIENT("message.recipient"),
+ RECIPIENT("message.recipient")
}
class CapabilityBuilder :
@@ -188,33 +187,27 @@
.bindRepeatedParameter(
"message.recipient",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.RECIPIENT.key]
- as Property<Recipient>
- )
+ properties[PropertyMapStrings.RECIPIENT.key] as? Property<Recipient>
},
Arguments.Builder::setRecipientList,
RecipientValue.PARAM_VALUE_CONVERTER,
EntityConverter.of(RECIPIENT_TYPE_SPEC)
)
- .bindOptionalParameter(
+ .bindParameter(
"message.text",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.MESSAGE_TEXT.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.MESSAGE_TEXT.key] as? Property<StringValue>
},
Arguments.Builder::setMessageText,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"message",
Output::message,
ParamValueConverter.of(MESSAGE_TYPE_SPEC)::toParamValue
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
diff --git a/appactions/interaction/interaction-capabilities-core/api/api_lint.ignore b/appactions/interaction/interaction-capabilities-core/api/api_lint.ignore
deleted file mode 100644
index af5cd99..0000000
--- a/appactions/interaction/interaction-capabilities-core/api/api_lint.ignore
+++ /dev/null
@@ -1,15 +0,0 @@
-// Baseline format: 1.0
-BuilderSetStyle: androidx.appactions.interaction.capabilities.core.Capability.Builder#asBuilder():
- Builder methods names should use setFoo() / addFoo() / clearFoo() style: method androidx.appactions.interaction.capabilities.core.Capability.Builder.asBuilder()
-
-
-MissingGetterMatchingBuilder: androidx.appactions.interaction.capabilities.core.Capability.Builder#setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallback<ArgumentsT,OutputT>):
- androidx.appactions.interaction.capabilities.core.Capability does not declare a `getExecutionCallback()` method matching method androidx.appactions.interaction.capabilities.core.Capability.Builder.setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallback<ArgumentsT,OutputT>)
-MissingGetterMatchingBuilder: androidx.appactions.interaction.capabilities.core.Capability.Builder#setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallbackAsync<ArgumentsT,OutputT>):
- androidx.appactions.interaction.capabilities.core.Capability does not declare a `getExecutionCallback()` method matching method androidx.appactions.interaction.capabilities.core.Capability.Builder.setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallbackAsync<ArgumentsT,OutputT>)
-MissingGetterMatchingBuilder: androidx.appactions.interaction.capabilities.core.Capability.Builder#setExecutionSessionFactory(kotlin.jvm.functions.Function1<? super androidx.appactions.interaction.capabilities.core.HostProperties,? extends ExecutionSessionT>):
- androidx.appactions.interaction.capabilities.core.Capability does not declare a `getExecutionSessionFactory()` method matching method androidx.appactions.interaction.capabilities.core.Capability.Builder.setExecutionSessionFactory(kotlin.jvm.functions.Function1<? super androidx.appactions.interaction.capabilities.core.HostProperties,? extends ExecutionSessionT>)
-
-
-StaticFinalBuilder: androidx.appactions.interaction.capabilities.core.Capability.Builder:
- Builder must be final: androidx.appactions.interaction.capabilities.core.Capability.Builder
diff --git a/appactions/interaction/interaction-capabilities-core/api/current.txt b/appactions/interaction/interaction-capabilities-core/api/current.txt
index b2e08cf..4045e23 100644
--- a/appactions/interaction/interaction-capabilities-core/api/current.txt
+++ b/appactions/interaction/interaction-capabilities-core/api/current.txt
@@ -14,7 +14,6 @@
}
public abstract static class Capability.Builder<BuilderT extends androidx.appactions.interaction.capabilities.core.Capability.Builder<BuilderT, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT>, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT extends androidx.appactions.interaction.capabilities.core.BaseExecutionSession<ArgumentsT, OutputT>> {
- method public final BuilderT asBuilder();
method public androidx.appactions.interaction.capabilities.core.Capability build();
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallback<ArgumentsT,OutputT> executionCallback);
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallbackAsync<ArgumentsT,OutputT> executionCallbackAsync);
diff --git a/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
index b2e08cf..4045e23 100644
--- a/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
+++ b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
@@ -14,7 +14,6 @@
}
public abstract static class Capability.Builder<BuilderT extends androidx.appactions.interaction.capabilities.core.Capability.Builder<BuilderT, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT>, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT extends androidx.appactions.interaction.capabilities.core.BaseExecutionSession<ArgumentsT, OutputT>> {
- method public final BuilderT asBuilder();
method public androidx.appactions.interaction.capabilities.core.Capability build();
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallback<ArgumentsT,OutputT> executionCallback);
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallbackAsync<ArgumentsT,OutputT> executionCallbackAsync);
diff --git a/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
index b2e08cf..4045e23 100644
--- a/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
+++ b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
@@ -14,7 +14,6 @@
}
public abstract static class Capability.Builder<BuilderT extends androidx.appactions.interaction.capabilities.core.Capability.Builder<BuilderT, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT>, ArgumentsT, OutputT, ConfirmationT, ExecutionSessionT extends androidx.appactions.interaction.capabilities.core.BaseExecutionSession<ArgumentsT, OutputT>> {
- method public final BuilderT asBuilder();
method public androidx.appactions.interaction.capabilities.core.Capability build();
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallback<ArgumentsT,OutputT> executionCallback);
method public final BuilderT setExecutionCallback(androidx.appactions.interaction.capabilities.core.ExecutionCallbackAsync<ArgumentsT,OutputT> executionCallbackAsync);
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
index 6fc55f2..a720034 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
@@ -16,6 +16,7 @@
package androidx.appactions.interaction.capabilities.core
+import android.annotation.SuppressLint
import androidx.annotation.RestrictTo
import androidx.appactions.interaction.capabilities.core.impl.CapabilitySession
import androidx.appactions.interaction.capabilities.core.impl.SingleTurnCapabilityImpl
@@ -58,6 +59,7 @@
/**
* An abstract Builder class for Capability.
*/
+ @SuppressLint("StaticFinalBuilder")
abstract class Builder<
BuilderT :
Builder<
@@ -76,7 +78,7 @@
private var property: Map<String, Property<*>>? = null
private var executionCallback: ExecutionCallback<ArgumentsT, OutputT>? = null
private var sessionFactory:
- (hostProperties: HostProperties?) -> ExecutionSessionT? = { _ -> null }
+ (hostProperties: HostProperties?) -> ExecutionSessionT? = { _ -> null }
private var actionSpec: ActionSpec<ArgumentsT, OutputT>? = null
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -91,10 +93,14 @@
* @suppress
*/
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- protected open val sessionBridge: SessionBridge<ExecutionSessionT, ConfirmationT>? = null
+ protected open val sessionBridge: SessionBridge<
+ ExecutionSessionT,
+ ArgumentsT,
+ ConfirmationT
+ >? = null
@Suppress("UNCHECKED_CAST")
- fun asBuilder(): BuilderT {
+ private fun asBuilder(): BuilderT {
return this as BuilderT
}
@@ -123,6 +129,7 @@
* This method accepts a coroutine-based ExecutionCallback instance. There is also an
* overload which accepts the ExecutionCallbackAsync instead.
*/
+ @SuppressLint("MissingGetterMatchingBuilder")
fun setExecutionCallback(executionCallback: ExecutionCallback<ArgumentsT, OutputT>) =
asBuilder().apply {
this.executionCallback = executionCallback
@@ -137,6 +144,7 @@
* This method accepts the ExecutionCallbackAsync interface which returns a
* [ListenableFuture].
*/
+ @SuppressLint("MissingGetterMatchingBuilder")
fun setExecutionCallback(
executionCallbackAsync: ExecutionCallbackAsync<ArgumentsT, OutputT>
) = asBuilder().apply {
@@ -144,12 +152,13 @@
}
/**
- * Sets the lambda used to create [ExecutionSession] instances for this
+ * Sets the lambda used to create [ExecutionSessionT] instances for this
* capability.
*
* [setExecutionSessionFactory] and [setExecutionCallback] are mutually exclusive, so
* calling one will nullify the other.
*/
+ @SuppressLint("MissingGetterMatchingBuilder")
open fun setExecutionSessionFactory(
sessionFactory: (hostProperties: HostProperties?) -> ExecutionSessionT
): BuilderT = asBuilder().apply {
@@ -168,15 +177,11 @@
executionCallback!!
)
} else {
- val checkedSessionFactory = requireNotNull(sessionFactory) {
- "either setExecutionCallback or setExecutionSessionFactory" +
- " must be called before build"
- }
return TaskCapabilityImpl(
checkedId,
actionSpec!!,
checkedProperty,
- checkedSessionFactory,
+ sessionFactory,
sessionBridge!!,
::EmptyTaskUpdater
)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.kt
index 2e97ad9..9ca198c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.kt
@@ -17,20 +17,19 @@
package androidx.appactions.interaction.capabilities.core.impl.converters
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
import java.util.function.Function
internal data class FieldBinding<T, BuilderT> constructor(
val name: String,
- val valueGetter: Function<T, Optional<Value>>,
- val valueSetter: CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>>
+ val valueGetter: Function<T, Value?>,
+ val valueSetter: CheckedInterfaces.BiConsumer<BuilderT, Value>
) {
companion object {
@JvmStatic
fun <T, BuilderT> create(
name: String,
- valueGetter: Function<T, Optional<Value>>,
- valueSetter: CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>>
+ valueGetter: Function<T, Value?>,
+ valueSetter: CheckedInterfaces.BiConsumer<BuilderT, Value>
): FieldBinding<T, BuilderT> {
return FieldBinding(name, valueGetter, valueSetter)
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
index efacbfb4..a963bf6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
@@ -18,9 +18,13 @@
import androidx.annotation.NonNull;
import androidx.appactions.builtintypes.experimental.properties.Attendee;
+import androidx.appactions.builtintypes.experimental.properties.EndDate;
import androidx.appactions.builtintypes.experimental.properties.ItemListElement;
+import androidx.appactions.builtintypes.experimental.properties.Name;
import androidx.appactions.builtintypes.experimental.properties.Participant;
import androidx.appactions.builtintypes.experimental.properties.Recipient;
+import androidx.appactions.builtintypes.experimental.properties.StartDate;
+import androidx.appactions.builtintypes.experimental.properties.Text;
import androidx.appactions.builtintypes.experimental.types.Alarm;
import androidx.appactions.builtintypes.experimental.types.CalendarEvent;
import androidx.appactions.builtintypes.experimental.types.Call;
@@ -73,20 +77,17 @@
.build();
public static final TypeSpec<Person> PERSON_TYPE_SPEC =
- TypeSpecBuilder.newBuilderForThing(
- "Person",
- Person::Builder,
- Person.Builder::build)
- .bindStringField("email",
- person -> Optional.ofNullable(person.getEmail()),
- Person.Builder::setEmail)
+ TypeSpecBuilder.newBuilderForThing("Person", Person::Builder, Person.Builder::build)
+ .bindStringField("email", Person::getEmail, Person.Builder::setEmail)
.bindStringField(
- "telephone",
- person -> Optional.ofNullable(person.getTelephone()),
- Person.Builder::setTelephone)
- .bindStringField("name",
- person -> Optional.ofNullable(person.getName())
- .flatMap(name -> Optional.ofNullable(name.asText())),
+ "telephone", Person::getTelephone, Person.Builder::setTelephone)
+ .bindStringField(
+ "name",
+ person ->
+ Optional.ofNullable(person)
+ .map(Person::getName)
+ .map(Name::asText)
+ .orElse(null),
Person.Builder::setName)
.build();
public static final TypeSpec<Alarm> ALARM_TYPE_SPEC =
@@ -109,13 +110,19 @@
CalendarEvent.Builder::build)
.bindZonedDateTimeField(
"startDate",
- calendarEvent -> Optional.ofNullable(
- calendarEvent.getStartDate().asZonedDateTime()),
+ calendarEvent ->
+ Optional.ofNullable(calendarEvent)
+ .map(CalendarEvent::getStartDate)
+ .map(StartDate::asZonedDateTime)
+ .orElse(null),
CalendarEvent.Builder::setStartDate)
.bindZonedDateTimeField(
"endDate",
- calendarEvent -> Optional.ofNullable(
- calendarEvent.getEndDate().asZonedDateTime()),
+ calendarEvent ->
+ Optional.ofNullable(calendarEvent)
+ .map(CalendarEvent::getEndDate)
+ .map(EndDate::asZonedDateTime)
+ .orElse(null),
CalendarEvent.Builder::setEndDate)
.bindRepeatedSpecField(
"attendee",
@@ -130,11 +137,11 @@
SafetyCheck.Builder::build)
.bindDurationField(
"duration",
- safetyCheck -> Optional.ofNullable(safetyCheck.getDuration()),
+ SafetyCheck::getDuration,
SafetyCheck.Builder::setDuration)
.bindZonedDateTimeField(
"checkInTime",
- safetyCheck -> Optional.ofNullable(safetyCheck.getCheckInTime()),
+ SafetyCheck::getCheckInTime,
SafetyCheck.Builder::setCheckInTime)
.build();
public static final TypeSpec<Recipient> RECIPIENT_TYPE_SPEC =
@@ -152,11 +159,8 @@
PERSON_TYPE_SPEC)
.build();
public static final TypeSpec<Message> MESSAGE_TYPE_SPEC =
- TypeSpecBuilder.newBuilderForThing(
- "Message",
- Message::Builder,
- Message.Builder::build)
- .bindIdentifier(message -> Optional.ofNullable(message.getIdentifier()))
+ TypeSpecBuilder.newBuilderForThing("Message", Message::Builder, Message.Builder::build)
+ .bindIdentifier(Message::getIdentifier)
.bindRepeatedSpecField(
"recipient",
Message::getRecipientList,
@@ -164,7 +168,11 @@
RECIPIENT_TYPE_SPEC)
.bindStringField(
"text",
- message -> Optional.of(message.getText().asText()),
+ message ->
+ Optional.ofNullable(message)
+ .map(Message::getText)
+ .map(Text::asText)
+ .orElse(null),
Message.Builder::setText)
.build();
public static final TypeSpec<Call> CALL_TYPE_SPEC =
@@ -172,7 +180,7 @@
"Call",
Call::Builder,
Call.Builder::build)
- .bindIdentifier(call -> Optional.ofNullable(call.getIdentifier()))
+ .bindIdentifier(Call::getIdentifier)
.bindRepeatedSpecField(
"participant",
Call::getParticipantList,
@@ -368,11 +376,11 @@
SearchAction.Builder<T>::build)
.bindStringField(
"query",
- (searchAction) -> Optional.ofNullable(searchAction.getQuery()),
+ SearchAction::getQuery,
SearchAction.Builder<T>::setQuery)
.bindSpecField(
"filter",
- (searchAction) -> Optional.ofNullable(searchAction.getFilter()),
+ SearchAction::getFilter,
SearchAction.Builder<T>::setFilter,
nestedTypeSpec)
.build();
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
index a9bb491..21c30a6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
@@ -25,13 +25,12 @@
import androidx.appactions.interaction.protobuf.Value;
import java.time.Duration;
-import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.Optional;
+import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -42,7 +41,7 @@
private final Supplier<BuilderT> mBuilderSupplier;
private final Function<BuilderT, T> mBuilderFinalizer;
private CheckedInterfaces.Consumer<Struct> mStructValidator;
- private Function<T, Optional<String>> mIdentifierGetter = (unused) -> Optional.empty();
+ private Function<T, String> mIdentifierGetter = (unused) -> null;
private TypeSpecBuilder(
String typeName,
@@ -50,7 +49,7 @@
Function<BuilderT, T> builderFinalizer) {
this.mBuilderSupplier = builderSupplier;
this.mBuilderFinalizer = builderFinalizer;
- this.bindStringField("@type", (unused) -> Optional.of(typeName), (builder, val) -> {})
+ this.bindStringField("@type", (unused) -> typeName, (builder, val) -> {})
.setStructValidator(
struct -> {
if (!getFieldFromStruct(struct, "@type")
@@ -109,16 +108,16 @@
Supplier<BuilderT> builderSupplier,
Function<BuilderT, T> builderFinalizer) {
return new TypeSpecBuilder<>(typeName, builderSupplier, builderFinalizer)
- .bindIdentifier(thing -> Optional.ofNullable(thing.getIdentifier()))
- .bindStringField(
- "identifier",
- thing -> Optional.ofNullable(thing.getIdentifier()),
- BuilderT::setIdentifier)
+ .bindIdentifier(Thing::getIdentifier)
+ .bindStringField("identifier", Thing::getIdentifier, BuilderT::setIdentifier)
.bindStringField(
"name",
- thing ->
- Optional.ofNullable(thing.getName())
- .flatMap(name -> Optional.ofNullable(name.asText())),
+ thing -> {
+ if (thing.getName() == null) {
+ return null;
+ }
+ return thing.getName().asText();
+ },
BuilderT::setName);
}
@@ -128,15 +127,15 @@
return this;
}
- TypeSpecBuilder<T, BuilderT> bindIdentifier(Function<T, Optional<String>> identifierGetter) {
+ TypeSpecBuilder<T, BuilderT> bindIdentifier(Function<T, String> identifierGetter) {
this.mIdentifierGetter = identifierGetter;
return this;
}
private TypeSpecBuilder<T, BuilderT> bindFieldInternal(
String name,
- Function<T, Optional<Value>> valueGetter,
- CheckedInterfaces.BiConsumer<BuilderT, Optional<Value>> valueSetter) {
+ Function<T, Value> valueGetter,
+ CheckedInterfaces.BiConsumer<BuilderT, Value> valueSetter) {
mBindings.add(FieldBinding.create(name, valueGetter, valueSetter));
return this;
}
@@ -145,30 +144,28 @@
String name,
Function<T, List<V>> valueGetter,
BiConsumer<BuilderT, List<V>> valueSetter,
- Function<V, Optional<Value>> toValue,
+ Function<V, Value> toValue,
CheckedInterfaces.Function<Value, V> fromValue) {
return bindFieldInternal(
name,
/** valueGetter= */
object -> {
- if (valueGetter.apply(object).isEmpty()) {
- return Optional.empty();
+ List<V> valueList = valueGetter.apply(object);
+ if (valueList == null) {
+ return null;
}
- return Optional.of(
- getListValue(
- valueGetter.apply(object).stream()
- .map(toValue)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(toImmutableList())));
+ return getListValue(
+ valueList.stream()
+ .map(toValue)
+ .filter(Objects::nonNull)
+ .collect(toImmutableList()));
},
/** valueSetter= */
(builder, repeatedValue) -> {
- List<Value> values =
- repeatedValue
- .map(Value::getListValue)
- .map(ListValue::getValuesList)
- .orElseGet(Collections::emptyList);
+ if (repeatedValue.getListValue() == null) {
+ return;
+ }
+ List<Value> values = repeatedValue.getListValue().getValuesList();
List<V> convertedValues = new ArrayList<>();
for (Value value : values) {
convertedValues.add(fromValue.apply(value));
@@ -180,15 +177,22 @@
/** binds a String field to read from / write to Struct */
TypeSpecBuilder<T, BuilderT> bindStringField(
String name,
- Function<T, Optional<String>> stringGetter,
+ Function<T, String> stringGetter,
BiConsumer<BuilderT, String> stringSetter) {
return bindFieldInternal(
name,
- (object) -> stringGetter.apply(object).map(TypeSpecBuilder::getStringValue),
- (builder, value) ->
- value.map(Value::getStringValue)
- .ifPresent(
- stringValue -> stringSetter.accept(builder, stringValue)));
+ (object) -> {
+ String value = stringGetter.apply(object);
+ if (value == null) {
+ return null;
+ }
+ return TypeSpecBuilder.getStringValue(value);
+ },
+ (builder, value) -> {
+ if (value.hasStringValue()) {
+ stringSetter.accept(builder, value.getStringValue());
+ }
+ });
}
/**
@@ -197,19 +201,21 @@
*/
<E extends Enum<E>> TypeSpecBuilder<T, BuilderT> bindEnumField(
String name,
- Function<T, Optional<E>> valueGetter,
+ Function<T, E> valueGetter,
BiConsumer<BuilderT, E> valueSetter,
Class<E> enumClass) {
return bindFieldInternal(
name,
- (object) ->
- valueGetter
- .apply(object)
- .map(Enum::toString)
- .map(TypeSpecBuilder::getStringValue),
+ (object) -> {
+ E enumVal = valueGetter.apply(object);
+ if (enumVal == null) {
+ return null;
+ }
+ return TypeSpecBuilder.getStringValue(enumVal.toString());
+ },
(builder, value) -> {
- if (value.isPresent()) {
- String stringValue = value.get().getStringValue();
+ if (value.hasStringValue()) {
+ String stringValue = value.getStringValue();
E[] enumValues = enumClass.getEnumConstants();
if (enumValues != null) {
for (E enumValue : enumValues) {
@@ -231,24 +237,24 @@
*/
TypeSpecBuilder<T, BuilderT> bindDurationField(
String name,
- Function<T, Optional<Duration>> valueGetter,
+ Function<T, Duration> valueGetter,
BiConsumer<BuilderT, Duration> valueSetter) {
return bindFieldInternal(
name,
- (object) ->
- valueGetter
- .apply(object)
- .map(Duration::toString)
- .map(TypeSpecBuilder::getStringValue),
+ (object) -> {
+ Duration duration = valueGetter.apply(object);
+ if (duration == null) {
+ return null;
+ }
+ return TypeSpecBuilder.getStringValue(duration.toString());
+ },
(builder, value) -> {
- if (value.isPresent()) {
- try {
- valueSetter.accept(
- builder, Duration.parse(value.get().getStringValue()));
- } catch (DateTimeParseException e) {
- throw new StructConversionException(
- "Failed to parse ISO 8601 string to Duration", e);
- }
+ try {
+ valueSetter.accept(
+ builder, Duration.parse(value.getStringValue()));
+ } catch (DateTimeParseException e) {
+ throw new StructConversionException(
+ "Failed to parse ISO 8601 string to Duration", e);
}
});
}
@@ -259,21 +265,23 @@
*/
TypeSpecBuilder<T, BuilderT> bindZonedDateTimeField(
String name,
- Function<T, Optional<ZonedDateTime>> valueGetter,
+ Function<T, ZonedDateTime> valueGetter,
BiConsumer<BuilderT, ZonedDateTime> valueSetter) {
return bindFieldInternal(
name,
- (object) ->
- valueGetter
- .apply(object)
- .map(ZonedDateTime::toOffsetDateTime)
- .map(OffsetDateTime::toString)
- .map(TypeSpecBuilder::getStringValue),
+ (object) -> {
+ ZonedDateTime zonedDateTime = valueGetter.apply(object);
+ if (zonedDateTime == null) {
+ return null;
+ }
+ return TypeSpecBuilder.getStringValue(
+ zonedDateTime.toOffsetDateTime().toString());
+ },
(builder, value) -> {
- if (value.isPresent()) {
+ if (value.hasStringValue()) {
try {
valueSetter.accept(
- builder, ZonedDateTime.parse(value.get().getStringValue()));
+ builder, ZonedDateTime.parse(value.getStringValue()));
} catch (DateTimeParseException e) {
throw new StructConversionException(
"Failed to parse ISO 8601 string to ZonedDateTime", e);
@@ -285,23 +293,20 @@
/** Binds a spec field to read from / write to Struct. */
<V> TypeSpecBuilder<T, BuilderT> bindSpecField(
String name,
- Function<T, Optional<V>> valueGetter,
+ Function<T, V> valueGetter,
BiConsumer<BuilderT, V> valueSetter,
TypeSpec<V> spec) {
return bindFieldInternal(
name,
- (object) ->
- valueGetter
- .apply(object)
- .map(Function.identity()) // Static analyzer incorrectly
- // throws error stating that the
- // input to toStruct is nullable. This is a workaround to avoid
- // the error from the analyzer.
- .map(spec::toValue),
- (builder, value) -> {
- if (value.isPresent()) {
- valueSetter.accept(builder, spec.fromValue(value.get()));
+ (object) -> {
+ V value = valueGetter.apply(object);
+ if (value == null) {
+ return null;
}
+ return spec.toValue(value);
+ },
+ (builder, value) -> {
+ valueSetter.accept(builder, spec.fromValue(value));
});
}
@@ -315,16 +320,16 @@
name,
valueGetter,
valueSetter,
- (element) -> Optional.ofNullable(element).map(spec::toValue),
+ spec::toValue,
(value) -> spec.fromValue(value));
}
TypeSpec<T> build() {
- return new TypeSpecImpl<T, BuilderT>(
+ return new TypeSpecImpl<>(
mIdentifierGetter,
mBindings,
mBuilderSupplier,
mBuilderFinalizer,
- Optional.ofNullable(mStructValidator));
+ mStructValidator);
}
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
index f101314..da4488c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
@@ -25,7 +25,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -35,26 +34,31 @@
*/
final class TypeSpecImpl<T, BuilderT> implements TypeSpec<T> {
/* The function to retrieve the identifier. */
- final Function<T, Optional<String>> mIdentifierGetter;
+ @NonNull
+ final Function<T, String> mIdentifierGetter;
/** The list of FieldBinding objects. */
+ @NonNull
final List<FieldBinding<T, BuilderT>> mBindings;
/** Validates the Struct during conversion to java object. */
- final Optional<CheckedInterfaces.Consumer<Struct>> mStructValidator;
+ @Nullable
+ final CheckedInterfaces.Consumer<Struct> mStructValidator;
/** Supplies BuilderT instances. */
+ @NonNull
final Supplier<BuilderT> mBuilderSupplier;
/** Builds the object instance. */
+ @NonNull
final Function<BuilderT, T> mBuilderFinalizer;
TypeSpecImpl(
- Function<T, Optional<String>> identifierGetter,
- List<FieldBinding<T, BuilderT>> bindings,
- Supplier<BuilderT> builderSupplier,
- Function<BuilderT, T> builderFinalizer,
- Optional<CheckedInterfaces.Consumer<Struct>> structValidator) {
+ @NonNull Function<T, String> identifierGetter,
+ @NonNull List<FieldBinding<T, BuilderT>> bindings,
+ @NonNull Supplier<BuilderT> builderSupplier,
+ @NonNull Function<BuilderT, T> builderFinalizer,
+ @Nullable CheckedInterfaces.Consumer<Struct> structValidator) {
this.mIdentifierGetter = identifierGetter;
this.mBindings = Collections.unmodifiableList(bindings);
this.mBuilderSupplier = builderSupplier;
@@ -65,7 +69,7 @@
@Nullable
@Override
public String getIdentifier(T obj) {
- return mIdentifierGetter.apply(obj).orElse(null);
+ return mIdentifierGetter.apply(obj);
}
/** Converts a java object into a Struct proto using List of FieldBinding. */
@@ -74,9 +78,10 @@
public Value toValue(@NonNull T obj) {
Struct.Builder structBuilder = Struct.newBuilder();
for (FieldBinding<T, BuilderT> binding : mBindings) {
- binding.getValueGetter()
- .apply(obj)
- .ifPresent(value -> structBuilder.putFields(binding.getName(), value));
+ Value value = binding.getValueGetter().apply(obj);
+ if (value != null) {
+ structBuilder.putFields(binding.getName(), value);
+ }
}
return Value.newBuilder().setStructValue(structBuilder).build();
}
@@ -94,15 +99,17 @@
throw new StructConversionException(
String.format("TypeSpecImpl cannot deserializes non-Struct value: %s", value));
}
- if (mStructValidator.isPresent()) {
- mStructValidator.get().accept(struct);
+ if (mStructValidator != null) {
+ mStructValidator.accept(struct);
}
BuilderT builder = mBuilderSupplier.get();
Map<String, Value> fieldsMap = struct.getFieldsMap();
for (FieldBinding<T, BuilderT> binding : mBindings) {
- Optional<Value> fieldValue = Optional.ofNullable(fieldsMap.get(binding.getName()));
- binding.getValueSetter().accept(builder, fieldValue);
+ Value fieldValue = fieldsMap.get(binding.getName());
+ if (fieldValue != null) {
+ binding.getValueSetter().accept(builder, fieldValue);
+ }
}
return mBuilderFinalizer.apply(builder);
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.kt
index a523193..ad4b7b6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.kt
@@ -26,7 +26,6 @@
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.proto.AppActionsContext
import androidx.appactions.interaction.proto.ParamValue
-import java.util.Optional
import java.util.function.BiConsumer
import java.util.function.Function
import java.util.function.Supplier
@@ -70,8 +69,7 @@
*/
private fun bindParameterInternal(
paramName: String,
- paramGetter: Function<Map<String, Property<*>>,
- Optional<AppActionsContext.IntentParameter>>,
+ paramGetter: Function<Map<String, Property<*>>, AppActionsContext.IntentParameter?>,
argumentSetter: ArgumentSetter<ArgumentsBuilderT>
): ActionSpecBuilder<ArgumentsT, ArgumentsBuilderT, OutputT> {
paramBindingList.add(create(paramName, paramGetter, argumentSetter))
@@ -81,8 +79,8 @@
/**
* Binds the parameter name, getter, and setter for a [Property].
*
- *
- * This parameter is required for any capability built from the generated [ActionSpec].
+ * If the Property getter returns a null value, this parameter will not exist in the parameter
+ * definition of the capability.
*
* @param paramName the name of this action' parameter.
* @param propertyGetter a getter of the Property from the property, which must be able to
@@ -94,43 +92,7 @@
*/
fun <T, PossibleValueT> bindParameter(
paramName: String,
- propertyGetter: Function<Map<String, Property<*>>, Property<PossibleValueT>>,
- paramConsumer: BiConsumer<in ArgumentsBuilderT, T>,
- paramValueConverter: ParamValueConverter<T>,
- entityConverter: EntityConverter<PossibleValueT>
- ): ActionSpecBuilder<ArgumentsT, ArgumentsBuilderT, OutputT> {
- return bindOptionalParameter(
- paramName,
- { propertyMap ->
- Optional.of(propertyGetter.apply(propertyMap))
- },
- paramConsumer,
- paramValueConverter,
- entityConverter
- )
- }
-
- /**
- * Binds the parameter name, getter, and setter for a [Property].
- *
- *
- * This parameter is optional for any capability built from the generated [ActionSpec].
- * If the Property Optional is not set, this parameter will not exist in the parameter
- * definition of the capability.
- *
- * @param paramName the name of this action' parameter.
- * @param optionalPropertyGetter an optional getter of the Property from the property,
- * which may be able to fetch a non-null `Property` from `PropertyT`,
- * or get [Optional.empty].
- * @param paramConsumer a setter to set the string value in the argument builder.
- * @param paramValueConverter converter FROM assistant ParamValue proto
- * @param entityConverter converter TO assistant Entity proto
- * @return the builder itself.
- */
- fun <T, PossibleValueT> bindOptionalParameter(
- paramName: String,
- optionalPropertyGetter: Function<Map<String, Property<*>>,
- Optional<Property<PossibleValueT>>>,
+ propertyGetter: Function<Map<String, Property<*>>, Property<PossibleValueT>?>,
paramConsumer: BiConsumer<in ArgumentsBuilderT, T>,
paramValueConverter: ParamValueConverter<T>,
entityConverter: EntityConverter<PossibleValueT>
@@ -138,35 +100,31 @@
return bindParameterInternal(
paramName,
{ propertyMap ->
- optionalPropertyGetter
- .apply(propertyMap)
- .map { property ->
- buildIntentParameter(paramName, property, entityConverter)
- }
+ propertyGetter.apply(propertyMap)?.let {
+ buildIntentParameter(paramName, it, entityConverter)
+ }
+ },
+ { argBuilder: ArgumentsBuilderT, paramList: List<ParamValue> ->
+ if (paramList.isNotEmpty()) {
+ paramConsumer.accept(
+ argBuilder,
+ SlotTypeConverter.ofSingular(paramValueConverter).convert(paramList)
+ )
+ }
}
- ) { argBuilder: ArgumentsBuilderT, paramList: List<ParamValue?> ->
- if (paramList.isNotEmpty()) {
- paramConsumer.accept(
- argBuilder,
- SlotTypeConverter.ofSingular(paramValueConverter).convert(paramList)
- )
- }
- }
+ )
}
/**
- * This is similar to [ActionSpecBuilder.bindOptionalParameter] but for setting a list of
+ * This is similar to [ActionSpecBuilder.bindParameter] but for setting a list of
* entities instead.
*
- *
- * This parameter is optional for any capability built from the generated [ActionSpec].
- * If the Property Optional is not set, this parameter will not exist in the parameter
+ * If the Property getter returns a null value, this parameter will not exist in the parameter
* definition of the capability.
*/
fun <T, PossibleValueT> bindRepeatedParameter(
paramName: String,
- optionalPropertyGetter: Function<Map<String, Property<*>>,
- Optional<Property<PossibleValueT>>>,
+ propertyGetter: Function<Map<String, Property<*>>, Property<PossibleValueT>?>,
paramConsumer: BiConsumer<in ArgumentsBuilderT, List<T>>,
paramValueConverter: ParamValueConverter<T>,
entityConverter: EntityConverter<PossibleValueT>
@@ -174,18 +132,17 @@
return bindParameterInternal(
paramName,
{ propertyMap ->
- optionalPropertyGetter
- .apply(propertyMap)
- .map { property ->
- buildIntentParameter(paramName, property, entityConverter)
- }
+ propertyGetter.apply(propertyMap)?.let {
+ buildIntentParameter(paramName, it, entityConverter)
+ }
+ },
+ { argBuilder: ArgumentsBuilderT, paramList: List<ParamValue?>? ->
+ paramConsumer.accept(
+ argBuilder,
+ SlotTypeConverter.ofRepeated(paramValueConverter).convert(paramList!!)
+ )
}
- ) { argBuilder: ArgumentsBuilderT, paramList: List<ParamValue?>? ->
- paramConsumer.accept(
- argBuilder,
- SlotTypeConverter.ofRepeated(paramValueConverter).convert(paramList!!)
- )
- }
+ )
}
/**
@@ -195,7 +152,7 @@
* @param outputFieldGetter a getter of the output from the `OutputT` instance.
* @param converter a converter from an output object to a ParamValue.
*/
- fun <T> bindOptionalOutput(
+ fun <T> bindOutput(
name: String,
outputFieldGetter: Function<OutputT, T?>,
converter: Function<T, ParamValue>
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.kt
index 552b6b7..a05c8ed 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecImpl.kt
@@ -41,9 +41,8 @@
.setName(capabilityName)
.addAllParams(
paramBindingList.stream()
- .map { binding -> binding.paramGetter.apply(property) }
- .filter { optional -> optional.isPresent }
- .map { optional -> optional.get() }
+ .map { binding -> binding.propertyConverter.apply(property) }
+ .filter { intentParam -> intentParam != null }
.collect(ImmutableCollectors.toImmutableList())
)
.build()
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.kt
index ffe3807..7e57ead 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ParamBinding.kt
@@ -21,13 +21,12 @@
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter
import androidx.appactions.interaction.proto.ParamValue
-import java.util.Optional
import java.util.function.Function
data class ParamBinding<ArgumentsT, ArgumentsBuilderT : BuilderOf<ArgumentsT>>
internal constructor(
val name: String,
- val paramGetter: Function<Map<String, Property<*>>, Optional<IntentParameter>>,
+ val propertyConverter: Function<Map<String, Property<*>>, IntentParameter?>,
val argumentSetter: ArgumentSetter<ArgumentsBuilderT>
) {
/**
@@ -44,7 +43,7 @@
@JvmStatic
fun <ArgumentsT, ArgumentsBuilderT : BuilderOf<ArgumentsT>> create(
name: String,
- paramGetter: Function<Map<String, Property<*>>, Optional<IntentParameter>>,
+ paramGetter: Function<Map<String, Property<*>>, IntentParameter?>,
argumentSetter: ArgumentSetter<ArgumentsBuilderT>
): ParamBinding<ArgumentsT, ArgumentsBuilderT> {
return ParamBinding(name, paramGetter, argumentSetter)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListener.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListener.kt
new file mode 100644
index 0000000..ed30805
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListener.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.task
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput
+
+/**
+ * Generic onReadyToConfirm listener for a task capability.
+ * This should wrap only the external Session#onReadyToConfirm and nothing else.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface OnReadyToConfirmListener<ArgumentsT, ConfirmationT> {
+ /** onReadyToConfirm callback for a task session. */
+ suspend fun onReadyToConfirm(arguments: ArgumentsT): ConfirmationOutput<ConfirmationT>
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListenerInternal.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListenerInternal.kt
deleted file mode 100644
index ffbfb24..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/OnReadyToConfirmListenerInternal.kt
+++ /dev/null
@@ -1,33 +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.impl.task
-
-import androidx.appactions.interaction.capabilities.core.ConfirmationOutput
-import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException
-import androidx.appactions.interaction.proto.ParamValue
-
-/**
- * Generic onReadyToConfirm listener for a task capability. This is the entry point to specific
- * onReadyToConfirm listeners. For example, Search/Update sub-BIIs factories may invoke specific
- * onReadyToConfirm listeners for that BII.
- */
-interface OnReadyToConfirmListenerInternal<ConfirmationT> {
- /** onReadyToConfirm callback for a task capability. */
- @Throws(StructConversionException::class)
- suspend fun onReadyToConfirm(
- args: Map<String, List<ParamValue>>
- ): ConfirmationOutput<ConfirmationT>
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/SessionBridge.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/SessionBridge.kt
index 6242b6f8..a31759c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/SessionBridge.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/SessionBridge.kt
@@ -26,7 +26,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun interface SessionBridge<
ExecutionSessionT,
+ ArgumentsT,
ConfirmationT
> {
- fun createTaskHandler(externalSession: ExecutionSessionT): TaskHandler<ConfirmationT>
+ fun createTaskHandler(
+ externalSession: ExecutionSessionT
+ ): TaskHandler<ArgumentsT, ConfirmationT>
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityImpl.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityImpl.kt
index fc9eeaf..4dee5fa 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityImpl.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityImpl.kt
@@ -46,7 +46,7 @@
private val actionSpec: ActionSpec<ArgumentsT, OutputT>,
private val property: Map<String, Property<*>>,
private val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSessionT?,
- private val sessionBridge: SessionBridge<ExecutionSessionT, ConfirmationT>,
+ private val sessionBridge: SessionBridge<ExecutionSessionT, ArgumentsT, ConfirmationT>,
private val sessionUpdaterSupplier: Supplier<SessionUpdaterT>
) : Capability(id) {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
index 1f577f5..6ef8d2b 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
@@ -41,7 +41,7 @@
override val sessionId: String,
actionSpec: ActionSpec<ArgumentsT, OutputT>,
appAction: AppAction,
- taskHandler: TaskHandler<ConfirmationT>,
+ taskHandler: TaskHandler<ArgumentsT, ConfirmationT>,
externalSession: BaseExecutionSession<ArgumentsT, OutputT>,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
) : CapabilitySession, TaskUpdateHandler {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
index 790a750..9601c24 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
@@ -29,24 +29,24 @@
/** Container of multi-turn Task related function references. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-data class TaskHandler<ConfirmationT>
+data class TaskHandler<ArgumentsT, ConfirmationT>
internal constructor(
internal val taskParamMap: Map<String, TaskParamBinding<*>>,
internal val confirmationDataBindings: Map<String, (ConfirmationT) -> List<ParamValue>>,
- internal val onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>?,
+ internal val onReadyToConfirmListener: OnReadyToConfirmListener<ArgumentsT, ConfirmationT>?,
) {
- class Builder<ConfirmationT>() {
+ class Builder<ArgumentsT, ConfirmationT>() {
private val mutableTaskParamMap = mutableMapOf<String, TaskParamBinding<*>>()
private val confirmationDataBindings =
mutableMapOf<String, (ConfirmationT) -> List<ParamValue>>()
- private var onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>? =
+ private var onReadyToConfirmListener: OnReadyToConfirmListener<ArgumentsT, ConfirmationT>? =
null
fun <ValueTypeT> registerInventoryTaskParam(
paramName: String,
listener: InventoryListener<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -62,7 +62,7 @@
paramName: String,
listener: InventoryListListener<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -80,7 +80,7 @@
converter: ParamValueConverter<ValueTypeT>,
entityConverter: EntityConverter<ValueTypeT>,
searchActionConverter: SearchActionConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -98,7 +98,7 @@
converter: ParamValueConverter<ValueTypeT>,
entityConverter: EntityConverter<ValueTypeT>,
searchActionConverter: SearchActionConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -114,7 +114,7 @@
paramName: String,
listener: ValueListener<ValueTypeT>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -130,7 +130,7 @@
paramName: String,
listener: ValueListener<List<ValueTypeT>>,
converter: ParamValueConverter<ValueTypeT>,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
@@ -154,26 +154,25 @@
paramName: String,
confirmationGetter: (ConfirmationT) -> T?,
converter: (T) -> ParamValue,
- ): Builder<ConfirmationT> = apply {
+ ) = apply {
confirmationDataBindings[paramName] = { output: ConfirmationT ->
listOfNotNull(confirmationGetter(output)).map(converter)
}
}
- /** Sets the onReadyToConfirmListener for this capability. */
- fun setOnReadyToConfirmListenerInternal(
- onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>,
- ): Builder<ConfirmationT> = apply {
+ /** Sets the onReadyToConfirmListener for this session. */
+ fun setOnReadyToConfirmListener(
+ onReadyToConfirmListener: OnReadyToConfirmListener<ArgumentsT, ConfirmationT>,
+ ) = apply {
this.onReadyToConfirmListener = onReadyToConfirmListener
}
- fun build(): TaskHandler<ConfirmationT> {
- return TaskHandler(
- mutableTaskParamMap.toMap(),
- confirmationDataBindings,
- onReadyToConfirmListener,
- )
- }
+ fun build() = TaskHandler(
+ mutableTaskParamMap.toMap(),
+ confirmationDataBindings,
+ onReadyToConfirmListener,
+ )
+
companion object {
val GROUND_IF_NO_IDENTIFIER = { paramValue: ParamValue -> !paramValue.hasIdentifier() }
val GROUND_NEVER = { _: ParamValue -> false }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
index f39671e..3b4ca84 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
@@ -60,7 +60,7 @@
private val sessionId: String,
private val actionSpec: ActionSpec<ArgumentsT, OutputT>,
private val appAction: AppActionsContext.AppAction,
- private val taskHandler: TaskHandler<ConfirmationT>,
+ private val taskHandler: TaskHandler<ArgumentsT, ConfirmationT>,
private val externalSession: BaseExecutionSession<ArgumentsT, OutputT>,
private val scope: CoroutineScope,
) {
@@ -450,9 +450,12 @@
private suspend fun getFulfillmentResponseForConfirmation(
finalArguments: Map<String, List<ParamValue>>,
): FulfillmentResponse {
+ val arguments = actionSpec.buildArguments(finalArguments)
+ requireNotNull(taskHandler.onReadyToConfirmListener) {
+ "caller must ensure TaskHandler.onReadyToConfirmListener is not null"
+ }
val result = invokeExternalSuspendBlock("onReadyToConfirm") {
- // TODO(b/280692953) split onReadyToConfirmListener to external & internal blocks
- taskHandler.onReadyToConfirmListener!!.onReadyToConfirm(finalArguments)
+ taskHandler.onReadyToConfirmListener.onReadyToConfirm(arguments)
}
val fulfillmentResponse = FulfillmentResponse.newBuilder()
convertToConfirmationOutput(result)?.let { fulfillmentResponse.confirmationData = it }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CallbackUtils.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CallbackUtils.kt
index cd36c47..4410f42 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CallbackUtils.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/utils/CallbackUtils.kt
@@ -23,6 +23,7 @@
import androidx.appactions.interaction.capabilities.core.impl.task.exceptions.DisambigStateException
import androidx.appactions.interaction.capabilities.core.impl.task.exceptions.InvalidResolverException
import kotlin.reflect.KClass
+import kotlinx.coroutines.withTimeout
private const val LOG_TAG = "CallbackUtils"
@@ -39,14 +40,18 @@
/** invoke an externally implemented suspend method, wrapping any exceptions with
* ExternalException.
*/
+
+private const val TIMEOUT_MILLIS = 3000L
internal suspend fun <T> invokeExternalSuspendBlock(
description: String,
block: suspend () -> T
): T {
- try {
- return block()
- } catch (t: Throwable) {
- throw ExternalException("exception occurred during '$description'", t)
+ return withTimeout(TIMEOUT_MILLIS) {
+ try {
+ block()
+ } catch (t: Throwable) {
+ throw ExternalException("exception occurred during '$description'", t)
+ }
}
}
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 1d81ebd..322f1d7 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
@@ -42,7 +42,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.proto.TaskInfo
import com.google.common.truth.Truth.assertThat
-import java.util.Optional
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
@@ -337,19 +336,16 @@
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"optionalString",
{ properties ->
- properties["optionalString"]
- ?.let { it as Property<StringValue> }
- ?.let { Optional.of(it) }
- ?: Optional.ofNullable(null)
+ properties["optionalString"] as? Property<StringValue>
},
Arguments.Builder::setOptionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"optionalStringOutput",
Output::optionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER::toParamValue
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
index 50c9349..291bcb0 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
@@ -36,7 +36,6 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
-import java.util.Optional;
@RunWith(JUnit4.class)
public final class TypeSpecImplTest {
@@ -49,7 +48,7 @@
TypeSpec<TestEntity> entityTypeSpec =
TypeSpecBuilder.newBuilder(
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
- .bindIdentifier(testEntity -> Optional.ofNullable(testEntity.getId()))
+ .bindIdentifier(TestEntity::getId)
.build();
assertThat(
entityTypeSpec.getIdentifier(
@@ -65,7 +64,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindEnumField(
"enum",
- (testEntity) -> Optional.ofNullable(testEntity.getEnum()),
+ TestEntity::getEnum,
TestEntity.Builder::setEnum,
TestEnum.class)
.build();
@@ -92,7 +91,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindEnumField(
"enum",
- (testEntity) -> Optional.ofNullable(testEntity.getEnum()),
+ TestEntity::getEnum,
TestEntity.Builder::setEnum,
TestEnum.class)
.build();
@@ -118,7 +117,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindDurationField(
"duration",
- (testEntity) -> Optional.ofNullable(testEntity.getDuration()),
+ TestEntity::getDuration,
TestEntity.Builder::setDuration)
.build();
TestEntity entity = new TestEntity.Builder().setDuration(Duration.ofMinutes(5)).build();
@@ -144,7 +143,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindZonedDateTimeField(
"date",
- (testEntity) -> Optional.ofNullable(testEntity.getZonedDateTime()),
+ TestEntity::getZonedDateTime,
TestEntity.Builder::setZonedDateTime)
.build();
TestEntity entity =
@@ -175,7 +174,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindZonedDateTimeField(
"date",
- (testEntity) -> Optional.ofNullable(testEntity.getZonedDateTime()),
+ TestEntity::getZonedDateTime,
TestEntity.Builder::setZonedDateTime)
.build();
TestEntity entity =
@@ -212,7 +211,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindZonedDateTimeField(
"date",
- (testEntity) -> Optional.ofNullable(testEntity.getZonedDateTime()),
+ TestEntity::getZonedDateTime,
TestEntity.Builder::setZonedDateTime)
.build();
Value malformedValue =
@@ -237,7 +236,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindSpecField(
"entity",
- (testEntity) -> Optional.ofNullable(testEntity.getEntity()),
+ TestEntity::getEntity,
TestEntity.Builder::setEntity,
TypeSpecBuilder.newBuilder(
"TestEntity",
@@ -245,8 +244,7 @@
TestEntity.Builder::build)
.bindStringField(
"name",
- (testEntity) ->
- Optional.ofNullable(testEntity.getName()),
+ TestEntity::getName,
TestEntity.Builder::setName)
.build())
.build();
@@ -292,7 +290,7 @@
"TestEntity", TestEntity.Builder::new, TestEntity.Builder::build)
.bindSpecField(
"entity",
- (testEntity) -> Optional.ofNullable(testEntity.getEntity()),
+ TestEntity::getEntity,
TestEntity.Builder::setEntity,
TypeSpecBuilder.newBuilder(
"TestEntity",
@@ -300,8 +298,7 @@
TestEntity.Builder::build)
.bindStringField(
"name",
- (testEntity) ->
- Optional.ofNullable(testEntity.getName()),
+ TestEntity::getName,
TestEntity.Builder::setName)
.build())
.build();
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 ec07c27..899825f 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
@@ -43,7 +43,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
@RunWith(JUnit4.class)
@@ -57,18 +56,18 @@
"requiredString",
properties ->
{
- return (Property<StringValue>) (properties.get(
- "requiredString"));
+ Property<?> property = properties.get("requiredString");
+ return (property == null) ? null : (Property<StringValue>) property;
},
Arguments.Builder::setRequiredStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER)
- .bindOptionalParameter(
+ .bindParameter(
"optionalString",
properties ->
{
- return Optional.ofNullable((Property<StringValue>) (properties.get(
- "optionalString")));
+ Property<?> property = properties.get("optionalString");
+ return (property == null) ? null : (Property<StringValue>) property;
},
Arguments.Builder::setOptionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
@@ -77,13 +76,13 @@
"repeatedString",
properties ->
{
- return Optional.ofNullable((Property<StringValue>) (properties.get(
- "repeatedString")));
+ Property<?> property = properties.get("repeatedString");
+ return (property == null) ? null : (Property<StringValue>) property;
},
Arguments.Builder::setRepeatedStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER)
- .bindOptionalOutput(
+ .bindOutput(
"optionalStringOutput",
Output::getOptionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER::toParamValue)
@@ -130,16 +129,17 @@
.bindParameter(
"requiredEntity",
properties -> {
- return (Property<TestEntity>) (properties.get("requiredEntity"));
+ Property<?> property = properties.get("requiredEntity");
+ return (property == null) ? null : (Property<TestEntity>) property;
},
GenericEntityArguments.Builder::setSingularField,
TEST_ENTITY_PARAM_VALUE_CONVERTER,
TEST_ENTITY_CONVERTER)
- .bindOptionalParameter(
+ .bindParameter(
"optionalEntity",
properties -> {
- return Optional.of(
- (Property<TestEntity>) (properties.get("optionalEntity")));
+ Property<?> property = properties.get("optionalEntity");
+ return (property == null) ? null : (Property<TestEntity>) property;
},
GenericEntityArguments.Builder::setOptionalField,
TEST_ENTITY_PARAM_VALUE_CONVERTER,
@@ -147,9 +147,8 @@
.bindRepeatedParameter(
"repeatedEntities",
properties -> {
- return Optional.of(
- (Property<TestEntity>)
- (properties.get("repeatedEntities")));
+ Property<?> property = properties.get("repeatedEntities");
+ return (property == null) ? null : (Property<TestEntity>) property;
},
GenericEntityArguments.Builder::setRepeatedField,
TEST_ENTITY_PARAM_VALUE_CONVERTER,
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/impl/task/TaskCapabilityImplTest.kt
similarity index 95%
rename from appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
rename to appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityImplTest.kt
index 899612d..241d5f3 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/impl/task/TaskCapabilityImplTest.kt
@@ -70,7 +70,6 @@
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.ListenableFuture
-import java.util.Optional
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Supplier
@@ -93,7 +92,7 @@
Futures.immediateFuture(ExecutionResult.Builder<Output>().build())
}
},
- sessionBridge = { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = { TaskHandler.Builder<Arguments, Confirmation>().build() },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
private val hostProperties: HostProperties =
@@ -139,7 +138,7 @@
Futures.immediateFuture(ExecutionResult.Builder<Output>().build())
}
},
- sessionBridge = { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = { TaskHandler.Builder<Arguments, Confirmation>().build() },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
mutableEntityList.add(StringValue.of("entity1"))
@@ -186,7 +185,7 @@
createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
{ externalSession },
- { TaskHandler.Builder<Confirmation>().build() },
+ { TaskHandler.Builder<Arguments, Confirmation>().build() },
::EmptyTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -213,7 +212,9 @@
)
}
},
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -290,7 +291,9 @@
val capability: Capability = createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
sessionFactory = { _ -> externalSession },
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::RequiredTaskUpdater
)
val session = capability.createSession("mySessionId", hostProperties)
@@ -328,7 +331,9 @@
)
}
},
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::RequiredTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -377,8 +382,12 @@
val sessionBridge =
SessionBridge<
CapabilityTwoStrings.ExecutionSession,
+ CapabilityTwoStrings.Arguments,
CapabilityTwoStrings.Confirmation> {
- TaskHandler.Builder<CapabilityTwoStrings.Confirmation>()
+ TaskHandler.Builder<
+ CapabilityTwoStrings.Arguments,
+ CapabilityTwoStrings.Confirmation
+ >()
.registerValueTaskParam(
"stringSlotA",
AUTO_ACCEPT_STRING_VALUE,
@@ -468,8 +477,12 @@
val sessionBridge =
SessionBridge<
CapabilityTwoStrings.ExecutionSession,
+ CapabilityTwoStrings.Arguments,
CapabilityTwoStrings.Confirmation> {
- TaskHandler.Builder<CapabilityTwoStrings.Confirmation>()
+ TaskHandler.Builder<
+ CapabilityTwoStrings.Arguments,
+ CapabilityTwoStrings.Confirmation
+ >()
.registerValueTaskParam(
"stringSlotA",
AUTO_ACCEPT_STRING_VALUE,
@@ -544,7 +557,9 @@
createCapability(
property,
sessionFactory = { _ -> ExecutionSession.DEFAULT },
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -622,8 +637,8 @@
}
},
sessionBridge =
- SessionBridge<ExecutionSession, Confirmation> { session ->
- val builder = TaskHandler.Builder<Confirmation>()
+ SessionBridge<ExecutionSession, Arguments, Confirmation> { session ->
+ val builder = TaskHandler.Builder<Arguments, Confirmation>()
session.requiredStringListener?.let {
listener: AppEntityListener<String> ->
builder.registerAppEntityTaskParam(
@@ -778,8 +793,12 @@
val sessionBridge =
SessionBridge<
CapabilityStructFill.ExecutionSession,
+ CapabilityStructFill.Arguments,
CapabilityStructFill.Confirmation> { session ->
- TaskHandler.Builder<CapabilityStructFill.Confirmation>()
+ TaskHandler.Builder<
+ CapabilityStructFill.Arguments,
+ CapabilityStructFill.Confirmation
+ >()
.registerAppEntityTaskParam(
"listItem",
session.listItemListener,
@@ -984,7 +1003,9 @@
createCapability(
property,
sessionFactory = sessionFactory,
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -1025,8 +1046,8 @@
}
}
var onReadyToConfirm =
- object : OnReadyToConfirmListenerInternal<Confirmation> {
- override suspend fun onReadyToConfirm(args: Map<String, List<ParamValue>>):
+ object : OnReadyToConfirmListener<Arguments, Confirmation> {
+ override suspend fun onReadyToConfirm(arguments: Arguments):
ConfirmationOutput<Confirmation> {
return ConfirmationOutput.Builder<Confirmation>()
.setConfirmation(
@@ -1044,8 +1065,8 @@
property,
sessionFactory = sessionFactory,
sessionBridge = SessionBridge {
- TaskHandler.Builder<Confirmation>()
- .setOnReadyToConfirmListenerInternal(onReadyToConfirm)
+ TaskHandler.Builder<Arguments, Confirmation>()
+ .setOnReadyToConfirmListener(onReadyToConfirm)
.build()
},
sessionUpdaterSupplier = ::EmptyTaskUpdater
@@ -1093,7 +1114,9 @@
createCapability(
property,
sessionFactory = sessionFactory,
- sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Arguments, Confirmation>().build()
+ },
sessionUpdaterSupplier = ::EmptyTaskUpdater
)
val session = capability.createSession(fakeSessionId, hostProperties)
@@ -1126,6 +1149,7 @@
.isEqualTo(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
}
+ @Test
fun structConversionException_shouldReportStructConversionFailure() {
val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
{ _ ->
@@ -1235,9 +1259,13 @@
setProperty(SINGLE_REQUIRED_FIELD_PROPERTY)
}
- override val sessionBridge: SessionBridge<ExecutionSession, Confirmation> = SessionBridge {
+ override val sessionBridge: SessionBridge<
+ ExecutionSession,
+ Arguments,
+ Confirmation
+ > = SessionBridge {
session ->
- val builder = TaskHandler.Builder<Confirmation>()
+ val builder = TaskHandler.Builder<Arguments, Confirmation>()
session.requiredStringListener?.let {
listener: AppEntityListener<String> ->
builder.registerAppEntityTaskParam(
@@ -1321,25 +1349,19 @@
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"optional",
{ properties ->
- properties["optional"]
- ?.let { it as Property<StringValue> }
- ?.let { Optional.of(it) }
- ?: Optional.ofNullable(null)
+ properties["optional"] as? Property<StringValue>
},
Arguments.Builder::setOptionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"optionalEnum",
{ properties ->
- properties["optionalEnum"]
- ?.let { it as Property<TestEnum> }
- ?.let { Optional.of(it) }
- ?: Optional.ofNullable(null)
+ properties["optionalEnum"] as? Property<TestEnum>
},
Arguments.Builder::setEnumField,
ENUM_CONVERTER,
@@ -1348,16 +1370,13 @@
.bindRepeatedParameter(
"repeated",
{ properties ->
- properties["repeated"]
- ?.let { it as Property<StringValue> }
- ?.let { Optional.of(it) }
- ?: Optional.ofNullable(null)
+ properties["repeated"] as? Property<StringValue>
},
Arguments.Builder::setRepeatedStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"optionalStringOutput",
Output::optionalStringField,
TypeConverters.STRING_PARAM_VALUE_CONVERTER::toParamValue
@@ -1395,7 +1414,7 @@
private fun <SessionUpdaterT : AbstractTaskUpdater> createCapability(
property: Map<String, Property<*>>,
sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession,
- sessionBridge: SessionBridge<ExecutionSession, Confirmation>,
+ sessionBridge: SessionBridge<ExecutionSession, Arguments, Confirmation>,
sessionUpdaterSupplier: Supplier<SessionUpdaterT>
): TaskCapabilityImpl<
Arguments,
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/impl/task/TaskCapabilityUtilsTest.kt
similarity index 100%
rename from appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
rename to appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilityUtilsTest.kt
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskSlotProcessorTest.kt
similarity index 100%
rename from appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.kt
rename to appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskSlotProcessorTest.kt
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.kt
index 8415ed7..40d0f99 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.kt
@@ -26,7 +26,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.TEST"
@@ -35,7 +34,7 @@
class Arguments internal constructor(
val listItem: ListItem?,
- val anyString: String?,
+ val anyString: String?
) {
override fun toString(): String {
return "Arguments(listItem=$listItem, " +
@@ -86,19 +85,19 @@
val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"listItem",
{ properties ->
- Optional.ofNullable(properties["listItem"] as Property<ListItem>)
+ properties["listItem"] as? Property<ListItem>
},
Arguments.Builder::setListItem,
ParamValueConverter.of(TypeConverters.LIST_ITEM_TYPE_SPEC),
EntityConverter.of(TypeConverters.LIST_ITEM_TYPE_SPEC)::convert
)
- .bindOptionalParameter(
+ .bindParameter(
"string",
{ properties ->
- Optional.ofNullable(properties["anyString"] as Property<StringValue>)
+ properties["anyString"] as? Property<StringValue>
},
Arguments.Builder::setAnyString,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.kt
index 542807b..612d3ca 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.kt
@@ -23,7 +23,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.TEST"
@@ -32,7 +31,7 @@
class Arguments internal constructor(
val stringSlotA: String?,
- val stringSlotB: String?,
+ val stringSlotB: String?
) {
override fun toString(): String {
return "Arguments(stringSlotA=$stringSlotA, " +
@@ -81,18 +80,20 @@
val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"stringSlotA",
{ properties ->
- Optional.ofNullable(properties["stringSlotA"] as Property<StringValue>) },
+ properties["stringSlotA"] as? Property<StringValue>
+ },
Arguments.Builder::setStringSlotA,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"stringSlotB",
{ properties ->
- Optional.ofNullable(properties["stringSlotB"] as Property<StringValue>) },
+ properties["stringSlotB"] as? Property<StringValue>
+ },
Arguments.Builder::setStringSlotB,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
TypeConverters.STRING_VALUE_ENTITY_CONVERTER
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
index eeae92b..4fd5dc7 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import java.time.LocalTime
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.GET_EXERCISE_OBSERVATION"
@@ -33,7 +32,7 @@
class GetExerciseObservation private constructor() {
internal enum class PropertyMapStrings(val key: String) {
START_TIME("exerciseObservation.startTime"),
- END_TIME("exerciseObservation.endTime"),
+ END_TIME("exerciseObservation.endTime")
}
class CapabilityBuilder :
@@ -110,25 +109,19 @@
Arguments::Builder
)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"exerciseObservation.startTime",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.START_TIME.key]
- as Property<LocalTime>
- )
+ properties[PropertyMapStrings.START_TIME.key] as? Property<LocalTime>
},
Arguments.Builder::setStartTime,
TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"exerciseObservation.endTime",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.END_TIME.key]
- as Property<LocalTime>
- )
+ properties[PropertyMapStrings.END_TIME.key] as? Property<LocalTime>
},
Arguments.Builder::setEndTime,
TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
index cfacf6e..7d843ac 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import java.time.LocalTime
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.GET_HEALTH_OBSERVATION"
@@ -34,7 +33,7 @@
internal enum class PropertyMapStrings(val key: String) {
START_TIME("healthObservation.startTime"),
- END_TIME("healthObservation.endTime"),
+ END_TIME("healthObservation.endTime")
}
class CapabilityBuilder :
@@ -115,25 +114,19 @@
Arguments::Builder
)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"healthObservation.startTime",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.START_TIME.key]
- as Property<LocalTime>
- )
+ properties[PropertyMapStrings.START_TIME.key] as? Property<LocalTime>
},
Arguments.Builder::setStartTime,
TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"healthObservation.endTime",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.END_TIME.key]
- as Property<LocalTime>
- )
+ properties[PropertyMapStrings.END_TIME.key] as? Property<LocalTime>
},
Arguments.Builder::setEndTime,
TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
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 f8b68c1..8a94643d 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
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.PAUSE_EXERCISE"
@@ -32,7 +31,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class PauseExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- NAME("exercise.name"),
+ NAME("exercise.name")
}
class CapabilityBuilder :
@@ -98,13 +97,10 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"exercise.name",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.NAME.key] as? Property<StringValue>
},
Arguments.Builder::setName,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
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 a154fc2..963f7ad 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
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.RESUME_EXERCISE"
@@ -32,7 +31,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class ResumeExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- NAME("exercise.name"),
+ NAME("exercise.name")
}
class CapabilityBuilder :
@@ -98,13 +97,10 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"exercise.name",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.NAME.key] as? Property<StringValue>
},
Arguments.Builder::setName,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
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 1d89b82..3de87ec 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
@@ -25,7 +25,6 @@
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
import java.time.Duration
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
@@ -34,7 +33,7 @@
class StartExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
NAME("exercise.name"),
- DURATION("exercise.duration"),
+ DURATION("exercise.duration")
}
class CapabilityBuilder :
@@ -112,25 +111,19 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"exercise.duration",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
+ properties[PropertyMapStrings.DURATION.key] as? Property<Duration>
},
Arguments.Builder::setDuration,
TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
TypeConverters.DURATION_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"exercise.name",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.NAME.key] as? Property<StringValue>
},
Arguments.Builder::setName,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
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 5bbc5eb7..e257fac 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
@@ -24,7 +24,6 @@
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.STOP_EXERCISE"
@@ -32,7 +31,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class StopExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- NAME("exercise.name"),
+ NAME("exercise.name")
}
class CapabilityBuilder :
@@ -99,13 +98,10 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"exercise.name",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.NAME.key] as? Property<StringValue>
},
Arguments.Builder::setName,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
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 bf30c36..260c074 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
@@ -28,7 +28,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.PAUSE_TIMER"
@@ -36,7 +35,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class PauseTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- TIMER_LIST("timer.timerList"),
+ TIMER_LIST("timer.timerList")
}
class CapabilityBuilder :
@@ -60,7 +59,7 @@
class Arguments
internal constructor(
- val timerList: List<TimerValue>?,
+ val timerList: List<TimerValue>?
) {
override fun toString(): String {
return "Arguments(timerList=$timerList)"
@@ -146,7 +145,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()
}
@@ -165,19 +164,16 @@
.bindRepeatedParameter(
"timer",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
+ properties[PropertyMapStrings.TIMER_LIST.key] as? Property<TimerValue>
},
Arguments.Builder::setTimerList,
TimerValue.PARAM_VALUE_CONVERTER,
TimerValue.ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
- ExecutionStatus::toParamValue,
+ ExecutionStatus::toParamValue
)
.build()
}
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
index fce016f..8fb648c 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
@@ -28,7 +28,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.RESET_TIMER"
@@ -36,7 +35,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class ResetTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- TIMER_LIST("timer.timerList"),
+ TIMER_LIST("timer.timerList")
}
class CapabilityBuilder :
@@ -82,7 +81,7 @@
private var timerList: List<TimerValue>? = null
fun setTimerList(
- timerList: List<TimerValue>,
+ timerList: List<TimerValue>
): Builder = apply { this.timerList = timerList }
override fun build(): Arguments = Arguments(timerList)
@@ -143,7 +142,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()
}
@@ -162,16 +161,13 @@
.bindRepeatedParameter(
"timer",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
+ properties[PropertyMapStrings.TIMER_LIST.key] as? Property<TimerValue>
},
Arguments.Builder::setTimerList,
TimerValue.PARAM_VALUE_CONVERTER,
TimerValue.ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
index 76f3019..702d431 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
@@ -28,7 +28,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.RESUME_TIMER"
@@ -36,7 +35,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class ResumeTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- TIMER_LIST("timer.timerList"),
+ TIMER_LIST("timer.timerList")
}
class CapabilityBuilder :
@@ -82,7 +81,7 @@
private var timerList: List<TimerValue>? = null
fun setTimerList(
- timerList: List<TimerValue>,
+ timerList: List<TimerValue>
): Builder = apply { this.timerList = timerList }
override fun build(): Arguments = Arguments(timerList)
@@ -143,7 +142,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()
}
@@ -162,16 +161,13 @@
.bindRepeatedParameter(
"timer",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
+ properties[PropertyMapStrings.TIMER_LIST.key] as? Property<TimerValue>
},
Arguments.Builder::setTimerList,
TimerValue.PARAM_VALUE_CONVERTER,
TimerValue.ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
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 ea01afc..6ffb63a 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
@@ -34,7 +34,6 @@
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
import java.time.Duration
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.START_TIMER"
@@ -45,7 +44,7 @@
TIMER_LIST("timer.timerList"),
IDENTIFIER("timer.identifier"),
NAME("timer.name"),
- DURATION("timer.duration"),
+ DURATION("timer.duration")
}
class CapabilityBuilder :
@@ -58,10 +57,14 @@
>(ACTION_SPEC) {
private var properties = mutableMapOf<String, Property<*>>()
- override val sessionBridge: SessionBridge<ExecutionSession, Confirmation> = SESSION_BRIDGE
+ override val sessionBridge: SessionBridge<
+ ExecutionSession,
+ Arguments,
+ Confirmation
+ > = SESSION_BRIDGE
override fun setExecutionSessionFactory(
- sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession,
+ sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession
): CapabilityBuilder = super.setExecutionSessionFactory(sessionFactory)
fun setTimerList(timerList: Property<TimerValue>): CapabilityBuilder = apply {
@@ -96,7 +99,7 @@
class Arguments internal constructor(
val identifier: String?,
val name: String?,
- val duration: Duration?,
+ val duration: Duration?
) {
override fun toString(): String {
return "Arguments(identifier=$identifier,name=$name,duration=$duration)"
@@ -191,7 +194,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()
}
@@ -205,64 +208,55 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"timer.identifier",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.IDENTIFIER.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.IDENTIFIER.key] as? Property<StringValue>
},
Arguments.Builder::setIdentifier,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"timer.name",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
+ properties[PropertyMapStrings.NAME.key] as? Property<StringValue>
},
Arguments.Builder::setName,
TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"timer.duration",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
+ properties[PropertyMapStrings.DURATION.key] as? Property<Duration>
},
Arguments.Builder::setDuration,
TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
- TypeConverters.DURATION_ENTITY_CONVERTER,
+ TypeConverters.DURATION_ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
- ExecutionStatus::toParamValue,
+ ExecutionStatus::toParamValue
)
.build()
- private val SESSION_BRIDGE = SessionBridge<ExecutionSession, Confirmation> {
+ private val SESSION_BRIDGE = SessionBridge<ExecutionSession, Arguments, Confirmation> {
session ->
- val taskHandlerBuilder = TaskHandler.Builder<Confirmation>()
+ val taskHandlerBuilder = TaskHandler.Builder<Arguments, Confirmation>()
session.nameListener?.let {
taskHandlerBuilder.registerValueTaskParam(
"timer.name",
it,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER
)
}
session.durationListener?.let {
taskHandlerBuilder.registerValueTaskParam(
"timer.duration",
it,
- TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+ TypeConverters.DURATION_PARAM_VALUE_CONVERTER
)
}
taskHandlerBuilder.build()
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
index c6f55b5..4c279e2 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
@@ -28,7 +28,6 @@
import androidx.appactions.interaction.proto.ParamValue
import androidx.appactions.interaction.protobuf.Struct
import androidx.appactions.interaction.protobuf.Value
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.STOP_TIMER"
@@ -36,7 +35,7 @@
@CapabilityFactory(name = CAPABILITY_NAME)
class StopTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
- TIMER_LIST("timer.timerList"),
+ TIMER_LIST("timer.timerList")
}
class CapabilityBuilder :
@@ -82,7 +81,7 @@
private var timerList: List<TimerValue>? = null
fun setTimerList(
- timerList: List<TimerValue>,
+ timerList: List<TimerValue>
): Builder = apply { this.timerList = timerList }
override fun build(): Arguments = Arguments(timerList)
@@ -143,7 +142,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()
}
@@ -162,16 +161,13 @@
.bindRepeatedParameter(
"timer",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
+ properties[PropertyMapStrings.TIMER_LIST.key] as? Property<TimerValue>
},
Arguments.Builder::setTimerList,
TimerValue.PARAM_VALUE_CONVERTER,
TimerValue.ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
index 0f06ed6..908051a 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
@@ -162,7 +162,7 @@
Arguments::Builder
)
.setOutput(Output::class.java)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue,
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
index 81a7bdab..5a9a6e2 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
@@ -38,7 +38,6 @@
import androidx.appactions.interaction.protobuf.Value
import java.time.Duration
import java.time.ZonedDateTime
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.START_SAFETY_CHECK"
@@ -47,7 +46,7 @@
class StartSafetyCheck private constructor() {
internal enum class PropertyMapStrings(val key: String) {
DURATION("safetycheck.duration"),
- CHECK_IN_TIME("safetycheck.checkInTime"),
+ CHECK_IN_TIME("safetycheck.checkInTime")
}
// TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
@@ -216,7 +215,7 @@
.setStructValue(
Struct.newBuilder()
.putFields(TypeConverters.FIELD_NAME_TYPE, value)
- .build(),
+ .build()
)
.build()
}
@@ -232,36 +231,30 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalParameter(
+ .bindParameter(
"safetyCheck.duration",
{ properties ->
- Optional.ofNullable(
- properties[PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
+ properties[PropertyMapStrings.DURATION.key] as? Property<Duration>
},
Arguments.Builder::setDuration,
TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
TypeConverters.DURATION_ENTITY_CONVERTER
)
- .bindOptionalParameter(
+ .bindParameter(
"safetyCheck.checkInTime",
- { property ->
- Optional.ofNullable(
- property[PropertyMapStrings.CHECK_IN_TIME.key]
- as Property<ZonedDateTime>
- )
+ { properties ->
+ properties[PropertyMapStrings.CHECK_IN_TIME.key] as? Property<ZonedDateTime>
},
Arguments.Builder::setCheckInTime,
TypeConverters.ZONED_DATETIME_PARAM_VALUE_CONVERTER,
TypeConverters.ZONED_DATETIME_ENTITY_CONVERTER
)
- .bindOptionalOutput(
+ .bindOutput(
"safetyCheck",
Output::safetyCheck,
ParamValueConverter.of(SAFETY_CHECK_TYPE_SPEC)::toParamValue
)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
index 2f631de..28967d6 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
@@ -163,7 +163,7 @@
Arguments::Builder
)
.setOutput(Output::class.java)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue,
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
index 295e995..53893f4 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
@@ -159,7 +159,7 @@
ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
.setArguments(Arguments::class.java, Arguments::Builder)
.setOutput(Output::class.java)
- .bindOptionalOutput(
+ .bindOutput(
"executionStatus",
Output::executionStatus,
ExecutionStatus::toParamValue
diff --git a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/testing/internal/FakeCapability.kt b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/testing/internal/FakeCapability.kt
index 9708c4a..1105d83 100644
--- a/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/testing/internal/FakeCapability.kt
+++ b/appactions/interaction/interaction-service/src/test/java/androidx/appactions/interaction/service/testing/internal/FakeCapability.kt
@@ -26,19 +26,8 @@
import androidx.appactions.interaction.capabilities.core.impl.task.TaskHandler
import androidx.appactions.interaction.capabilities.core.properties.Property
import androidx.appactions.interaction.capabilities.core.properties.StringValue
-import java.util.Optional
private const val CAPABILITY_NAME = "actions.intent.FAKE_CAPABILITY"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(FakeCapability.Arguments::class.java, FakeCapability.Arguments::Builder)
- .setOutput(FakeCapability.Output::class.java).bindOptionalParameter(
- "fieldOne",
- { property -> Optional.ofNullable(property["fieldOne"] as Property<StringValue>) },
- FakeCapability.Arguments.Builder::setFieldOne,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
- ).build()
class FakeCapability private constructor() {
class Properties(
@@ -73,9 +62,9 @@
Confirmation,
ExecutionSession,
>(ACTION_SPEC) {
- override val sessionBridge = SessionBridge<ExecutionSession, Confirmation> {
+ override val sessionBridge = SessionBridge<ExecutionSession, Arguments, Confirmation> {
session ->
- val builder = TaskHandler.Builder<Confirmation>()
+ val builder = TaskHandler.Builder<Arguments, Confirmation>()
session.fieldOneListener?.let {
builder.registerValueTaskParam(
"fieldOne",
@@ -103,4 +92,19 @@
return super.build()
}
}
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindParameter(
+ "fieldOne",
+ { properties -> properties["fieldOne"] as? Property<StringValue> },
+ Arguments.Builder::setFieldOne,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+ )
+ .build()
+ }
}
diff --git a/appcompat/appcompat/lint-baseline.xml b/appcompat/appcompat/lint-baseline.xml
index 0936e35..dc8d901 100644
--- a/appcompat/appcompat/lint-baseline.xml
+++ b/appcompat/appcompat/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="NewApi"
@@ -434,6 +434,24 @@
</issue>
<issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appcompat/widget/DropDownListView.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appcompat/widget/DropDownListView.java"/>
+ </issue>
+
+ <issue
id="KotlinPropertyAccess"
message="The getter return type (`View`) and setter parameter type (`ScrollingTabContainerView`) getter and setter methods for property `tabContainer` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
errorLine1=" public View getTabContainer() {"
diff --git a/appsearch/appsearch-platform-storage/lint-baseline.xml b/appsearch/appsearch-platform-storage/lint-baseline.xml
index 279ab97..d82c712 100644
--- a/appsearch/appsearch-platform-storage/lint-baseline.xml
+++ b/appsearch/appsearch-platform-storage/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.1.0-alpha07">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="WrongConstant"
@@ -244,4 +244,31 @@
file="src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" return BuildCompat.isAtLeastT();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java"/>
+ </issue>
+
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastT cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastT()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java"/>
+ </issue>
+
</issues>
diff --git a/benchmark/baseline-profile-gradle-plugin/build.gradle b/benchmark/baseline-profile-gradle-plugin/build.gradle
index 057bc2d1..9038336 100644
--- a/benchmark/baseline-profile-gradle-plugin/build.gradle
+++ b/benchmark/baseline-profile-gradle-plugin/build.gradle
@@ -22,6 +22,15 @@
id("java-gradle-plugin")
}
+// This custom configuration ensures that dependencies used in tests with gradle test kit with
+// generated build.gradle are considered when determining which tests to run on CI due to a change.
+// For reference: b/281515796
+configurations {
+ neededForGradleTestKit {
+ canBeResolved = true
+ }
+}
+
dependencies {
implementation(gradleApi())
implementation(libs.androidGradlePluginz)
@@ -35,6 +44,8 @@
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
testImplementation(libs.truth)
+
+ neededForGradleTestKit(libs.androidGradlePluginz)
}
SdkResourceGenerator.generateForHostTest(project)
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
index 0ae17d0..00934a5 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
@@ -54,19 +54,30 @@
// These are the default global settings.
it.mergeIntoMain = null
it.baselineProfileOutputDir = "generated/baselineProfiles"
- it.enableR8BaselineProfileRewrite = false
+ it.baselineProfileRulesRewrite = null
+ it.dexLayoutOptimization = null
it.saveInSrc = true
it.automaticGenerationDuringBuild = false
}
/**
- * Controls the global [BaselineProfileVariantConfiguration.enableR8BaselineProfileRewrite].
+ * Controls the global [BaselineProfileVariantConfiguration.baselineProfileRulesRewrite].
* Note that this value is overridden by per variant configurations.
*/
- override var enableR8BaselineProfileRewrite: Boolean?
- get() = main.enableR8BaselineProfileRewrite
+ override var baselineProfileRulesRewrite: Boolean?
+ get() = main.baselineProfileRulesRewrite
set(value) {
- main.enableR8BaselineProfileRewrite = value
+ main.baselineProfileRulesRewrite = value
+ }
+
+ /**
+ * Controls the global [BaselineProfileVariantConfiguration.dexLayoutOptimization].
+ * Note that this value is overridden by per variant configurations.
+ */
+ override var dexLayoutOptimization: Boolean?
+ get() = main.dexLayoutOptimization
+ set(value) {
+ main.dexLayoutOptimization = value
}
/**
@@ -182,7 +193,13 @@
* TODO: This feature is experimental and currently not working properly.
* https://issuetracker.google.com/issue?id=271172067.
*/
- var enableR8BaselineProfileRewrite: Boolean?
+ var baselineProfileRulesRewrite: Boolean?
+
+ /**
+ * Enables R8 to optimize the primary dex file used to contain only classes utilized for
+ * startup.
+ */
+ var dexLayoutOptimization: Boolean?
/**
* Specifies whether generated baseline profiles should be stored in the src folder.
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
index 995c84d..b2792c6 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
@@ -20,6 +20,7 @@
import androidx.baselineprofile.gradle.consumer.task.MainGenerateBaselineProfileTask
import androidx.baselineprofile.gradle.consumer.task.MergeBaselineProfileTask
import androidx.baselineprofile.gradle.consumer.task.PrintConfigurationForVariantTask
+import androidx.baselineprofile.gradle.consumer.task.PrintMapPropertiesForVariantTask
import androidx.baselineprofile.gradle.consumer.task.maybeCreateGenerateTask
import androidx.baselineprofile.gradle.utils.AgpFeature
import androidx.baselineprofile.gradle.utils.AgpPlugin
@@ -169,10 +170,19 @@
variant = variant,
variantConfig = variantConfiguration
)
+ PrintMapPropertiesForVariantTask.registerForVariant(
+ project = project,
+ variant = variant
+ )
// Sets the r8 rewrite baseline profile for the non debuggable variant.
- if (variantConfiguration.enableR8BaselineProfileRewrite) {
- r8Utils.enableR8RulesRewriteForVariant(variant)
+ variantConfiguration.baselineProfileRulesRewrite?.let {
+ r8Utils.setRulesRewriteForVariantEnabled(variant, it)
+ }
+
+ // Sets the r8 startup dex optimization profile for the non debuggable variant.
+ variantConfiguration.dexLayoutOptimization?.let {
+ r8Utils.setDexLayoutOptimizationEnabled(variant, it)
}
// Check if this variant has any direct dependency
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/PerVariantConsumerExtensionManager.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/PerVariantConsumerExtensionManager.kt
index 6168f23..eb7b035 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/PerVariantConsumerExtensionManager.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/PerVariantConsumerExtensionManager.kt
@@ -52,8 +52,11 @@
val dependencies: List<Pair<Project, String?>>
get() = getMergedListForVariant(variant) { dependencies }
- val enableR8BaselineProfileRewrite: Boolean
- get() = getOverriddenValueForVariant(variant) { enableR8BaselineProfileRewrite }
+ val baselineProfileRulesRewrite: Boolean?
+ get() = getOverriddenValueForVariantAllowNull(variant) { baselineProfileRulesRewrite }
+
+ val dexLayoutOptimization: Boolean?
+ get() = getOverriddenValueForVariantAllowNull(variant) { dexLayoutOptimization }
val saveInSrc: Boolean
get() = getOverriddenValueForVariant(variant) { saveInSrc }
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintConfigurationForVariantTask.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintConfigurationForVariantTask.kt
index c9e2d08..d33f08a 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintConfigurationForVariantTask.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintConfigurationForVariantTask.kt
@@ -49,7 +49,8 @@
"""
mergeIntoMain=`${variantConfig.mergeIntoMain}`
baselineProfileOutputDir=`${variantConfig.baselineProfileOutputDir}`
- enableR8BaselineProfileRewrite=`${variantConfig.enableR8BaselineProfileRewrite}`
+ baselineProfileRulesRewrite=`${variantConfig.baselineProfileRulesRewrite}`
+ dexLayoutOptimization=`${variantConfig.dexLayoutOptimization}`
saveInSrc=`${variantConfig.saveInSrc}`
automaticGenerationDuringBuild=`${variantConfig.automaticGenerationDuringBuild}`
""".trimIndent()
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintMapPropertiesForVariantTask.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintMapPropertiesForVariantTask.kt
new file mode 100644
index 0000000..2318a42
--- /dev/null
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/PrintMapPropertiesForVariantTask.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.baselineprofile.gradle.consumer.task
+
+import androidx.baselineprofile.gradle.utils.maybeRegister
+import com.android.build.api.variant.Variant
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Only used for testing, this task does not have description or group so that it doesn't show up
+ * in the task list. It prints internal properties to facilitate assertions in integration tests.
+ */
+@DisableCachingByDefault(because = "Not worth caching. Used only for tests.")
+abstract class PrintMapPropertiesForVariantTask : DefaultTask() {
+
+ companion object {
+
+ private const val TASK_NAME_PREFIX = "printExperimentalPropertiesForVariant"
+
+ internal fun registerForVariant(
+ project: Project,
+ variant: Variant,
+ ) {
+ project
+ .tasks
+ .maybeRegister<PrintMapPropertiesForVariantTask>(TASK_NAME_PREFIX, variant.name) {
+ it.properties.set(variant.experimentalProperties)
+ }
+ }
+ }
+
+ @get: Input
+ abstract val properties: MapProperty<String, Any>
+
+ @TaskAction
+ fun exec() {
+ properties.get().forEach { logger.warn("${it.key}=${it.value}") }
+ }
+}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index 413406a..19d2d8d 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -50,16 +50,6 @@
private val maxAgpVersion: AndroidPluginVersion,
) {
- companion object {
- private val gradleSyncProps by lazy {
- listOf(
- "android.injected.build.model.v2",
- "android.injected.build.model.only",
- "android.injected.build.model.only.advanced",
- )
- }
- }
-
protected val logger: Logger
get() = project.logger
@@ -175,9 +165,7 @@
}
}
- protected fun isGradleSyncRunning() = gradleSyncProps.any {
- it in project.properties && project.properties[it].toString().toBoolean()
- }
+ protected fun isGradleSyncRunning() = project.isGradleSyncRunning()
protected fun afterVariants(block: () -> (Unit)) = afterVariantBlocks.add(block)
@@ -277,6 +265,18 @@
.findByType(com.android.build.gradle.TestExtension::class.java)
}
+private val gradleSyncProps by lazy {
+ listOf(
+ "android.injected.build.model.v2",
+ "android.injected.build.model.only",
+ "android.injected.build.model.only.advanced",
+ )
+}
+
+internal fun Project.isGradleSyncRunning() = gradleSyncProps.any {
+ it in project.properties && project.properties[it].toString().toBoolean()
+}
+
/**
* Enumerates the supported android plugins.
*/
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/R8Utils.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/R8Utils.kt
index 121dc35..a56a3de 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/R8Utils.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/R8Utils.kt
@@ -32,26 +32,56 @@
companion object {
private const val PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES =
"android.experimental.art-profile-r8-rewriting"
- private val AGP_MIN_VERSION = AndroidPluginVersion(8, 1, 0).alpha(8)
+ private const val PROPERTY_R8_DEX_LAYOUT_OPTIMIZATION =
+ "android.experimental.r8.dex-startup-optimization"
}
@Suppress("UnstableApiUsage")
- fun enableR8RulesRewriteForVariant(variant: Variant) {
+ fun setRulesRewriteForVariantEnabled(variant: Variant, value: Boolean) {
// Checks the AGP min version to support this.
- if (project.agpVersion() < AGP_MIN_VERSION) {
+ if (project.agpVersion() < AndroidPluginVersion(8, 1, 0).alpha(8)) {
+ if (!project.isGradleSyncRunning()) {
+ throw IllegalStateException(
+ """
+ Unable to set baseline profile rules rewrite property in module `${project.path}`
+ due to minimum AGP version requirement not met. This functionality requires at
+ least AGP version 8.1.0. Please check your module build.gradle file and ensure
+ the property `baselineProfileRulesRewrite` is not set.
+ """.trimIndent()
+ )
+ }
return
}
- // TODO: Checks the R8 version to support this.
-
// TODO: Note that currently there needs to be at least a baseline profile,
// even if empty. For this reason we always add a src set that points to
// an empty file. This can removed after b/271158087 is fixed.
- GenerateDummyBaselineProfileTask.setupForVariant(project, variant)
+ if (value) GenerateDummyBaselineProfileTask.setupForVariant(project, variant)
// Sets the experimental property
- variant.experimentalProperties.put(PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES, true)
+ variant.experimentalProperties.put(PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES, value)
+ }
+
+ @Suppress("UnstableApiUsage")
+ fun setDexLayoutOptimizationEnabled(variant: Variant, value: Boolean) {
+
+ // Checks the AGP min version to support this.
+ if (project.agpVersion() < AndroidPluginVersion(8, 1, 0).alpha(11)) {
+ if (!project.isGradleSyncRunning()) {
+ throw IllegalStateException(
+ """
+ Unable to set dex layout optimization property in module `${project.path}` due to
+ minimum AGP version requirement not met. This functionality requires at least AGP
+ version 8.1.0. Please check your module build.gradle file and ensure the property
+ `dexLayoutOptimization` is not set.
+ """.trimIndent()
+ )
+ }
+ }
+
+ // Sets the experimental property
+ variant.experimentalProperties.put(PROPERTY_R8_DEX_LAYOUT_OPTIMIZATION, value)
}
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
index a7e1cdf..2ab457a 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
@@ -27,6 +27,7 @@
import androidx.baselineprofile.gradle.utils.VariantProfile
import androidx.baselineprofile.gradle.utils.build
import androidx.baselineprofile.gradle.utils.buildAndAssertThatOutput
+import androidx.baselineprofile.gradle.utils.buildAndFailAndAssertThatOutput
import androidx.baselineprofile.gradle.utils.require
import androidx.baselineprofile.gradle.utils.requireInOrder
import com.google.common.truth.Truth.assertThat
@@ -286,9 +287,6 @@
androidPlugin = ANDROID_APPLICATION_PLUGIN,
flavors = true,
dependencyOnProducerProject = true,
- baselineProfileBlock = """
- enableR8BaselineProfileRewrite = false
- """.trimIndent(),
additionalGradleCodeBlock = """
androidComponents {
onVariants(selector()) { variant ->
@@ -343,7 +341,7 @@
}
@Test
- fun testR8RewriteBaselineProfilePropertySet() {
+ fun testExperimentalPropertiesNotSet() {
projectSetup.producer.setupWithFreeAndPaidFlavors(
freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
@@ -352,31 +350,20 @@
androidPlugin = ANDROID_LIBRARY_PLUGIN,
dependencyOnProducerProject = true,
flavors = true,
- buildTypeAnotherRelease = true,
- additionalGradleCodeBlock = """
- androidComponents {
- onVariants(selector()) { variant ->
- println(variant.name)
- tasks.register("print" + variant.name, PrintTask) { t ->
- def prop = "android.experimental.art-profile-r8-rewriting"
- if (prop in variant.experimentalProperties) {
- def value = variant.experimentalProperties[prop].get().toString()
- t.text.set( "r8-rw=" + value)
- } else {
- t.text.set( "r8-rw=false")
- }
- }
- }
- }
- """.trimIndent()
+ buildTypeAnotherRelease = true
)
arrayOf(
- "printFreeRelease",
- "printPaidRelease",
- "printFreeAnotherRelease",
- "printPaidAnotherRelease",
- ).forEach { gradleRunner.buildAndAssertThatOutput(it) { contains("r8-rw=false") } }
+ "printExperimentalPropertiesForVariantFreeRelease",
+ "printExperimentalPropertiesForVariantPaidRelease",
+ "printExperimentalPropertiesForVariantFreeAnotherRelease",
+ "printExperimentalPropertiesForVariantPaidAnotherRelease",
+ ).forEach {
+ gradleRunner.buildAndAssertThatOutput(it) {
+ doesNotContain("android.experimental.art-profile-r8-rewriting=")
+ doesNotContain("android.experimental.r8.dex-startup-optimization=")
+ }
+ }
}
@Test
@@ -672,7 +659,6 @@
baselineProfileBlock = """
// Global configuration
- enableR8BaselineProfileRewrite = false
saveInSrc = true
automaticGenerationDuringBuild = false
baselineProfileOutputDir = "generated/baselineProfiles"
@@ -681,14 +667,12 @@
// Per variant configuration overrides global configuration.
variants {
free {
- enableR8BaselineProfileRewrite = true
saveInSrc = false
automaticGenerationDuringBuild = true
baselineProfileOutputDir = "somefolder"
mergeIntoMain = false
}
paidRelease {
- enableR8BaselineProfileRewrite = true
saveInSrc = false
automaticGenerationDuringBuild = true
baselineProfileOutputDir = "someOtherfolder"
@@ -702,7 +686,6 @@
gradleRunner.buildAndAssertThatOutput(
"printBaselineProfileExtensionForVariantFreeRelease"
) {
- contains("enableR8BaselineProfileRewrite=`true`")
contains("saveInSrc=`false`")
contains("automaticGenerationDuringBuild=`true`")
contains("baselineProfileOutputDir=`somefolder`")
@@ -712,7 +695,6 @@
gradleRunner.buildAndAssertThatOutput(
"printBaselineProfileExtensionForVariantPaidRelease"
) {
- contains("enableR8BaselineProfileRewrite=`true`")
contains("saveInSrc=`false`")
contains("automaticGenerationDuringBuild=`true`")
contains("baselineProfileOutputDir=`someOtherfolder`")
@@ -732,7 +714,6 @@
baselineProfileBlock = """
// Global configuration
- enableR8BaselineProfileRewrite = false
saveInSrc = true
automaticGenerationDuringBuild = false
baselineProfileOutputDir = "generated/baselineProfiles"
@@ -741,14 +722,12 @@
// Per variant configuration overrides global configuration.
variants {
release {
- enableR8BaselineProfileRewrite = true
saveInSrc = false
automaticGenerationDuringBuild = true
baselineProfileOutputDir = "myReleaseFolder"
mergeIntoMain = false
}
paidRelease {
- enableR8BaselineProfileRewrite = true
saveInSrc = false
automaticGenerationDuringBuild = true
baselineProfileOutputDir = "someOtherfolder"
@@ -762,7 +741,6 @@
gradleRunner.buildAndAssertThatOutput(
"printBaselineProfileExtensionForVariantFreeRelease"
) {
- contains("enableR8BaselineProfileRewrite=`true`")
contains("saveInSrc=`false`")
contains("automaticGenerationDuringBuild=`true`")
contains("baselineProfileOutputDir=`myReleaseFolder`")
@@ -772,7 +750,6 @@
gradleRunner.buildAndAssertThatOutput(
"printBaselineProfileExtensionForVariantPaidRelease"
) {
- contains("enableR8BaselineProfileRewrite=`true`")
contains("saveInSrc=`false`")
contains("automaticGenerationDuringBuild=`true`")
contains("baselineProfileOutputDir=`someOtherfolder`")
@@ -1108,6 +1085,60 @@
assertThat(notFound).isEmpty()
}
}
+
+ @Test
+ fun testRulesRewriteExperimentalPropertiesSet() {
+ projectSetup.producer.setupWithFreeAndPaidFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
+ )
+ projectSetup.consumer.setup(
+ androidPlugin = ANDROID_LIBRARY_PLUGIN,
+ dependencyOnProducerProject = true,
+ flavors = true,
+ buildTypeAnotherRelease = true,
+ baselineProfileBlock = """
+ baselineProfileRulesRewrite = true
+ """.trimIndent()
+ )
+ arrayOf(
+ "printExperimentalPropertiesForVariantFreeRelease",
+ "printExperimentalPropertiesForVariantPaidRelease",
+ "printExperimentalPropertiesForVariantFreeAnotherRelease",
+ "printExperimentalPropertiesForVariantPaidAnotherRelease",
+ ).forEach {
+ projectSetup.consumer.gradleRunner.buildAndFailAndAssertThatOutput(it) {
+ contains("Unable to set baseline profile rules rewrite property")
+ }
+ }
+ }
+
+ @Test
+ fun testDexLayoutOptimizationExperimentalPropertiesSet() {
+ projectSetup.producer.setupWithFreeAndPaidFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
+ )
+ projectSetup.consumer.setup(
+ androidPlugin = ANDROID_LIBRARY_PLUGIN,
+ dependencyOnProducerProject = true,
+ flavors = true,
+ buildTypeAnotherRelease = true,
+ baselineProfileBlock = """
+ dexLayoutOptimization = true
+ """.trimIndent()
+ )
+ arrayOf(
+ "printExperimentalPropertiesForVariantFreeRelease",
+ "printExperimentalPropertiesForVariantPaidRelease",
+ "printExperimentalPropertiesForVariantFreeAnotherRelease",
+ "printExperimentalPropertiesForVariantPaidAnotherRelease",
+ ).forEach {
+ projectSetup.consumer.gradleRunner.buildAndFailAndAssertThatOutput(it) {
+ contains(" Unable to set dex layout optimization property")
+ }
+ }
+ }
}
@RunWith(JUnit4::class)
@@ -1253,4 +1284,35 @@
assertThat(notFound).isEmpty()
}
}
+
+ @Test
+ fun testExperimentalPropertiesSet() {
+ projectSetup.producer.setupWithFreeAndPaidFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
+ )
+ projectSetup.consumer.setup(
+ androidPlugin = ANDROID_LIBRARY_PLUGIN,
+ dependencyOnProducerProject = true,
+ flavors = true,
+ buildTypeAnotherRelease = true,
+ baselineProfileBlock = """
+ baselineProfileRulesRewrite = true
+ dexLayoutOptimization = true
+ """.trimIndent()
+ )
+
+ arrayOf(
+ "printExperimentalPropertiesForVariantFreeRelease",
+ "printExperimentalPropertiesForVariantPaidRelease",
+ "printExperimentalPropertiesForVariantFreeAnotherRelease",
+ "printExperimentalPropertiesForVariantPaidAnotherRelease",
+ ).forEach {
+ projectSetup.consumer.gradleRunner.buildAndAssertThatOutput(it) {
+ // These properties are ignored in agp 8.0
+ contains("android.experimental.art-profile-r8-rewriting=true")
+ contains("android.experimental.r8.dex-startup-optimization=true")
+ }
+ }
+ }
}
diff --git a/benchmark/benchmark-common/api/public_plus_experimental_current.txt b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
index a5a3dc8..c948ed9 100644
--- a/benchmark/benchmark-common/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
@@ -31,6 +31,21 @@
@kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
}
+ @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public abstract sealed class PerfettoConfig {
+ }
+
+ public static final class PerfettoConfig.Binary extends androidx.benchmark.perfetto.PerfettoConfig {
+ ctor public PerfettoConfig.Binary(byte[] bytes);
+ method public byte[] getBytes();
+ property public final byte[] bytes;
+ }
+
+ public static final class PerfettoConfig.Text extends androidx.benchmark.perfetto.PerfettoConfig {
+ ctor public PerfettoConfig.Text(String text);
+ method public String getText();
+ property public final String text;
+ }
+
@RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
ctor public PerfettoTrace(String path);
method public String getPath();
@@ -38,6 +53,10 @@
method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
property public final String path;
field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
}
@@ -47,6 +66,10 @@
method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
}
}
diff --git a/benchmark/benchmark-common/build.gradle b/benchmark/benchmark-common/build.gradle
index ab830a8..e8f2eca 100644
--- a/benchmark/benchmark-common/build.gradle
+++ b/benchmark/benchmark-common/build.gradle
@@ -72,6 +72,15 @@
description = "Android Benchmark - Common"
}
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ // Enable using experimental APIs from within same version group
+ freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
+ ]
+ }
+}
+
// https://github.com/square/wire/issues/1947
// Remove when we upgrade to fixed wire library
afterEvaluate {
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt
index 95b10d3..a919048 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoHelperTest.kt
@@ -17,6 +17,7 @@
package androidx.benchmark
import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -52,7 +53,7 @@
// start perfetto
val capture = PerfettoCapture(unbundled)
- capture.start(listOf(Packages.TEST))
+ capture.start(PerfettoConfig.Benchmark(listOf(Packages.TEST)))
// should be at least one perfetto process
assertNotEquals(illegal = listOf(), actual = getPerfettoPids())
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
index 8eba619..affb068 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
@@ -17,13 +17,19 @@
package androidx.benchmark
import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.benchmark.perfetto.perfettoConfig
+import androidx.benchmark.perfetto.validateAndEncode
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.Assume.assumeTrue
import org.junit.Test
@@ -34,6 +40,7 @@
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 23)
class PerfettoTraceTest {
+
@Test
fun record_basic() {
assumeTrue(PerfettoHelper.isAbiSupported())
@@ -51,6 +58,94 @@
"$perfettoTrace didn't match!"
}
}
+
+ private fun verifyRecordSuccess(
+ config: PerfettoConfig
+ ) {
+ var perfettoTrace: PerfettoTrace? = null
+ val label = "successTrace${config.javaClass.simpleName}"
+ PerfettoTrace.record(
+ fileLabel = label,
+ config = config,
+ traceCallback = { trace ->
+ perfettoTrace = trace
+ }
+ ) {
+ // noop
+ }
+ assertNotNull(perfettoTrace)
+ assert(perfettoTrace!!.path.matches(Regex(".*/${label}_[0-9-]+.perfetto-trace"))) {
+ "$perfettoTrace didn't match!"
+ }
+ }
+
+ private fun verifyRecordFails(
+ config: PerfettoConfig
+ ) {
+ var perfettoTrace: PerfettoTrace? = null
+ val exception = assertFailsWith<IllegalStateException> {
+ PerfettoTrace.record(
+ fileLabel = "failTrace",
+ config = config,
+ traceCallback = { trace ->
+ perfettoTrace = trace
+ }
+ ) {
+ // noop
+ }
+ }
+ assertTrue(exception.message!!.contains("Perfetto unexpected exit code"))
+ assertNull(perfettoTrace)
+ }
+
+ @Test
+ fun record_invalidText() = verifyRecordFails(PerfettoConfig.Text("INVALID"))
+
+ @Test
+ fun record_invalidBinary() = verifyRecordFails(PerfettoConfig.Binary(byteArrayOf(1, 0, 1)))
+
+ @Test
+ fun record_validText() = verifyRecordSuccess(PerfettoConfig.Text("""
+ # basic config generated from https://ui.perfetto.dev/#!/record
+ buffers: {
+ size_kb: 63488
+ fill_policy: RING_BUFFER
+ }
+ buffers: {
+ size_kb: 2048
+ fill_policy: RING_BUFFER
+ }
+ data_sources: {
+ config {
+ name: "linux.ftrace"
+ ftrace_config {
+ ftrace_events: "ftrace/print"
+ atrace_categories: "am"
+ atrace_categories: "dalvik"
+ atrace_categories: "gfx"
+ atrace_categories: "view"
+ atrace_categories: "wm"
+ }
+ }
+ }
+ duration_ms: 10000
+ flush_period_ms: 30000
+ incremental_state_config {
+ clear_period_ms: 5000
+ }
+ """.trimIndent()))
+
+ @Test
+ fun record_validBinary() = verifyRecordSuccess(
+ PerfettoConfig.Binary(
+ perfettoConfig(
+ atraceApps = listOf(
+ InstrumentationRegistry.getInstrumentation().targetContext.packageName
+ )
+ ).validateAndEncode()
+ )
+ )
+
@Test
fun record_reentrant() {
assumeTrue(PerfettoHelper.isAbiSupported())
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index 26a4da0..e99d5a1 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -53,31 +53,24 @@
private val helper: PerfettoHelper = PerfettoHelper(unbundled)
- public fun isRunning() = helper.isRunning()
+ fun isRunning() = helper.isRunning()
/**
* Start collecting perfetto trace.
- *
- * TODO: provide configuration options
*/
- public fun start(packages: List<String>) = userspaceTrace("start perfetto") {
- // Write binary proto to dir that shell can read
- // TODO: cache on disk
+ fun start(config: PerfettoConfig) = userspaceTrace("start perfetto") {
+ // Write config proto to dir that shell can read
+ // We use `.pb` even with textproto so we'll only ever have one file
val configProtoFile = File(Outputs.dirUsableByAppAndShell, "trace_config.pb")
try {
userspaceTrace("write config") {
- val atraceApps = if (Build.VERSION.SDK_INT <= 28 || packages.isEmpty()) {
- packages
- } else {
- listOf("*")
- }
- configProtoFile.writeBytes(perfettoConfig(atraceApps).validateAndEncode())
+ config.writeTo(configProtoFile)
if (Outputs.forceFilesForShellAccessible) {
configProtoFile.setReadable(true, /* ownerOnly = */ false)
}
}
userspaceTrace("start perfetto process") {
- helper.startCollecting(configProtoFile.absolutePath, false)
+ helper.startCollecting(configProtoFile.absolutePath, config.isTextProto)
}
} finally {
configProtoFile.delete()
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 010cde4..bb1e825 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -54,23 +54,21 @@
@RequiresApi(23)
private fun start(
- appTagPackages: List<String>,
+ config: PerfettoConfig,
userspaceTracingPackage: String?
): Boolean {
capture?.apply {
- if (Build.VERSION.SDK_INT >= 23) {
- Log.d(LOG_TAG, "Recording perfetto trace")
- if (userspaceTracingPackage != null &&
- Build.VERSION.SDK_INT >= 30
- ) {
- val result = enableAndroidxTracingPerfetto(
- targetPackage = userspaceTracingPackage,
- provideBinariesIfMissing = true
- ) ?: "Success"
- Log.d(LOG_TAG, "Enable full tracing result=$result")
- }
- start(appTagPackages)
+ Log.d(LOG_TAG, "Recording perfetto trace")
+ if (userspaceTracingPackage != null &&
+ Build.VERSION.SDK_INT >= 30
+ ) {
+ val result = enableAndroidxTracingPerfetto(
+ targetPackage = userspaceTracingPackage,
+ provideBinariesIfMissing = true
+ ) ?: "Success"
+ Log.d(LOG_TAG, "Enable full tracing result=$result")
}
+ start(config)
}
return true
@@ -94,7 +92,7 @@
fun record(
fileLabel: String,
- appTagPackages: List<String>,
+ config: PerfettoConfig,
userspaceTracingPackage: String?,
traceCallback: ((String) -> Unit)? = null,
block: () -> Unit
@@ -125,7 +123,7 @@
val path: String
try {
propOverride?.forceValue()
- start(appTagPackages, userspaceTracingPackage)
+ start(config, userspaceTracingPackage)
try {
block()
} finally {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 6a2283e..2ea9b46 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -20,6 +20,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.Shell
+import java.io.File
import perfetto.protos.DataSourceConfig
import perfetto.protos.AndroidPowerConfig
import perfetto.protos.FtraceConfig
@@ -30,6 +31,73 @@
import perfetto.protos.TraceConfig.BufferConfig
import perfetto.protos.TraceConfig.BufferConfig.FillPolicy
+/**
+ * Configuration for Perfetto trace recording.
+ *
+ * For more info, see https://perfetto.dev/docs/concepts/config
+ */
+@ExperimentalPerfettoCaptureApi
+sealed class PerfettoConfig constructor(
+ internal val isTextProto: Boolean
+) {
+ @RequiresApi(23)
+ internal abstract fun writeTo(file: File)
+
+ /**
+ * Binary representation of a Perfetto config proto.
+ *
+ * This can be generated by a proto library, together with the definition here:
+ */
+ class Binary(
+ val bytes: ByteArray
+ ) : PerfettoConfig(isTextProto = false) {
+ @RequiresApi(23)
+ override fun writeTo(file: File) {
+ file.writeBytes(bytes)
+ }
+ }
+
+ /**
+ * TextProto representation of a Perfetto config.
+ *
+ * This can be generated with https://ui.perfetto.dev/#!/record/
+ *
+ * Note: this format is not recommended for long term use - the [Binary] proto
+ * representation is more likely to remain stable over time, across Perfetto/Android OS
+ * versions. For more information, see
+ * [the Perfetto documentation](https://perfetto.dev/docs/concepts/config#pbtx-vs-binary-format).
+ */
+ class Text(
+ val text: String
+ ) : PerfettoConfig(isTextProto = true) {
+ @RequiresApi(23)
+ override fun writeTo(file: File) {
+ file.writeText(text)
+ }
+ }
+
+ /**
+ * Benchmark defined config for perfetto trace capture, used by benchmark/macrobenchmark.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ class Benchmark(
+ private val appTagPackages: List<String>
+ ) : PerfettoConfig(isTextProto = false) {
+ @RequiresApi(23)
+ override fun writeTo(file: File) {
+ file.writeBytes(
+ perfettoConfig(
+ atraceApps = if (Build.VERSION.SDK_INT <= 28 || appTagPackages.isEmpty()) {
+ appTagPackages
+ } else {
+ listOf("*")
+ }
+ ).validateAndEncode()
+ )
+ }
+ }
+}
+
private fun ftraceDataSource(
atraceApps: List<String>
) = TraceConfig.DataSource(
@@ -165,7 +233,7 @@
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun perfettoConfig(
+internal fun perfettoConfig(
atraceApps: List<String>
) = TraceConfig(
buffers = listOf(
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
index 5e2c29a..656ba2f 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
@@ -83,15 +83,79 @@
* Block to be traced.
*/
block: () -> Unit
+ ) = record(
+ fileLabel = fileLabel,
+ config = PerfettoConfig.Benchmark(appTagPackages),
+ userspaceTracingPackage = userspaceTracingPackage,
+ traceCallback = traceCallback,
+ block = block
+ )
+
+ /**
+ * Record a Perfetto System Trace for the specified [block], with a fully custom Perfetto
+ * config, either text or binary.
+ *
+ * ```
+ * PerfettoTrace.record("myTrace", config = """...""") {
+ * // content in here is traced to myTrace_<timestamp>.perfetto_trace
+ * }
+ * ```
+ *
+ * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
+ * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
+ *
+ * If the block throws, the trace is still captured and passed to [traceCallback].
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun record(
+ /**
+ * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
+ *
+ * This timestamp is used for uniqueness when trace files are pulled automatically to
+ * Studio.
+ */
+ fileLabel: String,
+ /**
+ * Trace recording configuration.
+ */
+ config: PerfettoConfig,
+ /**
+ * Process to emphasize in the tracing UI.
+ *
+ * Used to emphasize the target process, e.g. by pre-populating Studio trace viewer
+ * process selection.
+ *
+ * Defaults to the test's target process. Note that for self-instrumenting tests that
+ * measure another app, you must pass that target app package.
+ */
+ highlightPackage: String =
+ InstrumentationRegistry.getInstrumentation().targetContext.packageName,
+ /**
+ * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`,
+ * ignored below API 30.
+ *
+ * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is
+ * currently experimental.
+ */
+ userspaceTracingPackage: String? = null,
+ /**
+ * Callback for trace capture.
+ *
+ * This callback allows you to process the trace even if the block throws, e.g. during
+ * a test failure.
+ */
+ traceCallback: ((PerfettoTrace) -> Unit)? = null,
+ /**
+ * Block to be traced.
+ */
+ block: () -> Unit
) {
PerfettoCaptureWrapper().record(
fileLabel = fileLabel,
- appTagPackages = appTagPackages,
- userspaceTracingPackage = userspaceTracingPackage,
+ config,
+ userspaceTracingPackage,
traceCallback = { path ->
- // emphasize the first package in the package list, or target package otherwise
- val highlightPackage = appTagPackages.firstOrNull()
- ?: InstrumentationRegistry.getInstrumentation().targetContext.packageName
File(path).appendUiState(
UiState(
timelineStart = null,
diff --git a/benchmark/benchmark-junit4/build.gradle b/benchmark/benchmark-junit4/build.gradle
index baf3b33..4db5a73 100644
--- a/benchmark/benchmark-junit4/build.gradle
+++ b/benchmark/benchmark-junit4/build.gradle
@@ -15,6 +15,7 @@
*/
import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -51,3 +52,12 @@
inceptionYear = "2019"
description = "Android Benchmark - JUnit4"
}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ // Enable using experimental APIs from within same version group
+ freeCompilerArgs += [
+ "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
+ ]
+ }
+}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 1be0375..0c686fe 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -23,6 +23,7 @@
import androidx.benchmark.BenchmarkState
import androidx.benchmark.UserspaceTracing
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.UiState
import androidx.benchmark.perfetto.appendUiState
import androidx.test.platform.app.InstrumentationRegistry
@@ -212,7 +213,7 @@
val tracePath = PerfettoCaptureWrapper().record(
fileLabel = uniqueName,
- appTagPackages = packages,
+ config = PerfettoConfig.Benchmark(packages),
userspaceTracingPackage = null
) {
UserspaceTracing.commitToTrace() // clear buffer
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 0bb6d0b..538d68d 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -158,6 +158,13 @@
}
}
+androidx {
+ deviceTests {
+ targetAppProject = project(":benchmark:integration-tests:macrobenchmark-target")
+ targetAppVariant = "release"
+ }
+}
+
// Define a task dependency so the app is installed before we run macro benchmarks.
afterEvaluate {
// `:benchmark:integration-tests:macrobenchmark-target:installRelease` is not in the compose
diff --git a/benchmark/benchmark-macro/lint-baseline.xml b/benchmark/benchmark-macro/lint-baseline.xml
index fb8dd8f..9433540 100644
--- a/benchmark/benchmark-macro/lint-baseline.xml
+++ b/benchmark/benchmark-macro/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.0-alpha11" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-alpha11)" variant="all" version="8.1.0-alpha11">
+<issues format="6" by="lint 8.1.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta01)" variant="all" version="8.1.0-beta01">
<issue
id="BanThreadSleep"
@@ -22,15 +22,6 @@
<issue
id="BanThreadSleep"
message="Uses Thread.sleep()"
- errorLine1=" Thread.sleep(Arguments.killProcessDelayMillis)"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/benchmark/macro/BaselineProfiles.kt"/>
- </issue>
-
- <issue
- id="BanThreadSleep"
- message="Uses Thread.sleep()"
errorLine1=" Thread.sleep(5000)"
errorLine2=" ~~~~~">
<location
@@ -118,4 +109,13 @@
file="src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt"/>
</issue>
+ <issue
+ id="PrereleaseSdkCoreDependency"
+ message="Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core"
+ errorLine1=" if (BuildCompat.isAtLeastU() || Shell.isSessionRooted()) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/benchmark/macro/CompilationMode.kt"/>
+ </issue>
+
</issues>
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index dc048f2c..cf8d947 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -23,6 +23,7 @@
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Outputs
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -293,13 +294,16 @@
metric.configure(packageName)
val tracePath = PerfettoCaptureWrapper().record(
fileLabel = packageName,
- // note - packageName may be this package, so we convert to set then list to make unique
- // and on API 23 and below, we use reflection to trace instead within this process
- appTagPackages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
- listOf(packageName, Packages.TEST)
- } else {
- listOf(packageName)
- },
+ config = PerfettoConfig.Benchmark(
+ // note - packageName may be this package, so we convert to set then list to make unique
+ // and on API 23 and below, we use reflection to trace instead within this process
+ appTagPackages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
+ listOf(packageName, Packages.TEST)
+ } else {
+ listOf(packageName)
+ },
+
+ ),
userspaceTracingPackage = packageName,
block = measureBlock
)!!
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
index 657a201..ec0e17c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
@@ -19,6 +19,7 @@
import androidx.benchmark.macro.FileLinkingRule
import androidx.benchmark.macro.Packages
import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.benchmark.perfetto.PerfettoTraceProcessor
@@ -66,7 +67,7 @@
verifyTraceEnable(false)
- perfettoCapture.start(listOf(Packages.TEST))
+ perfettoCapture.start(PerfettoConfig.Benchmark(listOf(Packages.TEST)))
assertTrue(
Trace.isEnabled(),
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index 8797a72..a8fb5bf 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -16,10 +16,12 @@
package androidx.benchmark.macro.perfetto
+import android.annotation.SuppressLint
import android.os.Build
import androidx.benchmark.macro.FileLinkingRule
import androidx.benchmark.macro.Packages
import androidx.benchmark.perfetto.PerfettoCapture
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoHelper
import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOWEST_BUNDLED_VERSION_SUPPORTED
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
@@ -85,6 +87,7 @@
captureAndValidateTrace(unbundled = true)
}
+ @SuppressLint("BanThreadSleep")
private fun captureAndValidateTrace(unbundled: Boolean) {
assumeTrue(isAbiSupported())
@@ -93,7 +96,7 @@
verifyTraceEnable(false)
- perfettoCapture.start(listOf(Packages.TEST))
+ perfettoCapture.start(PerfettoConfig.Benchmark(listOf(Packages.TEST)))
if (!Trace.isEnabled()) {
// Should be available immediately, but let's wait a while to see if it works slowly.
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index c220445..2a4020b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -33,6 +33,7 @@
import androidx.benchmark.checkAndGetSuppressionState
import androidx.benchmark.conditionalError
import androidx.benchmark.perfetto.PerfettoCaptureWrapper
+import androidx.benchmark.perfetto.PerfettoConfig
import androidx.benchmark.perfetto.PerfettoTrace
import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.benchmark.perfetto.UiState
@@ -214,22 +215,23 @@
val iterString = iteration.toString().padStart(3, '0')
val tracePath = perfettoCollector.record(
fileLabel = "${uniqueName}_iter$iterString",
-
- /**
- * Prior to API 24, every package name was joined into a single setprop which
- * can overflow, and disable *ALL* app level tracing.
- *
- * For safety here, we only trace the macrobench package on newer platforms,
- * and use reflection in the macrobench test process to trace important
- * sections
- *
- * @see androidx.benchmark.macro.perfetto.ForceTracing
- */
- appTagPackages = if (Build.VERSION.SDK_INT >= 24) {
- listOf(packageName, macrobenchPackageName)
- } else {
- listOf(packageName)
- },
+ config = PerfettoConfig.Benchmark(
+ /**
+ * Prior to API 24, every package name was joined into a single setprop
+ * which can overflow, and disable *ALL* app level tracing.
+ *
+ * For safety here, we only trace the macrobench package on newer platforms,
+ * and use reflection in the macrobench test process to trace important
+ * sections
+ *
+ * @see androidx.benchmark.macro.perfetto.ForceTracing
+ */
+ appTagPackages = if (Build.VERSION.SDK_INT >= 24) {
+ listOf(packageName, macrobenchPackageName)
+ } else {
+ listOf(packageName)
+ },
+ ),
userspaceTracingPackage = userspaceTracingPackage
) {
try {
diff --git a/benchmark/integration-tests/baselineprofile-flavors-producer/build.gradle b/benchmark/integration-tests/baselineprofile-flavors-producer/build.gradle
index 0634f31..05d1c99 100644
--- a/benchmark/integration-tests/baselineprofile-flavors-producer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-flavors-producer/build.gradle
@@ -61,5 +61,7 @@
}
androidx {
- disableDeviceTests = true
+ deviceTests {
+ enabled = false
+ }
}
diff --git a/benchmark/integration-tests/baselineprofile-library-producer/build.gradle b/benchmark/integration-tests/baselineprofile-library-producer/build.gradle
index b6cee42..44b3bda 100644
--- a/benchmark/integration-tests/baselineprofile-library-producer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-library-producer/build.gradle
@@ -56,5 +56,7 @@
}
androidx {
- disableDeviceTests = true
+ deviceTests {
+ enabled = false
+ }
}
diff --git a/benchmark/integration-tests/baselineprofile-producer/build.gradle b/benchmark/integration-tests/baselineprofile-producer/build.gradle
index a696a90..e04a270 100644
--- a/benchmark/integration-tests/baselineprofile-producer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-producer/build.gradle
@@ -56,5 +56,7 @@
}
androidx {
- disableDeviceTests = true
+ deviceTests {
+ enabled = false
+ }
}
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index a62c063..58d7ee0 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -19,7 +19,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -28,34 +28,31 @@
minSdkVersion 23
}
sourceSets {
- androidTest.assets.srcDirs += new File(
+ main.assets.srcDirs += new File(
SupportConfigKt.getPrebuiltsRoot(project),
"androidx/traceprocessor/testdata"
)
}
namespace "androidx.benchmark.integration.macrobenchmark"
+ targetProjectPath = ":benchmark:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(project(":tracing:tracing-ktx"))
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.testExtTruth)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":benchmark:integration-tests:macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":benchmark:integration-tests:macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(project(":tracing:tracing-ktx"))
+ implementation(libs.kotlinTest)
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
+ implementation(libs.testExtTruth)
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/BaselineProfileRuleTest.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/GithubBrowserBaselineProfile.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/GithubBrowserBaselineProfile.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/GithubBrowserBaselineProfile.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/GithubBrowserBaselineProfile.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/GridBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/GridBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/GridBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/GridBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SeparateProcessBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SingleColorPowerBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SingleColorPowerBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SingleColorPowerBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SingleColorPowerBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupPressHomeBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/StartupPressHomeBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupPressHomeBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/StartupPressHomeBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SystemUiBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SystemUiBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SystemUiBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/SystemUiBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBaselineProfile.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBaselineProfile.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBaselineProfile.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBaselineProfile.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
similarity index 100%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
similarity index 92%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
rename to benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
index 2e75f3a..8090aa3 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
@@ -20,19 +20,27 @@
import androidx.benchmark.macro.StartupMode;
import androidx.benchmark.macro.StartupTimingMetric;
import androidx.benchmark.macro.junit4.MacrobenchmarkRule;
+import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
+import kotlin.Unit;
+
import org.junit.Rule;
import org.junit.Test;
import java.util.Collections;
-import kotlin.Unit;
-
+/**
+ * Simple benchmark for startup in java.
+ */
+@LargeTest
public class TrivialStartupJavaBenchmark {
@Rule
public MacrobenchmarkRule mBenchmarkRule = new MacrobenchmarkRule();
+ /**
+ * Benchmark for startup.
+ */
@Test
@SdkSuppress(minSdkVersion = 29)
public void startup() {
diff --git a/benchmark/integration-tests/test-module-sample/build.gradle b/benchmark/integration-tests/test-module-sample/build.gradle
deleted file mode 100644
index 5351938..0000000
--- a/benchmark/integration-tests/test-module-sample/build.gradle
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 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.
- */
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.test")
- id("kotlin-android")
-}
-
-android {
- targetProjectPath = ":benchmark:integration-tests:macrobenchmark-target"
- experimentalProperties["android.experimental.self-instrumenting"] = true
-
- // note: below is optional, mimicing eventual benchmark module setup
- buildTypes {
- release {
- debuggable = true
- }
- }
-
- defaultConfig {
- minSdkVersion 28
- }
- namespace "androidx.benchmark.integration.testmodulesample"
-}
-
-// note: below is optional, mimicing eventual benchmark module setup
-androidComponents {
- beforeVariants(selector().all()) {
- // Enable only the release buildType, since we only want to measure
- // release build performance
- getLogger().info("setting enable for variant" + name + ", buildType " + buildType)
- enabled = buildType == 'release'
- }
-}
-
-dependencies {
- implementation(libs.kotlinStdlib)
- implementation(libs.testRules)
- implementation(libs.testExtJunit)
- implementation(libs.testCore)
- implementation(libs.testRunner)
-}
diff --git a/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt b/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt
deleted file mode 100644
index aac7497..0000000
--- a/benchmark/integration-tests/test-module-sample/src/main/java/androidx/benchmark/integration/macrobenchmark/TrivialTestModuleTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import android.content.pm.PackageManager
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class TrivialTestModuleTest {
- @Test
- fun targetPackage() {
- // if self-instrumenting wasn't used, this would instrument the target app,
- // and we'd see TargetPackage instead
- assertEquals(
- TestPackage,
- InstrumentationRegistry.getInstrumentation().targetContext.packageName
- )
- }
-
- @Test
- fun testPackage() {
- assertEquals(
- TestPackage,
- InstrumentationRegistry.getInstrumentation().context.packageName
- )
- }
-
- @Ignore // b/202321897
- @Test
- @Suppress("DEPRECATION")
- fun targetPackageInstalled() {
- val pm = InstrumentationRegistry.getInstrumentation().context.packageManager
- try {
- pm.getApplicationInfo(TargetPackage, 0)
- } catch (notFoundException: PackageManager.NameNotFoundException) {
- throw AssertionError(
- "Unable to find target package $TargetPackage, is it installed?",
- notFoundException
- )
- }
- }
-
- companion object {
- const val TargetPackage = "androidx.benchmark.integration.macrobenchmark.target"
- const val TestPackage = "androidx.benchmark.integration.testmodulesample"
- }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 8c0a117..242b964 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -23,13 +23,16 @@
import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
/**
* Extension for [AndroidXImplPlugin] that's responsible for holding configuration options.
*/
-open class AndroidXExtension(val project: Project) {
+abstract class AndroidXExtension(val project: Project) : ExtensionAware {
+
@JvmField
val LibraryVersions: Map<String, Version>
@@ -45,6 +48,8 @@
private val versionService: LibraryVersionsService
+ val deviceTests = DeviceTests.register(project.extensions)
+
init {
val tomlFileName = "libraryversions.toml"
val toml = lazyReadFile(tomlFileName)
@@ -376,8 +381,6 @@
var metalavaK2UastEnabled = false
- var disableDeviceTests = false
-
val additionalDeviceTestApkKeys = mutableListOf<String>()
val additionalDeviceTestTags: MutableList<String> by lazy {
@@ -422,3 +425,18 @@
var name: String? = null
var url: String? = null
}
+
+abstract class DeviceTests {
+
+ companion object {
+ private const val EXTENSION_NAME = "deviceTests"
+ internal fun register(extensions: ExtensionContainer): DeviceTests {
+ return extensions.findByType(DeviceTests::class.java)
+ ?: extensions.create(EXTENSION_NAME, DeviceTests::class.java)
+ }
+ }
+
+ var enabled = true
+ var targetAppProject: Project? = null
+ var targetAppVariant = "debug"
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 3fae48c..d5f73f51 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -35,6 +35,8 @@
import androidx.build.resources.configurePublicResourcesStub
import androidx.build.sbom.validateAllArchiveInputsRecognized
import androidx.build.studio.StudioTask
+import androidx.build.testConfiguration.ModuleInfoGenerator
+import androidx.build.testConfiguration.TestModule
import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
import androidx.build.testConfiguration.configureTestConfigGeneration
import com.android.build.api.artifact.SingleArtifact
@@ -227,7 +229,19 @@
task.inputs.property("ignoreFailures", ignoreFailures)
val xmlReportDestDir = project.getHostTestResultDirectory()
- val archiveName = "${project.path}:${task.name}.zip"
+ val testName = "${project.path}:${task.name}"
+ project.rootProject.tasks.named("createModuleInfo").configure {
+ it as ModuleInfoGenerator
+ it.testModules.add(
+ TestModule(
+ name = testName,
+ path = listOf(
+ project.projectDir.toRelativeString(project.getSupportRootFolder())
+ )
+ )
+ )
+ }
+ val archiveName = "$testName.zip"
if (project.isDisplayTestOutput()) {
// Enable tracing to see results in command line
task.testLogging.apply {
@@ -323,7 +337,7 @@
private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) {
project.extensions.getByType<AppExtension>().apply {
configureAndroidBaseOptions(project, androidXExtension)
- configureAndroidApplicationOptions(project)
+ configureAndroidApplicationOptions(project, androidXExtension)
}
project.extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
@@ -347,6 +361,7 @@
) {
project.extensions.getByType<TestExtension>().apply {
configureAndroidBaseOptions(project, androidXExtension)
+ project.addAppApkToTestConfigGeneration(androidXExtension)
}
project.configureJavaCompilationWarnings(androidXExtension)
@@ -410,6 +425,7 @@
) {
val libraryExtension = project.extensions.getByType<LibraryExtension>().apply {
configureAndroidBaseOptions(project, androidXExtension)
+ project.addAppApkToTestConfigGeneration(androidXExtension)
configureAndroidLibraryOptions(project, androidXExtension)
// Make sure the main Kotlin source set doesn't contain anything under src/main/kotlin.
@@ -808,13 +824,16 @@
}
}
- private fun AppExtension.configureAndroidApplicationOptions(project: Project) {
+ private fun AppExtension.configureAndroidApplicationOptions(
+ project: Project,
+ androidXExtension: AndroidXExtension
+ ) {
defaultConfig.apply {
versionCode = 1
versionName = "1.0"
}
- project.addAppApkToTestConfigGeneration()
+ project.addAppApkToTestConfigGeneration(androidXExtension)
project.addAppApkToFtlRunner()
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index f6c6252..f6afa85 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -90,7 +90,7 @@
fun desktop(
block: Action<KotlinJvmTarget>? = null
): KotlinJvmTarget? {
- return if (project.enableJvm()) {
+ return if (project.enableDesktop()) {
kotlinExtension.jvm("desktop") {
block?.execute(this)
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index 1960148..3f8005e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -19,10 +19,13 @@
import androidx.build.dependencyTracker.ProjectSubset
import com.android.build.api.variant.BuiltArtifactsLoader
import java.io.File
+import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
+import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
@@ -42,13 +45,19 @@
* This config gets ingested by Tradefed.
*/
@DisableCachingByDefault(because = "Doesn't benefit from caching")
-abstract class GenerateTestConfigurationTask : DefaultTask() {
+abstract class GenerateTestConfigurationTask @Inject constructor(
+ private val objects: ObjectFactory
+) : DefaultTask() {
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val appFolder: DirectoryProperty
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val appFileCollection: ConfigurableFileCollection
+
@get:Internal
abstract val appLoader: Property<BuiltArtifactsLoader>
@@ -129,7 +138,20 @@
val configBuilder = ConfigBuilder()
configBuilder.configName = outputFile.asFile.get().name
if (appLoader.isPresent) {
- val appApk = appLoader.get().load(appFolder.get())
+
+ // Decides where to load the app apk from, depending on whether appFolder or
+ // appFileCollection has been set.
+ val appDir = if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
+ appFolder.get()
+ } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
+ objects.directoryProperty().also { it.set(appFileCollection.files.first()) }.get()
+ } else {
+ throw IllegalStateException("""
+ App apk not specified or both appFileCollection and appFolder specified.
+ """.trimIndent())
+ }
+
+ val appApk = appLoader.get().load(appDir)
?: throw RuntimeException("Cannot load required APK for task: $name")
// We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
val appApkBuiltArtifact = appApk.elements.single()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
index e75e8a9..c6bce06 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
@@ -52,7 +52,7 @@
}
/**
- * Register two tasks need to generate information for Android test owners service.
+ * Register two tasks needed to generate information for Android test owners service.
* One task zips all the OWNERS files in frameworks/support, and second task creates a
* module-info.json that links test modules to paths.
*/
@@ -62,6 +62,7 @@
task.destinationDirectory.set(getDistributionDirectory())
task.from(layout.projectDirectory)
task.include("**/OWNERS")
+ task.exclude("buildSrc/.gradle/**")
task.includeEmptyDirs = false
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 8ca7eb7..152b49a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -35,27 +35,31 @@
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.HasAndroidTest
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.api.variant.TestAndroidComponentsExtension
+import com.android.build.api.variant.Variant
import com.android.build.gradle.BaseExtension
import java.io.File
import org.gradle.api.Project
-import org.gradle.api.UnknownTaskException
+import com.android.build.gradle.TestExtension
+import com.android.build.gradle.internal.attributes.VariantAttr
+import com.android.build.gradle.internal.publishing.AndroidArtifacts
+import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType
+import org.gradle.api.attributes.Usage
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.named
/**
* Creates and configures the test config generation task for a project. Configuration includes
* populating the task with relevant data from the first 4 params, and setting whether the task
* is enabled.
- *
- * @param overrideProject Allows the config task for one project to get registered to an
- * alternative project. Default is for the project to register the new config task to itself
*/
fun Project.createTestConfigurationGenerationTask(
variantName: String,
artifacts: Artifacts,
minSdk: Int,
testRunner: String,
- overrideProject: Project = this
) {
val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
@@ -68,7 +72,7 @@
)
)
}
- val generateTestConfigurationTask = overrideProject.tasks.register(
+ val generateTestConfigurationTask = tasks.register(
"${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName",
GenerateTestConfigurationTask::class.java
) { task ->
@@ -114,7 +118,7 @@
afterEvaluate {
val androidXExtension = extensions.getByType<AndroidXExtension>()
generateTestConfigurationTask.configure {
- it.enabled = hasAndroidTestSourceCode() && !androidXExtension.disableDeviceTests
+ it.enabled = androidXExtension.deviceTests.enabled && hasAndroidTestSourceCode()
}
}
this.rootProject.tasks.findByName(ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
@@ -128,76 +132,143 @@
* there is a test app in addition to the instrumentation app, and the only thing it configures is
* the location of the testapp.
*/
-fun Project.addAppApkToTestConfigGeneration() {
- if (isMacrobenchmarkTarget()) {
- extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
- onVariants(selector().withBuildType("release")) { appVariant ->
- getOrCreateMacrobenchmarkConfigTask().configure { configTask ->
- configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
- configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
- configTask.outputAppApk.set(
- File(
- getTestConfigDirectory(),
- "${path.asFilenamePrefix()}-${appVariant.name}.apk"
- )
- )
- configTask.constrainedOutputAppApk.set(
- File(
- getConstrainedTestConfigDirectory(),
- "${path.asFilenamePrefix()}-${appVariant.name}.apk"
- )
- )
- }
- if (path == ":benchmark:integration-tests:macrobenchmark-target") {
- // Ugly workaround for b/188699825 where we hardcode that
- // :benchmark:integration-tests:macrobenchmark-target needs to be installed
- // for :benchmark:benchmark-macro tests to work.
- project(MACRO_PROJECT).tasks.withType(
- GenerateTestConfigurationTask::class.java
- ).named(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}debugAndroidTest"
- ).configure { configTask ->
- configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
- configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
- configTask.outputAppApk.set(
- File(
- getTestConfigDirectory(),
- "${MACRO_PROJECT.asFilenamePrefix()}-${appVariant.name}.apk"
- )
- )
- configTask.constrainedOutputAppApk.set(
- File(
- getConstrainedTestConfigDirectory(),
- "${MACRO_PROJECT.asFilenamePrefix()}-${appVariant.name}.apk"
- )
- )
- }
- }
- }
+fun Project.addAppApkToTestConfigGeneration(androidXExtension: AndroidXExtension) {
+
+ fun outputAppApkFile(
+ variant: Variant,
+ appProjectPath: String,
+ instrumentationProjectPath: String?
+ ): File {
+ var filename = appProjectPath.asFilenamePrefix()
+ if (instrumentationProjectPath != null) {
+ filename += "_for_${instrumentationProjectPath.asFilenamePrefix()}"
}
- return
+ filename += "-${variant.name}.apk"
+ return File(getTestConfigDirectory(), filename)
+ }
+ fun constrainedOutputAppApkFile(
+ variant: Variant,
+ path: String,
+ instrumentationPath: String?
+ ): File {
+ var filename = path.asFilenamePrefix()
+ if (instrumentationPath != null) {
+ filename += "-for-${instrumentationPath.asFilenamePrefix()}"
+ }
+ filename += "-${variant.name}.apk"
+ return File(getConstrainedTestConfigDirectory(), filename)
}
- extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
- onVariants(selector().withBuildType("debug")) { appVariant ->
+ // For application modules, the instrumentation apk is generated in the module itself
+ extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
+ onVariants(selector().withBuildType("debug")) { variant ->
tasks.named(
- AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK +
- "${appVariant.name}AndroidTest"
- ) { configTask ->
- configTask as GenerateTestConfigurationTask
- configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
- configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
- configTask.outputAppApk.set(
- File(
- getTestConfigDirectory(),
- "${path.asFilenamePrefix()}-${appVariant.name}.apk"
- )
+ "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
+ GenerateTestConfigurationTask::class.java
+ ) { task ->
+ task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
+ task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+
+ // The target project is the same being evaluated
+ task.outputAppApk.set(outputAppApkFile(variant, path, null))
+ task.constrainedOutputAppApk.set(constrainedOutputAppApkFile(variant, path, null))
+ }
+ }
+ }
+
+ // For tests modules, the instrumentation apk is pulled from the <variant>TestedApks
+ // configuration. Note that also the associated test configuration task name is different
+ // from the application one.
+ extensions.findByType(TestAndroidComponentsExtension::class.java)?.apply {
+ onVariants(selector().all()) { variant ->
+ tasks.named(
+ "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}",
+ GenerateTestConfigurationTask::class.java
+ ) { task ->
+ task.appLoader.set(
+ variant.artifacts.getBuiltArtifactsLoader()
)
- configTask.constrainedOutputAppApk.set(
- File(
- getConstrainedTestConfigDirectory(),
- "${path.asFilenamePrefix()}-${appVariant.name}.apk"
- )
+
+ // The target app path is defined in the targetProjectPath field in the android
+ // extension of the test module
+ val targetProjectPath = project
+ .extensions
+ .getByType(TestExtension::class.java)
+ .targetProjectPath
+ ?: throw IllegalStateException("""
+ Module `$path` does not have a targetProjectPath defined.
+ """.trimIndent())
+ task.outputAppApk.set(
+ outputAppApkFile(variant, targetProjectPath, path)
+ )
+ task.constrainedOutputAppApk.set(
+ constrainedOutputAppApkFile(variant, targetProjectPath, path)
+ )
+
+ task.appFileCollection.from(
+ configurations
+ .named("${variant.name}TestedApks")
+ .get()
+ .incoming
+ .artifactView {
+ it.attributes { container ->
+ container.attribute(
+ AndroidArtifacts.ARTIFACT_TYPE,
+ ArtifactType.APK.type
+ )
+ }
+ }
+ .files
+ )
+ }
+ }
+ }
+
+ // For library modules we only look at the build type debug. The target app project can be
+ // specified through the androidX extension, through: targetAppProjectForInstrumentationTest
+ // and targetAppProjectVariantForInstrumentationTest.
+ extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.apply {
+ onVariants(selector().withBuildType("debug")) { variant ->
+
+ val targetAppProject =
+ androidXExtension.deviceTests.targetAppProject ?: return@onVariants
+ val targetAppProjectVariant =
+ androidXExtension.deviceTests.targetAppVariant
+
+ // Recreate the same configuration existing for test modules to pull the artifact
+ // from the application module specified in the deviceTests extension.
+ val configuration = configurations.create("${variant.name}TestedApks") { config ->
+ config.isCanBeResolved = true
+ config.isCanBeConsumed = false
+ config.attributes {
+ it.attribute(VariantAttr.ATTRIBUTE, objects.named(targetAppProjectVariant))
+ it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
+ }
+ config
+ .dependencies
+ .add(project.dependencyFactory.create(targetAppProject))
+ }
+
+ tasks.named(
+ "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
+ GenerateTestConfigurationTask::class.java
+ ) { task ->
+ task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+
+ // The target app path is defined in the androidx extension
+ task.outputAppApk.set(
+ outputAppApkFile(variant, targetAppProject.path, path)
+ )
+ task.constrainedOutputAppApk.set(
+ constrainedOutputAppApkFile(variant, targetAppProject.path, path)
+ )
+
+ task.appFileCollection.from(
+ configuration.incoming.artifactView { view ->
+ view.attributes {
+ it.attribute(AndroidArtifacts.ARTIFACT_TYPE, ArtifactType.APK.type)
+ }
+ }.files
)
}
}
@@ -329,94 +400,7 @@
}
}
-private fun Project.getOrCreateMacrobenchmarkConfigTask():
- TaskProvider<GenerateTestConfigurationTask> {
- val parentProject = this.parent!!
- return try {
- parentProject.tasks.withType(GenerateTestConfigurationTask::class.java)
- .named(AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK)
- } catch (e: UnknownTaskException) {
- parentProject.tasks.register(
- AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK,
- GenerateTestConfigurationTask::class.java
- )
- }
-}
-
-private fun Project.configureMacrobenchmarkConfigTask(
- variantName: String,
- artifacts: Artifacts,
- minSdk: Int,
- testRunner: String
-) {
- val configTask = getOrCreateMacrobenchmarkConfigTask()
- configTask.configure { task ->
- val androidXExtension = extensions.getByType<AndroidXExtension>()
- val fileNamePrefix = path.asFilenamePrefix()
- task.testFolder.set(artifacts.get(SingleArtifact.APK))
- task.testLoader.set(artifacts.getBuiltArtifactsLoader())
- task.outputTestApk.set(
- File(getTestConfigDirectory(), "${path.asFilenamePrefix()}-$variantName.apk")
- )
- task.constrainedOutputTestApk.set(
- File(
- getConstrainedTestConfigDirectory(),
- "${path.asFilenamePrefix()}-$variantName.apk"
- )
- )
- task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
- task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
- task.outputXml.fileValue(
- File(getTestConfigDirectory(), "$fileNamePrefix$variantName.xml")
- )
- task.outputJson.fileValue(
- File(getTestConfigDirectory(), "_$fileNamePrefix$variantName.json")
- )
- task.constrainedOutputXml.fileValue(
- File(
- getTestConfigDirectory(),
- "${path.asFilenamePrefix()}$variantName.xml"
- )
- )
- task.minSdk.set(minSdk)
- task.hasBenchmarkPlugin.set(hasBenchmarkPlugin())
- task.testRunner.set(testRunner)
- task.testProjectPath.set(path)
- task.presubmit.set(isPresubmitBuild())
- val detector = AffectedModuleDetector.getInstance(project)
- task.affectedModuleDetectorSubset.set(
- project.provider {
- detector.getSubset(task)
- }
- )
-
- AffectedModuleDetector.configureTaskGuard(task)
- }
- // Disable xml generation for projects that have no test sources
- afterEvaluate {
- val androidXExtension = extensions.getByType<AndroidXExtension>()
- configTask.configure {
- it.enabled = hasAndroidTestSourceCode() && !androidXExtension.disableDeviceTests
- }
- }
- rootProject.tasks.findByName(ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
- .dependsOn(configTask)
- rootProject.tasks.findByName(ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK)!!
- .dependsOn(configTask)
-}
-
-/**
- * Tells whether this project is the macrobenchmark-target project
- */
-fun Project.isMacrobenchmarkTarget(): Boolean {
- return path.endsWith("macrobenchmark-target")
-}
-
fun Project.configureTestConfigGeneration(baseExtension: BaseExtension) {
- if (isMacrobenchmarkTarget()) {
- // macrobenchmark target projects use special setup. See addAppApkToTestConfigGeneration
- return
- }
extensions.getByType(AndroidComponentsExtension::class.java).apply {
onVariants { variant ->
var name: String? = null
@@ -453,14 +437,6 @@
isMedia2 = false
)
}
- path.endsWith("macrobenchmark") -> {
- configureMacrobenchmarkConfigTask(
- name,
- artifacts,
- baseExtension.defaultConfig.minSdk!!,
- baseExtension.defaultConfig.testInstrumentationRunner!!
- )
- }
else -> {
createTestConfigurationGenerationTask(
name,
@@ -473,5 +449,3 @@
}
}
}
-
-private const val MACRO_PROJECT = ":benchmark:benchmark-macro"
\ No newline at end of file
diff --git a/busytown/androidx_host_tests.sh b/busytown/androidx_host_tests.sh
index bba9e74..67d8d37 100755
--- a/busytown/androidx_host_tests.sh
+++ b/busytown/androidx_host_tests.sh
@@ -5,7 +5,7 @@
cd "$(dirname $0)"
-impl/build.sh test \
+impl/build.sh test zipOwnersFiles \
-Pandroidx.ignoreTestFailures \
-Pandroidx.displayTestOutput=false \
"$@"
diff --git a/busytown/androidx_host_tests_max_dep_versions.sh b/busytown/androidx_host_tests_max_dep_versions.sh
index ef49669..4442f42 100755
--- a/busytown/androidx_host_tests_max_dep_versions.sh
+++ b/busytown/androidx_host_tests_max_dep_versions.sh
@@ -5,7 +5,7 @@
cd "$(dirname $0)"
-impl/build.sh test \
+impl/build.sh test zipOwnersFiles \
-Pandroidx.useMaxDepVersions \
-Pandroidx.displayTestOutput=false \
-Pandroidx.ignoreTestFailures "$@"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index 2b1d488..f920bed 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -19,6 +19,7 @@
package androidx.camera.camera2.pipe.integration.adapter
import android.content.Context
+import android.util.Pair
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraId
@@ -132,7 +133,7 @@
cameraId: String,
existingSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
- ): Map<UseCaseConfig<*>, StreamSpec> {
+ ): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
if (!checkIfSupportedCombinationExist(cameraId)) {
throw IllegalArgumentException(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index b98730d..516f8a5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -29,6 +29,7 @@
import android.media.MediaRecorder
import android.os.Build
import android.util.Rational
+import android.util.Pair
import android.util.Size
import android.view.Display
import androidx.annotation.RequiresApi
@@ -197,7 +198,7 @@
cameraMode: Int,
existingSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
- ): Map<UseCaseConfig<*>, StreamSpec> {
+ ): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
refreshPreviewSize()
val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
for (scc in existingSurfaces) {
@@ -293,7 +294,7 @@
" New configs: " + newUseCaseConfigs
)
}
- return suggestedStreamSpecMap
+ return Pair.create(suggestedStreamSpecMap, mapOf<AttachedSurfaceInfo, StreamSpec>())
}
/**
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index af3b4d6..c758dc4 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -1572,7 +1572,7 @@
CameraMode.DEFAULT,
attachedSurfaceInfoList,
useCaseConfigToOutputSizesMap
- )
+ ).first
useCasesExpectedResultMap.keys.forEach {
val resultSize = suggestedStreamSpecs[useCaseConfigMap[it]]!!.resolution
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index a85be9d..7c1d95e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.hardware.camera2.CameraDevice;
import android.media.CamcorderProfile;
+import android.util.Pair;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -60,7 +61,6 @@
/**
* Creates a new, initialized Camera2DeviceSurfaceManager.
- *
*/
@RestrictTo(Scope.LIBRARY)
public Camera2DeviceSurfaceManager(@NonNull Context context,
@@ -166,8 +166,10 @@
*/
@NonNull
@Override
- public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
- @CameraMode.Mode int cameraMode, @NonNull String cameraId,
+ public Pair<Map<UseCaseConfig<?>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>>
+ getSuggestedStreamSpecs(
+ @CameraMode.Mode int cameraMode,
+ @NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
Preconditions.checkArgument(!newUseCaseConfigsSupportedSizeMap.isEmpty(),
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
index 5481b76..1c8859f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
@@ -26,12 +26,10 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
import android.content.Context;
-import android.hardware.camera2.CameraDevice;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
@@ -66,22 +64,8 @@
final MutableOptionsBundle mutableConfig = MutableOptionsBundle.create();
SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
- switch (captureType) {
- case IMAGE_CAPTURE:
- sessionBuilder.setTemplateType(
- captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
- ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
- CameraDevice.TEMPLATE_PREVIEW);
- break;
- case PREVIEW:
- case IMAGE_ANALYSIS:
- sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
- break;
- case VIDEO_CAPTURE:
- case STREAM_SHARING:
- sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_RECORD);
- break;
- }
+ sessionBuilder.setTemplateType(
+ TemplateTypeUtil.getSessionConfigTemplateType(captureType, captureMode));
mutableConfig.insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionBuilder.build());
@@ -89,23 +73,8 @@
Camera2SessionOptionUnpacker.INSTANCE);
CaptureConfig.Builder captureBuilder = new CaptureConfig.Builder();
-
- switch (captureType) {
- case IMAGE_CAPTURE:
- captureBuilder.setTemplateType(
- captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
- ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
- CameraDevice.TEMPLATE_STILL_CAPTURE);
- break;
- case PREVIEW:
- case IMAGE_ANALYSIS:
- captureBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
- break;
- case VIDEO_CAPTURE:
- case STREAM_SHARING:
- captureBuilder.setTemplateType(CameraDevice.TEMPLATE_RECORD);
- break;
- }
+ captureBuilder.setTemplateType(
+ TemplateTypeUtil.getCaptureConfigTemplateType(captureType, captureMode));
mutableConfig.insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureBuilder.build());
// Only CAPTURE_TYPE_IMAGE_CAPTURE has its own ImageCaptureOptionUnpacker. Other
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
index 5d8196d..1f1efeb 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/StreamUseCaseUtil.java
@@ -16,8 +16,6 @@
package androidx.camera.camera2.internal;
-import static androidx.camera.camera2.impl.Camera2ImplConfig.STREAM_USE_CASE_OPTION;
-
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
@@ -28,27 +26,44 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.CameraMode;
+import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.streamsharing.StreamSharing;
+import androidx.core.util.Preconditions;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A class that contains utility methods for stream use case.
*/
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class StreamUseCaseUtil {
+ public static final Config.Option<Long> STREAM_USE_CASE_STREAM_SPEC_OPTION =
+ Config.Option.create("camera2.streamSpec.streamUseCase", long.class);
+
private StreamUseCaseUtil() {
}
@@ -93,11 +108,12 @@
}
for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
if (sessionConfig.getImplementationOptions().containsOption(
- STREAM_USE_CASE_OPTION) && putStreamUseCaseToMappingIfAvailable(
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION)
+ && putStreamUseCaseToMappingIfAvailable(
streamUseCaseMap,
surface,
sessionConfig.getImplementationOptions().retrieveOption(
- STREAM_USE_CASE_OPTION),
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION),
supportedStreamUseCases)) {
continue;
}
@@ -155,4 +171,272 @@
}
return sUseCaseToStreamUseCaseMapping;
}
+
+ /**
+ * Populate all implementation options needed to determine the StreamUseCase option in the
+ * StreamSpec for this UseCaseConfig
+ */
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ @NonNull
+ public static Camera2ImplConfig getStreamSpecImplementationOptions(
+ @NonNull UseCaseConfig<?> useCaseConfig
+ ) {
+ MutableOptionsBundle optionsBundle = MutableOptionsBundle.create();
+ if (useCaseConfig.containsOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
+ optionsBundle.insertOption(
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION,
+ useCaseConfig.retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)
+ );
+ }
+ if (useCaseConfig.containsOption(UseCaseConfig.OPTION_ZSL_DISABLED)) {
+ optionsBundle.insertOption(
+ UseCaseConfig.OPTION_ZSL_DISABLED,
+ useCaseConfig.retrieveOption(UseCaseConfig.OPTION_ZSL_DISABLED)
+ );
+ }
+ if (useCaseConfig.containsOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)) {
+ optionsBundle.insertOption(
+ ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE,
+ useCaseConfig
+ .retrieveOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)
+ );
+ }
+ if (useCaseConfig.containsOption(UseCaseConfig.OPTION_INPUT_FORMAT)) {
+ optionsBundle.insertOption(
+ UseCaseConfig.OPTION_INPUT_FORMAT,
+ useCaseConfig
+ .retrieveOption(UseCaseConfig.OPTION_INPUT_FORMAT)
+ );
+ }
+ return new Camera2ImplConfig(optionsBundle);
+ }
+
+ /**
+ * Populate the {@link STREAM_USE_CASE_STREAM_SPEC_OPTION} option in StreamSpecs for both
+ * existing UseCases and new UseCases to be attached. This option will be written into the
+ * session configurations of the UseCases. When creating a new capture session during
+ * downstream, it will be used to set the StreamUseCase flag via
+ * {@link android.hardware.camera2.params.OutputConfiguration#setStreamUseCase(long)}
+ *
+ * @param characteristicsCompat the camera characteristics of the device
+ * @param featureSettings the feature settings of this capture
+ * @param attachedSurfaces surface info of the already attached use cases
+ * @param suggestedStreamSpecMap the UseCaseConfig-to-StreamSpec map for new use cases
+ * @param attachedSurfaceStreamSpecMap the SurfaceInfo-to-StreamSpec map for attached use cases
+ * whose StreamSpecs needs to be updated
+ */
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ public static void populateStreamUseCaseStreamSpecOption(
+ @NonNull CameraCharacteristicsCompat characteristicsCompat,
+ @NonNull SupportedSurfaceCombination.FeatureSettings featureSettings,
+ @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
+ @NonNull Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap,
+ @NonNull Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap) {
+ if (Build.VERSION.SDK_INT < 33) {
+ return;
+ }
+ long[] availableStreamUseCases = characteristicsCompat.get(
+ CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES);
+ if (availableStreamUseCases == null || availableStreamUseCases.length == 0) {
+ return;
+ }
+ if (featureSettings.getCameraMode() != CameraMode.DEFAULT
+ || featureSettings.getRequiredMaxBitDepth() != DynamicRange.BIT_DEPTH_8_BIT) {
+ return;
+ }
+ List<UseCaseConfig<?>> newUseCaseConfigs = new ArrayList<>(suggestedStreamSpecMap.keySet());
+ // All AttachedSurfaceInfo should have implementation options
+ for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+ Preconditions.checkNotNull(attachedSurfaceInfo.getImplementationOptions());
+ }
+ // All StreamSpecs in the map should have implementation options
+ for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
+ Preconditions.checkNotNull(Preconditions.checkNotNull(
+ suggestedStreamSpecMap.get(useCaseConfig)).getImplementationOptions());
+ }
+ if (containsZslUseCase(attachedSurfaces, newUseCaseConfigs)) {
+ return;
+ }
+ Set<Long> availableStreamUseCaseSet = new HashSet<>();
+ for (Long availableStreamUseCase : availableStreamUseCases) {
+ availableStreamUseCaseSet.add(availableStreamUseCase);
+ }
+ if (isValidCamera2InteropOverride(attachedSurfaces, newUseCaseConfigs,
+ availableStreamUseCaseSet)) {
+ for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+ Config oldImplementationOptions = attachedSurfaceInfo.getImplementationOptions();
+ Config newImplementationOptions =
+ getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
+ oldImplementationOptions,
+ oldImplementationOptions.retrieveOption(
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION));
+ if (newImplementationOptions != null) {
+ StreamSpec.Builder newStreamSpecBuilder =
+ StreamSpec.builder(attachedSurfaceInfo.getSize())
+ .setDynamicRange(attachedSurfaceInfo.getDynamicRange())
+ .setImplementationOptions(newImplementationOptions);
+ if (attachedSurfaceInfo.getTargetFrameRate() != null) {
+ newStreamSpecBuilder.setExpectedFrameRateRange(
+ attachedSurfaceInfo.getTargetFrameRate());
+ }
+ attachedSurfaceStreamSpecMap.put(attachedSurfaceInfo,
+ newStreamSpecBuilder.build());
+ }
+ }
+ for (UseCaseConfig<?> newUseCaseConfig : newUseCaseConfigs) {
+ StreamSpec oldStreamSpec = suggestedStreamSpecMap.get(newUseCaseConfig);
+ Config oldImplementationOptions = oldStreamSpec.getImplementationOptions();
+ Config newImplementationOptions =
+ getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
+ oldImplementationOptions, oldImplementationOptions.retrieveOption(
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION));
+ if (newImplementationOptions != null) {
+ StreamSpec newStreamSpec =
+ oldStreamSpec.toBuilder().setImplementationOptions(
+ newImplementationOptions).build();
+ suggestedStreamSpecMap.put(newUseCaseConfig, newStreamSpec);
+ }
+ }
+ }
+ }
+
+ /**
+ * Given an old options, return a new option with stream use case stream spec option inserted
+ */
+ @Nullable
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ private static Config getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
+ Config oldImplementationOptions,
+ long streamUseCase
+ ) {
+ if (oldImplementationOptions.containsOption(STREAM_USE_CASE_STREAM_SPEC_OPTION)
+ && oldImplementationOptions.retrieveOption(STREAM_USE_CASE_STREAM_SPEC_OPTION)
+ == streamUseCase) {
+ // The old options already has the same stream use case. No need to update
+ return null;
+ }
+ MutableOptionsBundle optionsBundle =
+ MutableOptionsBundle.from(oldImplementationOptions);
+ optionsBundle.insertOption(STREAM_USE_CASE_STREAM_SPEC_OPTION, streamUseCase);
+ return new Camera2ImplConfig(optionsBundle);
+ }
+
+ /**
+ * Return true if any one of the existing or new UseCases is ZSL.
+ */
+ private static boolean containsZslUseCase(
+ List<AttachedSurfaceInfo> attachedSurfaces,
+ List<UseCaseConfig<?>> newUseCaseConfigs) {
+ for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+ List<UseCaseConfigFactory.CaptureType> captureTypes =
+ attachedSurfaceInfo.getCaptureTypes();
+ UseCaseConfigFactory.CaptureType captureType = captureTypes.get(0);
+ if (isZslUseCase(
+ attachedSurfaceInfo.getImplementationOptions(),
+ captureType)) {
+ return true;
+ }
+ }
+ for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
+ if (isZslUseCase(useCaseConfig, useCaseConfig.getCaptureType())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check whether a UseCase is ZSL.
+ */
+ private static boolean isZslUseCase(Config config,
+ UseCaseConfigFactory.CaptureType captureType) {
+ if (config.retrieveOption(UseCaseConfig.OPTION_ZSL_DISABLED, false)) {
+ return false;
+ }
+ // Skip if capture mode doesn't exist in the options
+ if (!config.containsOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)) {
+ return false;
+ }
+
+ @ImageCapture.CaptureMode int captureMode =
+ config.retrieveOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE);
+ return TemplateTypeUtil.getSessionConfigTemplateType(captureType, captureMode)
+ == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG;
+ }
+
+ /**
+ * Check whether the given StreamUseCases are available to the device.
+ */
+ private static boolean areStreamUseCasesAvailable(Set<Long> availableStreamUseCasesSet,
+ Set<Long> streamUseCases) {
+ for (Long streamUseCase : streamUseCases) {
+ if (!availableStreamUseCasesSet.contains(streamUseCase)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void throwInvalidCamera2InteropOverrideException() {
+ throw new IllegalArgumentException("Either all use cases must have non-default stream use "
+ + "case assigned or none should have it");
+ }
+
+ /**
+ * Return true if all existing UseCases and new UseCases have Camera2Interop override and
+ * these StreamUseCases are all available to the device.
+ */
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ private static boolean isValidCamera2InteropOverride(
+ List<AttachedSurfaceInfo> attachedSurfaces,
+ List<UseCaseConfig<?>> newUseCaseConfigs,
+ Set<Long> availableStreamUseCases) {
+ Set<Long> streamUseCases = new HashSet<>();
+ boolean hasNonDefaultStreamUseCase = false;
+ boolean hasDefaultOrNullStreamUseCase = false;
+ for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
+ if (!attachedSurfaceInfo.getImplementationOptions().containsOption(
+ Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
+ hasDefaultOrNullStreamUseCase = true;
+ break;
+ }
+ long streamUseCaseOverride =
+ attachedSurfaceInfo.getImplementationOptions()
+ .retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION);
+ if (streamUseCaseOverride
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) {
+ hasDefaultOrNullStreamUseCase = true;
+ break;
+ }
+ hasNonDefaultStreamUseCase = true;
+ break;
+ }
+ for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
+ if (!useCaseConfig.containsOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
+ hasDefaultOrNullStreamUseCase = true;
+ if (hasNonDefaultStreamUseCase) {
+ throwInvalidCamera2InteropOverrideException();
+ }
+ } else {
+ long streamUseCaseOverride =
+ useCaseConfig.retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION);
+ if (streamUseCaseOverride
+ == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) {
+ hasDefaultOrNullStreamUseCase = true;
+ if (hasNonDefaultStreamUseCase) {
+ throwInvalidCamera2InteropOverrideException();
+ }
+ } else {
+ hasNonDefaultStreamUseCase = true;
+ if (hasDefaultOrNullStreamUseCase) {
+ throwInvalidCamera2InteropOverrideException();
+ }
+ streamUseCases.add(streamUseCaseOverride);
+ }
+ }
+
+ }
+ return !hasDefaultOrNullStreamUseCase && areStreamUseCasesAvailable(availableStreamUseCases,
+ streamUseCases);
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 59fee41..b9a91cf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -33,6 +33,7 @@
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
+import android.util.Pair;
import android.util.Range;
import android.util.Rational;
import android.util.Size;
@@ -40,6 +41,7 @@
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
@@ -49,6 +51,7 @@
import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.impl.AttachedSurfaceInfo;
@@ -490,16 +493,20 @@
* @param attachedSurfaces the existing surfaces.
* @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output
* sizes map.
- * @return the suggested stream specifications, which is a mapping from UseCaseConfig to the
- * suggested stream specification.
+ * @return the suggested stream specifications, which is a pair of mappings. The first
+ * mapping is from UseCaseConfig to the suggested stream specification representing new
+ * UseCases. The second mapping is from attachedSurfaceInfo to the suggested stream
+ * specifications representing existing UseCases.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
* found. This may be due to no available output size, no
* available surface combination, unsupported combinations
* of {@link DynamicRange}, or requiring an
* unsupported combination of camera features.
*/
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@NonNull
- Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecifications(
+ Pair<Map<UseCaseConfig<?>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>>
+ getSuggestedStreamSpecifications(
@CameraMode.Mode int cameraMode,
@NonNull List<AttachedSurfaceInfo> attachedSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
@@ -669,7 +676,10 @@
useCasesPriorityOrder.indexOf(newUseCaseConfigs.indexOf(useCaseConfig)));
StreamSpec.Builder streamSpecBuilder = StreamSpec.builder(resolutionForUseCase)
.setDynamicRange(Preconditions.checkNotNull(
- resolvedDynamicRanges.get(useCaseConfig)));
+ resolvedDynamicRanges.get(useCaseConfig)))
+ .setImplementationOptions(
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(useCaseConfig)
+ );
if (targetFramerateForDevice != null) {
streamSpecBuilder.setExpectedFrameRateRange(targetFramerateForDevice);
}
@@ -683,7 +693,9 @@
+ " Existing surfaces: " + attachedSurfaces
+ " New configs: " + newUseCaseConfigs);
}
- return suggestedStreamSpecMap;
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ // TODO(b/266879290): Invoke StreamUSeCaseUtil.populateStreamUseCaseStreamSpecOption()
+ return new Pair<>(suggestedStreamSpecMap, attachedSurfaceStreamSpecMap);
}
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java
new file mode 100644
index 0000000..905e01f
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TemplateTypeUtil.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import android.hardware.camera2.CameraDevice;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ExperimentalZeroShutterLag;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+
+/**
+ * A class that contains utility methods for template type.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class TemplateTypeUtil {
+
+ private TemplateTypeUtil() {
+
+ }
+
+ /**
+ * Returns the appropriate template type for a session configuration.
+ */
+ @OptIn(markerClass = ExperimentalZeroShutterLag.class)
+ public static int getSessionConfigTemplateType(
+ @NonNull UseCaseConfigFactory.CaptureType captureType,
+ @ImageCapture.CaptureMode int captureMode
+ ) {
+ switch (captureType) {
+ case IMAGE_CAPTURE:
+ return captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
+ ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
+ CameraDevice.TEMPLATE_PREVIEW;
+ case VIDEO_CAPTURE:
+ case STREAM_SHARING:
+ return CameraDevice.TEMPLATE_RECORD;
+ case PREVIEW:
+ case IMAGE_ANALYSIS:
+ default:
+ return CameraDevice.TEMPLATE_PREVIEW;
+ }
+ }
+
+ /**
+ * Returns the appropriate template type for a capture configuration.
+ */
+ @OptIn(markerClass = ExperimentalZeroShutterLag.class)
+ public static int getCaptureConfigTemplateType(
+ @NonNull UseCaseConfigFactory.CaptureType captureType,
+ @ImageCapture.CaptureMode int captureMode
+ ) {
+ switch (captureType) {
+ case IMAGE_CAPTURE:
+ return captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
+ ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
+ CameraDevice.TEMPLATE_STILL_CAPTURE;
+ case VIDEO_CAPTURE:
+ case STREAM_SHARING:
+ return CameraDevice.TEMPLATE_RECORD;
+ case PREVIEW:
+ case IMAGE_ANALYSIS:
+ default:
+ return CameraDevice.TEMPLATE_PREVIEW;
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
index 3c1f547..3919413 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -18,8 +18,14 @@
import static android.os.Build.VERSION.SDK_INT;
+import static androidx.camera.camera2.internal.StreamUseCaseUtil.STREAM_USE_CASE_STREAM_SPEC_OPTION;
+import static androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
+import static androidx.camera.core.ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG;
+
+import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
+import android.graphics.ImageFormat;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
@@ -30,14 +36,25 @@
import androidx.annotation.NonNull;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Preview;
+import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.CameraMode;
import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.StreamSpec;
+import androidx.camera.core.impl.SurfaceConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.internal.utils.SizeUtil;
import androidx.camera.core.streamsharing.StreamSharing;
import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.test.filters.SdkSuppress;
@@ -54,6 +71,7 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
@RunWith(RobolectricTestRunner.class)
@@ -62,6 +80,10 @@
private CameraCharacteristics mCameraCharacteristics;
private static final String CAMERA_ID_0 = "0";
+ private static final Long TEST_STREAM_USE_CASE_OPTION_VALUE = Long.valueOf(
+ CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
+ private static final @ImageCapture.CaptureMode int TEST_OPTION_IMAGE_CAPTURE_MODE_VALUE =
+ CAPTURE_MODE_MAXIMIZE_QUALITY;
DeferrableSurface mMockSurface = new DeferrableSurface() {
private final ListenableFuture<Surface> mSurfaceFuture = ResolvableFuture.create();
@@ -283,9 +305,272 @@
== CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW);
}
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void getStreamSpecImplementationOptions() {
+ Camera2ImplConfig result =
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(
+ getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE));
+ assertTrue(result.retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)
+ == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ assertFalse(result.retrieveOption(UseCaseConfig.OPTION_ZSL_DISABLED));
+ assertTrue(result.retrieveOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)
+ == TEST_OPTION_IMAGE_CAPTURE_MODE_VALUE);
+ assertTrue(result.retrieveOption(UseCaseConfig.OPTION_INPUT_FORMAT)
+ == ImageFormat.PRIVATE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_streamUseCaseNotAvailable() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(
+ getCameraCharacteristicsCompat(true),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertFalse(suggestedStreamSpecMap.get(useCaseConfig)
+ .getImplementationOptions().containsOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION));
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_cameraModeNotSupported() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(
+ getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.CONCURRENT_CAMERA,
+ DynamicRange.BIT_DEPTH_8_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertFalse(suggestedStreamSpecMap.get(useCaseConfig)
+ .getImplementationOptions().containsOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION));
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_bitDepthNotSupported() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(
+ getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_10_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertFalse(suggestedStreamSpecMap.get(useCaseConfig)
+ .getImplementationOptions().containsOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION));
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_isZslUseCase() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, true,
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE, ImageFormat.JPEG);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertFalse(suggestedStreamSpecMap.get(useCaseConfig)
+ .getImplementationOptions().containsOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION));
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_isZslUseCase_ZslDisabled() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, true, true,
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE, ImageFormat.JPEG);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertTrue(suggestedStreamSpecMap.get(
+ useCaseConfig).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_isZslSurface() {
+ List<AttachedSurfaceInfo> attachedSurfaces = new ArrayList<>();
+ attachedSurfaces.add(getFakeAttachedSurfaceInfo(true, false, true,
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE, ImageFormat.JPEG));
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), attachedSurfaces, new HashMap<>(),
+ attachedSurfaceStreamSpecMap
+ );
+ assertFalse(attachedSurfaceStreamSpecMap.containsKey(attachedSurfaces.get(0)));
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_isZslSurface_ZslDisabled() {
+ List<AttachedSurfaceInfo> attachedSurfaces = new ArrayList<>();
+ attachedSurfaces.add(getFakeAttachedSurfaceInfo(true, true, true,
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE, ImageFormat.JPEG));
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), attachedSurfaces, new HashMap<>(),
+ attachedSurfaceStreamSpecMap
+ );
+ assertTrue(attachedSurfaceStreamSpecMap.get(
+ attachedSurfaces.get(0)).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_camera2InteropOverride_singleNewUseCase() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), new ArrayList<>(), suggestedStreamSpecMap,
+ new HashMap<>());
+ assertTrue(suggestedStreamSpecMap.get(
+ useCaseConfig).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_camera2InteropOverride_singleSurface() {
+ List<AttachedSurfaceInfo> attachedSurfaces = new ArrayList<>();
+ attachedSurfaces.add(getFakeAttachedSurfaceInfo(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE));
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), attachedSurfaces, new HashMap<>(),
+ attachedSurfaceStreamSpecMap
+ );
+ assertTrue(attachedSurfaceStreamSpecMap.get(
+ attachedSurfaces.get(0)).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ public void populateStreamUseCaseStreamSpecOption_camera2InteropOverride_useCaseAndSurface() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ List<AttachedSurfaceInfo> attachedSurfaces = new ArrayList<>();
+ attachedSurfaces.add(getFakeAttachedSurfaceInfo(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE));
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), attachedSurfaces, suggestedStreamSpecMap,
+ attachedSurfaceStreamSpecMap
+ );
+ assertTrue(suggestedStreamSpecMap.get(
+ useCaseConfig).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ assertTrue(attachedSurfaceStreamSpecMap.get(
+ attachedSurfaces.get(0)).getImplementationOptions().retrieveOption(
+ STREAM_USE_CASE_STREAM_SPEC_OPTION) == TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void populateStreamUseCaseStreamSpecOption_camera2InteropOverride_missingOverride() {
+ Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(false, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE);
+ suggestedStreamSpecMap.put(useCaseConfig,
+ getFakeStreamSpecFromFakeUseCaseConfig(useCaseConfig));
+ List<AttachedSurfaceInfo> attachedSurfaces = new ArrayList<>();
+ attachedSurfaces.add(getFakeAttachedSurfaceInfo(true, false, false,
+ UseCaseConfigFactory.CaptureType.PREVIEW, ImageFormat.PRIVATE));
+ Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
+ StreamUseCaseUtil.populateStreamUseCaseStreamSpecOption(getCameraCharacteristicsCompat(),
+ SupportedSurfaceCombination.FeatureSettings.of(CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT), attachedSurfaces, suggestedStreamSpecMap,
+ attachedSurfaceStreamSpecMap
+ );
+ }
+
+ private UseCaseConfig<?> getFakeUseCaseConfigWithOptions(boolean camera2InteropOverride,
+ boolean isZslDisabled, boolean isZslCaptureMode,
+ UseCaseConfigFactory.CaptureType captureType, int imageFormat) {
+ FakeUseCaseConfig.Builder fakeUseCaseConfigBuilder = new FakeUseCaseConfig.Builder(
+ captureType);
+ MutableConfig fakeConfig = fakeUseCaseConfigBuilder.getMutableConfig();
+ if (camera2InteropOverride) {
+ fakeConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION,
+ TEST_STREAM_USE_CASE_OPTION_VALUE);
+ }
+ fakeConfig.insertOption(UseCaseConfig.OPTION_ZSL_DISABLED, isZslDisabled);
+ fakeConfig.insertOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE,
+ isZslCaptureMode ? CAPTURE_MODE_ZERO_SHUTTER_LAG
+ : TEST_OPTION_IMAGE_CAPTURE_MODE_VALUE);
+ fakeConfig.insertOption(ImageCaptureConfig.OPTION_INPUT_FORMAT, imageFormat);
+ return fakeUseCaseConfigBuilder.getUseCaseConfig();
+ }
+
+ private AttachedSurfaceInfo getFakeAttachedSurfaceInfo(boolean camera2InteropOverride,
+ boolean isZslDisabled, boolean isZslCaptureMode,
+ UseCaseConfigFactory.CaptureType captureType, int imageFormat) {
+ UseCaseConfig<?> useCaseConfig = getFakeUseCaseConfigWithOptions(camera2InteropOverride,
+ isZslDisabled, isZslCaptureMode, captureType, imageFormat);
+ List<UseCaseConfigFactory.CaptureType> captureTypes = new ArrayList<>();
+ captureTypes.add(useCaseConfig.getCaptureType());
+ return AttachedSurfaceInfo.create(SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ ),
+ useCaseConfig.getInputFormat(),
+ SizeUtil.RESOLUTION_720P,
+ DynamicRange.SDR,
+ captureTypes,
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(useCaseConfig),
+ /*targetFrameRate=*/null);
+ }
+
+ private StreamSpec getFakeStreamSpecFromFakeUseCaseConfig(UseCaseConfig<?> fakeUseCaseConfig) {
+ return StreamSpec.builder(SizeUtil.RESOLUTION_720P)
+ .setDynamicRange(DynamicRange.UNSPECIFIED)
+ .setImplementationOptions(
+ StreamUseCaseUtil.getStreamSpecImplementationOptions(fakeUseCaseConfig)
+ ).build();
+ }
+
private CameraCharacteristicsCompat getCameraCharacteristicsCompat() {
+ return getCameraCharacteristicsCompat(false);
+ }
+
+ private CameraCharacteristicsCompat getCameraCharacteristicsCompat(
+ boolean noAvailableStreamUseCase) {
ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(mCameraCharacteristics);
- if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (SDK_INT >= Build.VERSION_CODES.TIRAMISU && !noAvailableStreamUseCase) {
long[] uc = new long[]{CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT,
CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW,
CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL,
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index b11b2a5a..d0ea2aa 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -1558,7 +1558,7 @@
cameraMode,
attachedSurfaceInfoList,
useCaseConfigToOutputSizesMap
- )
+ ).first
useCasesExpectedSizeMap.keys.forEach {
val resultSize = suggestedStreamSpecs[useCaseConfigMap[it]]!!.resolution
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index b7268b0..8c70299 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -22,6 +22,7 @@
import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_ONE_PIXEL_SHIFT_ENABLED;
import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_FORMAT;
import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_ROTATION_ENABLED;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
@@ -70,6 +71,7 @@
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageAnalysisConfig;
+import androidx.camera.core.impl.ImageInputConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.ImmediateSurface;
@@ -95,6 +97,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executor;
@@ -217,6 +220,9 @@
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ SessionConfig.Builder mSessionConfigBuilder;
+
@Nullable
private DeferrableSurface mDeferrableSurface;
@@ -370,6 +376,9 @@
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
streamSpec.getResolution());
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
@@ -387,8 +396,7 @@
sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
- sessionConfigBuilder.addSurface(mDeferrableSurface);
-
+ sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange());
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
clearPipeline();
@@ -814,14 +822,26 @@
protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
final ImageAnalysisConfig config = (ImageAnalysisConfig) getCurrentConfig();
- SessionConfig.Builder sessionConfigBuilder = createPipeline(getCameraId(), config,
+ mSessionConfigBuilder = createPipeline(getCameraId(), config,
suggestedStreamSpec);
- updateSessionConfig(sessionConfigBuilder.build());
+ updateSessionConfig(mSessionConfigBuilder.build());
return suggestedStreamSpec;
}
/**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ mSessionConfigBuilder.addImplementationOptions(config);
+ updateSessionConfig(mSessionConfigBuilder.build());
+ return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
+ }
+
+ /**
* Updates relative rotation if attached to a camera. No-op otherwise.
*/
private void tryUpdateRelativeRotation() {
@@ -1009,6 +1029,15 @@
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 1;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
+ /**
+ * Explicitly setting the default dynamic range to SDR (rather than UNSPECIFIED) means
+ * ImageAnalysis won't inherit dynamic ranges from other use cases.
+ */
+ // TODO(b/258099919): ImageAnalysis currently can't support HDR, so we don't expose the
+ // dynamic range setter and require SDR. We may want to get rid of this default once we
+ // can support tone-mapping from HDR -> SDR
+ private static final DynamicRange DEFAULT_DYNAMIC_RANGE = DynamicRange.SDR;
+
private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
new ResolutionSelector.Builder().setAspectRatioStrategy(
AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
@@ -1024,7 +1053,8 @@
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
.setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
.setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR)
- .setCaptureType(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS);
+ .setCaptureType(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS)
+ .setDynamicRange(DEFAULT_DYNAMIC_RANGE);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -1041,7 +1071,8 @@
public static final class Builder
implements ImageOutputConfig.Builder<Builder>,
ThreadConfig.Builder<Builder>,
- UseCaseConfig.Builder<ImageAnalysis, ImageAnalysisConfig, Builder> {
+ UseCaseConfig.Builder<ImageAnalysis, ImageAnalysisConfig, Builder>,
+ ImageInputConfig.Builder<Builder> {
private final MutableOptionsBundle mMutableConfig;
@@ -1610,5 +1641,29 @@
getMutableConfig().insertOption(OPTION_CAPTURE_TYPE, captureType);
return this;
}
+
+ // Implementations of ImageInputConfig.Builder default methods
+
+ /**
+ * Sets the {@link DynamicRange}.
+ *
+ * <p>This is currently only exposed to internally set the dynamic range to SDR.
+ * @return The current Builder.
+ * @see DynamicRange
+ */
+ @RestrictTo(Scope.LIBRARY)
+ @NonNull
+ @Override
+ public Builder setDynamicRange(@NonNull DynamicRange dynamicRange) {
+ // TODO(b/258099919): ImageAnalysis currently can't support HDR, so we require SDR.
+ // It's possible to support other DynamicRanges through tone-mapping or by exposing
+ // other ImageReader formats, such as YCBCR_P010.
+ if (!Objects.equals(DynamicRange.SDR, dynamicRange)) {
+ throw new UnsupportedOperationException(
+ "ImageAnalysis currently only supports SDR");
+ }
+ getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+ return this;
+ }
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index b547104..3152b85 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -38,6 +38,7 @@
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
@@ -100,6 +101,7 @@
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageCaptureConfig;
+import androidx.camera.core.impl.ImageInputConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.ImageReaderProxy;
@@ -145,6 +147,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
@@ -382,6 +385,10 @@
getCameraControl().addZslConfig(sessionConfigBuilder);
}
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
+
// Setup the ImageReader to do processing
Size resolution = streamSpec.getResolution();
if (config.getImageReaderProxyProvider() != null) {
@@ -1610,6 +1617,18 @@
}
/**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ mSessionConfigBuilder.addImplementationOptions(config);
+ updateSessionConfig(mSessionConfigBuilder.build());
+ return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
+ }
+
+ /**
* Initiates a set of captures that will be used to create the output of
* {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)} and its variants.
*
@@ -1733,6 +1752,9 @@
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
getCameraControl().addZslConfig(sessionConfigBuilder);
}
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
// TODO(b/143915543): Ensure this never gets called by a camera that is not attached
// to this use case so we don't need to do this check.
@@ -2020,13 +2042,17 @@
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build();
private static final ImageCaptureConfig DEFAULT_CONFIG;
+ // ImageCapture does not yet support HDR so we must default to SDR. This ensures it won't
+ // choose an HDR format when other use cases have selected HDR.
+ private static final DynamicRange DEFAULT_DYNAMIC_RANGE = DynamicRange.SDR;
static {
Builder builder = new Builder()
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
.setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
.setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR)
- .setCaptureType(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE);
+ .setCaptureType(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+ .setDynamicRange(DEFAULT_DYNAMIC_RANGE);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -2512,7 +2538,8 @@
public static final class Builder implements
UseCaseConfig.Builder<ImageCapture, ImageCaptureConfig, Builder>,
ImageOutputConfig.Builder<Builder>,
- IoConfig.Builder<Builder> {
+ IoConfig.Builder<Builder>,
+ ImageInputConfig.Builder<Builder> {
private final MutableOptionsBundle mMutableConfig;
@@ -3090,5 +3117,26 @@
getMutableConfig().insertOption(OPTION_CAPTURE_TYPE, captureType);
return this;
}
+
+ // Implementations of ImageInputConfig.Builder default methods
+
+ /**
+ * Sets the {@link DynamicRange}.
+ *
+ * <p>This is currently only exposed to internally set the dynamic range to SDR.
+ * @return The current Builder.
+ * @see DynamicRange
+ */
+ @RestrictTo(Scope.LIBRARY)
+ @NonNull
+ @Override
+ public Builder setDynamicRange(@NonNull DynamicRange dynamicRange) {
+ // TODO(b/280893255): ImageCapture currently does not support HDR.
+ if (!Objects.equals(DynamicRange.SDR, dynamicRange)) {
+ throw new UnsupportedOperationException("ImageCapture currently only supports SDR");
+ }
+ getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+ return this;
+ }
}
}
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 153f731..e593232 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
@@ -180,6 +180,9 @@
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ SessionConfig.Builder mSessionConfigBuilder;
+
// TODO(b/259308680): remove mSessionDeferrableSurface and rely on mCameraEdge to get the
// DeferrableSurface
private DeferrableSurface mSessionDeferrableSurface;
@@ -244,6 +247,9 @@
mSessionDeferrableSurface = surfaceRequest.getDeferrableSurface();
addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
return sessionConfigBuilder;
}
@@ -298,6 +304,9 @@
// Send the camera Surface to the camera2.
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config,
streamSpec.getResolution());
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
addCameraSurfaceAndErrorListener(sessionConfigBuilder, cameraId, config, streamSpec);
return sessionConfigBuilder;
}
@@ -368,7 +377,8 @@
// SurfaceProcessorNode and CaptureProcessor cases, since no surface provider also means no
// output target for these two cases.
if (mSurfaceProvider != null) {
- sessionConfigBuilder.addSurface(mSessionDeferrableSurface);
+ sessionConfigBuilder.addSurface(mSessionDeferrableSurface,
+ streamSpec.getDynamicRange());
}
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
@@ -516,7 +526,8 @@
private void updateConfigAndOutput(@NonNull String cameraId, @NonNull PreviewConfig config,
@NonNull StreamSpec streamSpec) {
- updateSessionConfig(createPipeline(cameraId, config, streamSpec).build());
+ mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
+ updateSessionConfig(mSessionConfigBuilder.build());
}
/**
@@ -643,6 +654,19 @@
/**
* {@inheritDoc}
*/
+ @NonNull
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ mSessionConfigBuilder.addImplementationOptions(config);
+ updateSessionConfig(mSessionConfigBuilder.build());
+ return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ */
@Override
@RestrictTo(Scope.LIBRARY)
public void setViewPortCropRect(@NonNull Rect viewPortCropRect) {
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 5b381e7..acdce0c 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
@@ -701,6 +701,32 @@
}
/**
+ * Update the implementation options of the stream specification for the UseCase.
+ *
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void updateSuggestedStreamSpecImplementationOptions(@NonNull Config config) {
+ mAttachedStreamSpec = onSuggestedStreamSpecImplementationOptionsUpdated(config);
+ }
+
+ /**
+ * Called when updating the stream specifications' implementation options of existing use cases
+ * via {@code CameraUseCaseAdapter#updateUseCases}.
+ *
+ * @param config The new implementationOptions for the stream specification.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ if (mAttachedStreamSpec == null) {
+ throw new UnsupportedOperationException("Attempt to update the implementation options "
+ + "for a use case without attached stream specifications.");
+ }
+ return mAttachedStreamSpec.toBuilder().setImplementationOptions(config).build();
+ }
+
+
+ /**
* Called when CameraControlInternal is attached into the UseCase. UseCase may need to
* override this method to configure the CameraControlInternal here. Ex. Setting correct flash
* mode by CameraControlInternal.setFlashMode to enable correct AE mode and flash state.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index b0c0a2a..9abb314 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -17,6 +17,7 @@
package androidx.camera.core.impl;
import android.content.Context;
+import android.util.Pair;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -42,8 +43,8 @@
/**
* Creates a new, initialized instance of a CameraDeviceSurfaceManager.
*
- * @param context the android context
- * @param cameraManager the camera manager object used to query the camera information.
+ * @param context the android context
+ * @param cameraManager the camera manager object used to query the camera information.
* @param availableCameraIds current available camera ids.
* @return the factory instance
* @throws InitializationException if it fails to create the factory
@@ -90,7 +91,8 @@
* is not a valid id.
*/
@NonNull
- Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
+ Pair<Map<UseCaseConfig<?>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>>
+ getSuggestedStreamSpecs(
@CameraMode.Mode int cameraMode,
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
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 2350fbc..7d284bd 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
@@ -31,6 +31,7 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
+import android.util.Pair;
import android.util.Size;
import android.view.Surface;
@@ -62,6 +63,7 @@
import androidx.camera.core.impl.RestrictedCameraControl;
import androidx.camera.core.impl.RestrictedCameraControl.CameraOperation;
import androidx.camera.core.impl.RestrictedCameraInfo;
+import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.SessionProcessor;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceConfig;
@@ -79,6 +81,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -334,6 +337,22 @@
}
mCameraInternal.detachUseCases(cameraUseCasesToDetach);
+ // Update StreamSpec for UseCases to keep.
+ if (!cameraUseCasesToDetach.isEmpty()) {
+ // Only do this if we are not removing UseCase, because updating SessionConfig
+ // when removing UseCases may lead to flickering.
+ for (UseCase useCase : cameraUseCasesToKeep) {
+ if (suggestedStreamSpecMap.containsKey(useCase)) {
+ StreamSpec newStreamSpec = suggestedStreamSpecMap.get(useCase);
+ Config config = newStreamSpec.getImplementationOptions();
+ if (config != null && hasImplementationOptionChanged(newStreamSpec,
+ useCase.getSessionConfig())) {
+ useCase.updateSuggestedStreamSpecImplementationOptions(config);
+ }
+ }
+ }
+ }
+
// Attach new UseCases.
for (UseCase useCase : cameraUseCasesToAttach) {
ConfigPair configPair = requireNonNull(configs.get(useCase));
@@ -361,6 +380,29 @@
}
}
+ /**
+ * Return true if the given StreamSpec has any option with a different value than that
+ * of the given sessionConfig.
+ */
+ private static boolean hasImplementationOptionChanged(
+ StreamSpec streamSpec,
+ SessionConfig sessionConfig) {
+ Config newStreamSpecOptions = streamSpec.getImplementationOptions();
+ Config sessionConfigOptions = sessionConfig.getImplementationOptions();
+ if (newStreamSpecOptions.listOptions().size()
+ != sessionConfig.getImplementationOptions().listOptions().size()) {
+ return true;
+ }
+ for (Config.Option<?> newOption : newStreamSpecOptions.listOptions()) {
+ if (!sessionConfigOptions.containsOption(newOption)
+ || !Objects.equals(sessionConfigOptions.retrieveOption(newOption),
+ newStreamSpecOptions.retrieveOption(newOption))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private @CameraMode.Mode int getCameraMode() {
synchronized (mLock) {
if (mCameraCoordinator.getCameraOperatingMode()
@@ -597,6 +639,7 @@
List<AttachedSurfaceInfo> existingSurfaces = new ArrayList<>();
String cameraId = cameraInfoInternal.getCameraId();
Map<UseCase, StreamSpec> suggestedStreamSpecs = new HashMap<>();
+ Map<AttachedSurfaceInfo, UseCase> surfaceInfoUseCaseMap = new HashMap<>();
// Get resolution for current use cases.
for (UseCase useCase : currentUseCases) {
@@ -606,12 +649,14 @@
cameraId,
useCase.getImageFormat(),
useCase.getAttachedSurfaceResolution());
- existingSurfaces.add(AttachedSurfaceInfo.create(surfaceConfig,
+ AttachedSurfaceInfo attachedSurfaceInfo = AttachedSurfaceInfo.create(surfaceConfig,
useCase.getImageFormat(), useCase.getAttachedSurfaceResolution(),
Preconditions.checkNotNull(useCase.getAttachedStreamSpec()).getDynamicRange(),
getCaptureTypes(useCase),
useCase.getAttachedStreamSpec().getImplementationOptions(),
- useCase.getCurrentConfig().getTargetFrameRate(null)));
+ useCase.getCurrentConfig().getTargetFrameRate(null));
+ existingSurfaces.add(attachedSurfaceInfo);
+ surfaceInfoUseCaseMap.put(attachedSurfaceInfo, useCase);
suggestedStreamSpecs.put(useCase, useCase.getAttachedStreamSpec());
}
@@ -643,7 +688,8 @@
}
// Get suggested stream specifications and update the use case session configuration
- Map<UseCaseConfig<?>, StreamSpec> useCaseConfigStreamSpecMap =
+ Pair<Map<UseCaseConfig<?>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>>
+ streamSpecMaps =
mCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
cameraMode,
cameraId, existingSurfaces,
@@ -651,7 +697,14 @@
for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
suggestedStreamSpecs.put(entry.getValue(),
- useCaseConfigStreamSpecMap.get(entry.getKey()));
+ streamSpecMaps.first.get(entry.getKey()));
+ }
+ for (Map.Entry<AttachedSurfaceInfo, StreamSpec> entry :
+ streamSpecMaps.second.entrySet()) {
+ if (surfaceInfoUseCaseMap.containsKey(entry.getKey())) {
+ suggestedStreamSpecs.put(surfaceInfoUseCaseMap.get(entry.getKey()),
+ entry.getValue());
+ }
}
}
return suggestedStreamSpecs;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 93cd629..830387a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -36,6 +36,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
@@ -86,6 +87,9 @@
@Nullable
private SurfaceEdge mSharingInputEdge;
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ SessionConfig.Builder mSessionConfigBuilder;
+
static {
MutableConfig mutableConfig = new StreamSharingBuilder().getMutableConfig();
mutableConfig.insertOption(OPTION_INPUT_FORMAT,
@@ -156,6 +160,18 @@
return streamSpec;
}
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ mSessionConfigBuilder.addImplementationOptions(config);
+ updateSessionConfig(mSessionConfigBuilder.build());
+ return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
+ }
+
@Override
public void onBind() {
super.onBind();
@@ -239,7 +255,11 @@
streamSpec.getResolution());
builder.addSurface(mCameraEdge.getDeferrableSurface());
builder.addRepeatingCameraCaptureCallback(mVirtualCamera.getParentMetadataCallback());
+ if (streamSpec.getImplementationOptions() != null) {
+ builder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
addCameraErrorListener(builder, cameraId, config, streamSpec);
+ mSessionConfigBuilder = builder;
return builder.build();
}
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 3195cae..dd4cd17 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
@@ -38,6 +38,7 @@
import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.ImageAnalysisConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.TagBundle;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
@@ -91,6 +92,8 @@
private static final long TIMESTAMP_1 = 1;
private static final long TIMESTAMP_2 = 2;
private static final long TIMESTAMP_3 = 3;
+ public static final androidx.camera.core.impl.Config.Option<Integer> TEST_OPTION =
+ androidx.camera.core.impl.Config.Option.create("test.testOption", int.class);
private Handler mCallbackHandler;
private Handler mBackgroundHandler;
@@ -438,6 +441,20 @@
assertCanReceiveAnalysisImage(mImageAnalysis);
}
+ @Test
+ public void sessionConfigHasStreamSpecImplementationOptions_whenUpdateStreamSpecImplOptions()
+ throws CameraUseCaseAdapter.CameraException {
+ setUpImageAnalysisWithStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST);
+ int newImplementationOptionValue = 6;
+ MutableOptionsBundle streamSpecOptions = MutableOptionsBundle.create();
+ streamSpecOptions.insertOption(TEST_OPTION, newImplementationOptionValue);
+ mImageAnalysis.updateSuggestedStreamSpecImplementationOptions(streamSpecOptions);
+ assertThat(
+ mImageAnalysis.getSessionConfig().getImplementationOptions().retrieveOption(
+ TEST_OPTION
+ )).isEqualTo(newImplementationOptionValue);
+ }
+
@SuppressWarnings("deprecation") // test for legacy resolution API
@Test
public void throwException_whenSetBothTargetResolutionAndAspectRatio() {
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 ad923b7f1..0e822e5 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
@@ -39,6 +39,7 @@
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Identifier
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.OptionsBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionProcessor
@@ -116,6 +117,12 @@
override fun onError(exception: ImageCaptureException) {
}
}
+ private val testImplementationOption: androidx.camera.core.impl.Config.Option<Int> =
+ androidx.camera.core.impl.Config.Option.create(
+ "test.testOption",
+ Int::class.javaPrimitiveType!!
+ )
+ private val testImplementationOptionValue = 5
@Before
@Throws(ExecutionException::class, InterruptedException::class)
@@ -626,6 +633,22 @@
assertThat(cameraControl.isZslConfigAdded).isTrue()
}
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenUpdateStreamSpecImplOptions() {
+ val imageCapture = bindImageCapture(
+ bufferFormat = ImageFormat.JPEG,
+ )
+ val newImplementationOptionValue = 6
+ val streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, newImplementationOptionValue)
+ imageCapture.updateSuggestedStreamSpecImplementationOptions(streamSpecOptions)
+ assertThat(
+ imageCapture.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(newImplementationOptionValue)
+ }
+
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
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 6f8fa37..3b09a2a 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
@@ -35,6 +35,7 @@
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.OptionsBundle
import androidx.camera.core.impl.PreviewConfig
import androidx.camera.core.impl.SessionConfig
@@ -80,6 +81,8 @@
@Config(
minSdk = Build.VERSION_CODES.LOLLIPOP
)
+// Option Declarations:
+// *********************************************************************************************
class PreviewTest {
private var cameraUseCaseAdapter: CameraUseCaseAdapter? = null
@@ -96,6 +99,13 @@
private val handlersToRelease = mutableListOf<Handler>()
+ private val testImplementationOption: androidx.camera.core.impl.Config.Option<Int> =
+ androidx.camera.core.impl.Config.Option.create(
+ "test.testOption",
+ Int::class.javaPrimitiveType!!
+ )
+ private val testImplementationOptionValue = 5
+
@Before
@Throws(ExecutionException::class, InterruptedException::class)
fun setUp() {
@@ -641,6 +651,30 @@
assertThat(receivedAfterAttach).isTrue()
}
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenCreatePipeline() {
+ val preview = createPreview(effect)
+ assertThat(
+ preview.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(testImplementationOptionValue)
+ }
+
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenUpdateStreamSpecImplOptions() {
+ val preview = createPreview(effect)
+ val newImplementationOptionValue = 6
+ val streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, newImplementationOptionValue)
+ preview.updateSuggestedStreamSpecImplementationOptions(streamSpecOptions)
+ assertThat(
+ preview.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(newImplementationOptionValue)
+ }
+
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
@@ -734,8 +768,11 @@
)
previewToDetach.bindToCamera(camera, null, previewConfig)
- val streamSpec = StreamSpec.builder(Size(640, 480)).build()
- previewToDetach.onSuggestedStreamSpecUpdated(streamSpec)
+ val streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, testImplementationOptionValue)
+ val streamSpec = StreamSpec.builder(Size(640, 480))
+ .setImplementationOptions(streamSpecOptions).build()
+ previewToDetach.updateSuggestedStreamSpec(streamSpec)
return previewToDetach
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index 969f695..82b2799 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -29,6 +29,7 @@
import androidx.camera.core.ImageProxy
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.UseCaseConfig
@@ -80,6 +81,12 @@
private lateinit var effectProcessor: FakeSurfaceProcessorInternal
private lateinit var sharingProcessor: FakeSurfaceProcessorInternal
private lateinit var effect: CameraEffect
+ private val testImplementationOption: androidx.camera.core.impl.Config.Option<Int> =
+ androidx.camera.core.impl.Config.Option.create(
+ "test.testOption",
+ Int::class.javaPrimitiveType!!
+ )
+ private val testImplementationOptionValue = 5
@Before
fun setUp() {
@@ -183,6 +190,57 @@
assertThat(result2.getCompleted().tagBundle.getTag(key)).isEqualTo(value)
}
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenCreatePipeline() {
+ // Arrange: set up StreamSharing with ImageCapture as child
+ val imageCapture = ImageCapture.Builder().build()
+ streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
+ streamSharing.bindToCamera(camera, null, defaultConfig)
+
+ // Act: update stream specification.
+ val streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, testImplementationOptionValue)
+ streamSharing.onSuggestedStreamSpecUpdated(
+ StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build()
+ )
+
+ // Assert: the session config gets the correct implementation options from stream
+ // specification.
+ assertThat(
+ streamSharing.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(testImplementationOptionValue)
+ }
+
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenUpdateStreamSpecImplOptions() {
+ // Arrange: set up StreamSharing with ImageCapture as child with initial stream
+ // specification implementation options.
+ val imageCapture = ImageCapture.Builder().build()
+ streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
+ streamSharing.bindToCamera(camera, null, defaultConfig)
+ var streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, testImplementationOptionValue)
+ streamSharing.updateSuggestedStreamSpec(
+ StreamSpec.builder(size).setImplementationOptions(streamSpecOptions).build()
+ )
+
+ // Act: update stream specification implementation options.
+ val newImplementationOptionValue = 6
+ streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, newImplementationOptionValue)
+ streamSharing.updateSuggestedStreamSpecImplementationOptions(streamSpecOptions)
+
+ // Assert: the session config gets the correct implementation options from stream
+ // specification.
+ assertThat(
+ streamSharing.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(newImplementationOptionValue)
+ }
+
private fun FakeUseCase.setTagBundleOnSessionConfigAsync(
key: String,
value: String
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 7909211..5d9db0e 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -285,8 +285,10 @@
@VisibleForTesting
@NonNull
public ListenableFuture<Void> shutdown() {
- runOnMainSync(this::unbindAll);
- mLifecycleCameraRepository.clear();
+ runOnMainSync(() -> {
+ unbindAll();
+ mLifecycleCameraRepository.clear();
+ });
ListenableFuture<Void> shutdownFuture = mCameraX != null ? mCameraX.shutdown() :
Futures.immediateFuture(null);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index b3ff181..bf73e16 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -23,6 +23,7 @@
import static com.google.common.primitives.Ints.asList;
+import android.util.Pair;
import android.util.Size;
import androidx.annotation.NonNull;
@@ -84,8 +85,10 @@
@NonNull
@Override
- public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
- @CameraMode.Mode int cameraMode, @NonNull String cameraId,
+ public Pair<Map<UseCaseConfig<?>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>>
+ getSuggestedStreamSpecs(
+ @CameraMode.Mode int cameraMode,
+ @NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
List<UseCaseConfig<?>> newUseCaseConfigs =
@@ -106,7 +109,7 @@
suggestedStreamSpecs.put(useCaseConfig, streamSpec);
}
- return suggestedStreamSpecs;
+ return new Pair<>(suggestedStreamSpecs, new HashMap<>());
}
/**
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 12a104a..8902d29 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -45,9 +45,11 @@
public class FakeUseCaseConfig implements UseCaseConfig<FakeUseCase>, ImageOutputConfig {
private final Config mConfig;
+ private final CaptureType mCaptureType;
- FakeUseCaseConfig(Config config) {
+ FakeUseCaseConfig(Config config, CaptureType captureType) {
mConfig = config;
+ mCaptureType = captureType;
}
@NonNull
@@ -62,6 +64,12 @@
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
}
+ @NonNull
+ @Override
+ public CaptureType getCaptureType() {
+ return mCaptureType;
+ }
+
/** Builder for an empty Config */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public static final class Builder implements
@@ -103,7 +111,7 @@
@NonNull
@Override
public FakeUseCaseConfig getUseCaseConfig() {
- return new FakeUseCaseConfig(OptionsBundle.from(mOptionsBundle));
+ return new FakeUseCaseConfig(OptionsBundle.from(mOptionsBundle), mCaptureType);
}
@Override
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index b5ba2a4..d60a5de 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -132,12 +132,12 @@
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
CameraMode.DEFAULT,
FAKE_CAMERA_ID0,
- Collections.emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig));
+ emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig)).first;
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
CameraMode.DEFAULT,
FAKE_CAMERA_ID1,
- Collections.emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig));
+ emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig)).first;
assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
index 5a9196e..3f32f37 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -30,6 +30,7 @@
import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
import androidx.camera.core.DynamicRange.FORMAT_HLG
import androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT
+import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.MutableStateObservable
@@ -115,6 +116,8 @@
arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
)
+
+ private val DYNAMIC_RANGE_HLG10 = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
}
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -390,18 +393,45 @@
@SdkSuppress(minSdkVersion = 33) // HLG10 only supported on API 33+
@Test
fun dynamicRangeHlg_selectsHlg(): Unit = runBlocking {
- val hlg10DynamicRange = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
assumeTrue(
"Device does not support HLG10",
- cameraInfo.supportedDynamicRanges.contains(hlg10DynamicRange)
+ cameraInfo.supportedDynamicRanges.contains(DYNAMIC_RANGE_HLG10)
)
testDynamicRangeSelection(
- requestedDynamicRange = hlg10DynamicRange
+ requestedDynamicRange = DYNAMIC_RANGE_HLG10
) { selectedDynamicRange ->
- assertThat(selectedDynamicRange).isEqualTo(hlg10DynamicRange)
+ assertThat(selectedDynamicRange).isEqualTo(DYNAMIC_RANGE_HLG10)
}
}
+ @SdkSuppress(minSdkVersion = 33) // HLG10 only supported on API 33+
+ @Test
+ fun dynamicRange_isSetInSessionConfig(): Unit = runBlocking {
+ // TODO(b/275632219): Disabled on camera-pipe until automatic dynamic range
+ // selection is supported
+ assumeTrue(implName != CameraPipeConfig::class.simpleName)
+ assumeTrue(
+ "Device does not support HLG10",
+ cameraInfo.supportedDynamicRanges.contains(DYNAMIC_RANGE_HLG10)
+ )
+
+ // Arrange.
+ val videoOutput = createTestVideoOutput()
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setDynamicRange(DYNAMIC_RANGE_HLG10)
+ .build()
+
+ // Act.
+ withContext(Dispatchers.Main) {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ // Wait for surface request to ensure session config was attached
+ videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ val outputConfig = videoCapture.sessionConfig.outputConfigs.first()
+ assertThat(outputConfig.dynamicRange).isEqualTo(DYNAMIC_RANGE_HLG10)
+ }
@SdkSuppress(minSdkVersion = 33) // 10-bit HDR only supported on API 33+
@Test
@@ -421,6 +451,46 @@
}
}
+ @SdkSuppress(minSdkVersion = 33) // 10-bit HDR only supported on API 33+
+ @Test
+ fun dynamicRangeHlg_selectsAndAppliesHlgForConcurrentPreview(): Unit = runBlocking {
+ // TODO(b/275632219): Disabled on camera-pipe until automatic dynamic range
+ // selection is supported
+ assumeTrue(implName != CameraPipeConfig::class.simpleName)
+ assumeTrue(
+ "Device does not support HLG10",
+ cameraInfo.supportedDynamicRanges.contains(DYNAMIC_RANGE_HLG10)
+ )
+
+ // Arrange.
+ val videoOutput = createTestVideoOutput()
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setDynamicRange(DYNAMIC_RANGE_HLG10)
+ .build()
+ // Preview will derive dynamic range from VideoCapture since it uses
+ // DynamicRange.UNSPECIFIED by default.
+ val preview = Preview.Builder().build()
+
+ // Act.
+ val deferredSurfaceRequest = CompletableDeferred<SurfaceRequest>()
+ withContext(Dispatchers.Main) {
+ // SurfaceProvider will run on main thread
+ preview.setSurfaceProvider {
+ deferredSurfaceRequest.complete(it)
+ }
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture, preview))
+ }
+
+ // Assert.
+ val timeout = 5.seconds
+ val previewSurfaceRequest = withTimeoutOrNull(timeout) {
+ deferredSurfaceRequest.await()
+ } ?: fail("Timed out waiting for Preview SurfaceRequest. Waited $timeout.")
+ val previewOutputConfig = preview.sessionConfig.outputConfigs.first()
+ assertThat(previewSurfaceRequest.dynamicRange).isEqualTo(DYNAMIC_RANGE_HLG10)
+ assertThat(previewOutputConfig.dynamicRange).isEqualTo(DYNAMIC_RANGE_HLG10)
+ }
+
private suspend fun testDynamicRangeSelection(
requestedDynamicRange: DynamicRange? = null,
assertBlock: (selectedDynamicRange: DynamicRange) -> Unit
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 8d75923..0a0373e 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
@@ -463,11 +463,13 @@
+ "specification should be already updated and shouldn't be null.");
Preconditions.checkState(mSurfaceRequest == null, "The surface request should be null "
+ "when VideoCapture is attached.");
+ StreamSpec attachedStreamSpec = Preconditions.checkNotNull(getAttachedStreamSpec());
mStreamInfo = fetchObservableValue(getOutput().getStreamInfo(),
StreamInfo.STREAM_INFO_ANY_INACTIVE);
mSessionConfigBuilder = createPipeline(getCameraId(),
- (VideoCaptureConfig<T>) getCurrentConfig(), getAttachedStreamSpec());
- applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
+ (VideoCaptureConfig<T>) getCurrentConfig(), attachedStreamSpec);
+ applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo,
+ attachedStreamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
// VideoCapture has to be active to apply SessionConfig's template type.
notifyActive();
@@ -508,6 +510,18 @@
clearPipeline();
}
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ protected StreamSpec onSuggestedStreamSpecImplementationOptionsUpdated(@NonNull Config config) {
+ mSessionConfigBuilder.addImplementationOptions(config);
+ updateSessionConfig(mSessionConfigBuilder.build());
+ return getAttachedStreamSpec().toBuilder().setImplementationOptions(config).build();
+ }
+
@NonNull
@Override
public String toString() {
@@ -711,6 +725,9 @@
if (USE_TEMPLATE_PREVIEW_BY_QUIRK) {
sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
}
+ if (streamSpec.getImplementationOptions() != null) {
+ sessionConfigBuilder.addImplementationOptions(streamSpec.getImplementationOptions());
+ }
return sessionConfigBuilder;
}
@@ -762,7 +779,8 @@
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
mSessionConfigBuilder = createPipeline(cameraId, config, streamSpec);
- applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo);
+ applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder, mStreamInfo,
+ streamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
}
@@ -861,6 +879,7 @@
// Doing resetPipeline() includes notifyReset/notifyUpdated(). Doing NotifyReset()
// includes notifyUpdated(). So we just take actions on higher order item for
// optimization.
+ StreamSpec attachedStreamSpec = Preconditions.checkNotNull(getAttachedStreamSpec());
if (!StreamInfo.NON_SURFACE_STREAM_ID.contains(currentStreamInfo.getId())
&& !StreamInfo.NON_SURFACE_STREAM_ID.contains(streamInfo.getId())
&& currentStreamInfo.getId() != streamInfo.getId()) {
@@ -874,11 +893,15 @@
&& streamInfo.getId() != STREAM_ID_ERROR)) {
// If id switch to STREAM_ID_ERROR, it means VideoOutput is failed to setup video
// stream. The surface should be removed from camera. Vice versa.
- applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, streamInfo);
+ applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder,
+ streamInfo,
+ attachedStreamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
} else if (currentStreamInfo.getStreamState() != streamInfo.getStreamState()) {
- applyStreamInfoToSessionConfigBuilder(mSessionConfigBuilder, streamInfo);
+ applyStreamInfoAndStreamSpecToSessionConfigBuilder(mSessionConfigBuilder,
+ streamInfo,
+ attachedStreamSpec);
updateSessionConfig(mSessionConfigBuilder.build());
notifyUpdated();
}
@@ -892,8 +915,9 @@
@MainThread
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- void applyStreamInfoToSessionConfigBuilder(@NonNull SessionConfig.Builder sessionConfigBuilder,
- @NonNull StreamInfo streamInfo) {
+ void applyStreamInfoAndStreamSpecToSessionConfigBuilder(
+ @NonNull SessionConfig.Builder sessionConfigBuilder,
+ @NonNull StreamInfo streamInfo, @NonNull StreamSpec streamSpec) {
final boolean isStreamError = streamInfo.getId() == StreamInfo.STREAM_ID_ERROR;
final boolean isStreamActive = streamInfo.getStreamState() == StreamState.ACTIVE;
if (isStreamError && isStreamActive) {
@@ -902,11 +926,12 @@
}
sessionConfigBuilder.clearSurfaces();
+ DynamicRange dynamicRange = streamSpec.getDynamicRange();
if (!isStreamError) {
if (isStreamActive) {
- sessionConfigBuilder.addSurface(mDeferrableSurface);
+ sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange);
} else {
- sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
+ sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface, dynamicRange);
}
} // Don't attach surface when stream is invalid.
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 ac5b39b..ccc85b0 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
@@ -57,6 +57,7 @@
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.MutableStateObservable
import androidx.camera.core.impl.Observable
import androidx.camera.core.impl.StreamSpec
@@ -141,6 +142,12 @@
private lateinit var camera: FakeCamera
private var surfaceRequestsToRelease = mutableListOf<SurfaceRequest>()
private val handlersToRelease = mutableListOf<Handler>()
+ private val testImplementationOption: androidx.camera.core.impl.Config.Option<Int> =
+ androidx.camera.core.impl.Config.Option.create(
+ "test.testOption",
+ Int::class.javaPrimitiveType!!
+ )
+ private val testImplementationOptionValue = 5
@Before
fun setup() {
@@ -1276,6 +1283,28 @@
)
}
+ @Test
+ fun sessionConfigHasStreamSpecImplementationOptions_whenUpdateStreamSpecImplOptions() {
+ // Arrange.
+ setupCamera()
+ createCameraUseCaseAdapter()
+ cameraUseCaseAdapter.setEffects(listOf(createFakeEffect()))
+ val videoCapture = createVideoCapture(createVideoOutput())
+ // Act.
+ addAndAttachUseCases(videoCapture)
+ val newImplementationOptionValue = 6
+ val streamSpecOptions = MutableOptionsBundle.create()
+ streamSpecOptions.insertOption(testImplementationOption, newImplementationOptionValue)
+ videoCapture.updateSuggestedStreamSpecImplementationOptions(streamSpecOptions)
+ addAndAttachUseCases(videoCapture)
+ // Assert.
+ assertThat(
+ videoCapture.sessionConfig.implementationOptions.retrieveOption(
+ testImplementationOption
+ )
+ ).isEqualTo(newImplementationOptionValue)
+ }
+
private fun testSurfaceRequestContainsExpected(
quality: Quality = HD, // HD maps to 1280x720 (4:3)
videoEncoderInfo: VideoEncoderInfo = createVideoEncoderInfo(),
diff --git a/camera/camera-viewfinder-compose/api/current.txt b/camera/camera-viewfinder-compose/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-viewfinder-compose/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-viewfinder-compose/api/public_plus_experimental_current.txt b/camera/camera-viewfinder-compose/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-viewfinder-compose/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-viewfinder-compose/api/res-current.txt b/camera/camera-viewfinder-compose/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/camera/camera-viewfinder-compose/api/res-current.txt
diff --git a/camera/camera-viewfinder-compose/api/restricted_current.txt b/camera/camera-viewfinder-compose/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/camera/camera-viewfinder-compose/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/camera/camera-viewfinder-compose/build.gradle b/camera/camera-viewfinder-compose/build.gradle
new file mode 100644
index 0000000..3476e5e
--- /dev/null
+++ b/camera/camera-viewfinder-compose/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.camera.viewfinder.compose"
+}
+
+androidx {
+ name = "androidx.camera:camera-viewfinder-compose"
+ type = LibraryType.PUBLISHED_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ inceptionYear = "2023"
+ description = "Composable ViewFinder implementation for CameraX"
+}
diff --git a/camera/camera-viewfinder-compose/src/main/java/androidx/camera/androidx-camera-camera-viewfinder-compose-documentation.md b/camera/camera-viewfinder-compose/src/main/java/androidx/camera/androidx-camera-camera-viewfinder-compose-documentation.md
new file mode 100644
index 0000000..10d4659
--- /dev/null
+++ b/camera/camera-viewfinder-compose/src/main/java/androidx/camera/androidx-camera-camera-viewfinder-compose-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+CameraX ViewFinder Compose
+
+# Package androidx.camera.viewfinder.compose
+
+Library providing a composable ViewFinder
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ColorVectorConverter.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ColorVectorConverter.kt
index 2b1411e..b11c7c0 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ColorVectorConverter.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ColorVectorConverter.kt
@@ -21,7 +21,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpace
import androidx.compose.ui.graphics.colorspace.ColorSpaces
-import kotlin.math.pow
/**
* A lambda that takes a [ColorSpace] and returns a converter that can both convert a [Color] to
@@ -32,34 +31,17 @@
{ colorSpace ->
TwoWayConverter(
convertToVector = { color ->
- // TODO: use Oklab when it is public API
- val colorXyz = color.convert(ColorSpaces.CieXyz)
- val x = colorXyz.red
- val y = colorXyz.green
- val z = colorXyz.blue
-
- val l = multiplyColumn(0, x, y, z, M1).pow(1f / 3f)
- val a = multiplyColumn(1, x, y, z, M1).pow(1f / 3f)
- val b = multiplyColumn(2, x, y, z, M1).pow(1f / 3f)
- AnimationVector4D(color.alpha, l, a, b)
+ val (l, a, b, alpha) = color.convert(ColorSpaces.Oklab)
+ AnimationVector4D(alpha, l, a, b)
},
- convertFromVector = {
- val l = it.v2.pow(3f)
- val a = it.v3.pow(3f)
- val b = it.v4.pow(3f)
-
- val x = multiplyColumn(0, l, a, b, InverseM1)
- val y = multiplyColumn(1, l, a, b, InverseM1)
- val z = multiplyColumn(2, l, a, b, InverseM1)
-
- val colorXyz = Color(
- alpha = it.v1.coerceIn(0f, 1f),
- red = x.coerceIn(-2f, 2f),
- green = y.coerceIn(-2f, 2f),
- blue = z.coerceIn(-2f, 2f),
- colorSpace = ColorSpaces.CieXyz // here we have the right color space
- )
- colorXyz.convert(colorSpace)
+ convertFromVector = { vector ->
+ Color(
+ vector.v2.coerceIn(0f, 1f), // L (red)
+ vector.v3.coerceIn(-0.5f, 0.5f), // a (blue)
+ vector.v4.coerceIn(-0.5f, 0.5f), // b (green)
+ vector.v1.coerceIn(0f, 1f), // alpha
+ ColorSpaces.Oklab
+ ).convert(colorSpace)
}
)
}
@@ -70,23 +52,5 @@
* [ColorSpace].
*/
val Color.Companion.VectorConverter:
- (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>
- get() = ColorToVector
-
-// These are utilities and constants to emulate converting to/from Oklab color space.
-// These can be removed when Oklab becomes public and we can use it directly in the conversion.
-private val M1 = floatArrayOf(
- 0.80405736f, 0.026893456f, 0.04586542f,
- 0.3188387f, 0.9319606f, 0.26299807f,
- -0.11419419f, 0.05105356f, 0.83999807f
-)
-
-private val InverseM1 = floatArrayOf(
- 1.2485008f, -0.032856926f, -0.057883114f,
- -0.48331892f, 1.1044513f, -0.3194066f,
- 0.19910365f, -0.07159331f, 1.202023f
-)
-
-private fun multiplyColumn(column: Int, x: Float, y: Float, z: Float, matrix: FloatArray): Float {
- return x * matrix[column] + y * matrix[3 + column] + z * matrix[6 + column]
-}
\ No newline at end of file
+ (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>
+ get() = ColorToVector
diff --git a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
index 1134f6c..4a889dc 100644
--- a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
+++ b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
@@ -63,48 +63,46 @@
0f
)
- // all channels should clamp:
+ // all channels are clamped before conversion, the result of 3/3/3 in OkLab should be
+ // 1,0,0 in sRGB
+ val maxOkLab = Color(1.0f, 0.5f, 0.5f, 1.0f, ColorSpaces.Oklab).convert(ColorSpaces.Srgb)
assertEquals(
- 1f,
+ maxOkLab.red,
converter.convertFromVector(AnimationVector4D(1.0f, 3f, 3f, 3f)).red,
0f
)
assertEquals(
- 1f,
+ maxOkLab.green,
converter.convertFromVector(AnimationVector4D(1.0f, 3f, 3f, 3f)).green,
0f
)
assertEquals(
- 1f,
+ maxOkLab.blue,
converter.convertFromVector(AnimationVector4D(1.0f, 3f, 3f, 3f)).blue,
0f
)
- // All channel below 0.0f clamps to 0.0f and the result is black
+ // all channels are clamped before conversion, the result of -3/-3/-3 in OkLab should be
+ // 0,0,1 in sRGB
+ val minOkLab = Color(0.0f, -0.5f, -0.5f, 1.0f, ColorSpaces.Oklab).convert(ColorSpaces.Srgb)
assertEquals(
- 0f,
+ minOkLab.red,
converter.convertFromVector(AnimationVector4D(1.0f, -3f, -3f, -3f))
.red,
0f
)
assertEquals(
- 0f,
+ minOkLab.green,
converter.convertFromVector(AnimationVector4D(1.0f, -3f, -3f, -3f))
.green,
0f
)
assertEquals(
- 0f,
+ minOkLab.blue,
converter.convertFromVector(AnimationVector4D(1.0f, -3f, -3f, -3f))
.blue,
0f
)
-
- // Green channel above 1.0f clamps to 1.0f and the result is green
- assertEquals(
- converter.convertFromVector(AnimationVector4D(1.0f, 0.0f, 1.1f, 0f)),
- Color.Green
- )
}
@Test
diff --git a/compose/compiler/OWNERS b/compose/compiler/OWNERS
index e48b97d..51ec95d 100644
--- a/compose/compiler/OWNERS
+++ b/compose/compiler/OWNERS
@@ -3,4 +3,5 @@
chuckj@google.com
lelandr@google.com
anbailey@google.com
+ashikov@google.com
per-file settings.gradle = dustinlam@google.com, rahulrav@google.com
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
index 7356bd9..cee92f7 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
@@ -53,6 +53,7 @@
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -1836,6 +1837,7 @@
)
}
+ @Ignore // b/281171119
@Test
fun testModifiers_doNotCauseUnnecessaryRemeasure() {
var first by mutableStateOf(true)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 5068c23..54b93c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -384,7 +384,7 @@
decoration = textDecoration
)
} else {
- val overrideColorVal = overrideColor?.produce() ?: Color.Unspecified
+ val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
val color = if (overrideColorVal.isSpecified) {
overrideColorVal
} else if (style.color.isSpecified) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index 2496e86..76386b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -303,7 +303,7 @@
textDecoration = textDecoration
)
} else {
- val overrideColorVal = overrideColor?.produce() ?: Color.Unspecified
+ val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
val color = if (overrideColorVal.isSpecified) {
overrideColorVal
} else if (style.color.isSpecified) {
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
deleted file mode 100644
index 5d7c893..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
+++ /dev/null
@@ -1,420 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress("unused", "ControlFlowWithEmptyBody", "UNUSED_PARAMETER", "UNUSED_VARIABLE")
-
-package androidx.compose.integration.docs.state
-
-import android.content.res.Resources
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Scaffold
-import androidx.compose.material.ScaffoldState
-import androidx.compose.material.Text
-import androidx.compose.material.rememberScaffoldState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.mapSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.rememberNavController
-import kotlinx.coroutines.launch
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/state
- *
- * No action required if it's modified.
- */
-
-private object StateSnippet1 {
- @Composable
- fun HelloContent() {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = "Hello!",
- modifier = Modifier.padding(bottom = 8.dp),
- style = MaterialTheme.typography.h5
- )
- OutlinedTextField(
- value = "",
- onValueChange = { },
- label = { Text("Name") }
- )
- }
- }
-}
-
-private object StateSnippet2 {
- /*
- * MutableState is part of the API, so StateSnippet2 can't have the exact code
- * for the published documentation. Instead, use the following commented code
- * in the doc. If FakeState<T> changes, update the commented code accordingly.
- *
- interface MutableState<T> : State<T> {
- override var value: T
- }
- */
- interface FakeState<T> : State<T> {
- override var value: T
- }
-
- interface FakeMutableState<T> : MutableState<String>
-}
-
-private object StateSnippet3 {
- @Composable
- fun HelloContent() {
- Column(modifier = Modifier.padding(16.dp)) {
- var name by remember { mutableStateOf("") }
- if (name.isNotEmpty()) {
- Text(
- text = "Hello, $name!",
- modifier = Modifier.padding(bottom = 8.dp),
- style = MaterialTheme.typography.h5
- )
- }
- OutlinedTextField(
- value = name,
- onValueChange = { name = it },
- label = { Text("Name") }
- )
- }
- }
-}
-
-private object StateSnippet4 {
- @Composable
- fun HelloScreen() {
- var name by rememberSaveable { mutableStateOf("") }
-
- HelloContent(name = name, onNameChange = { name = it })
- }
-
- @Composable
- fun HelloContent(name: String, onNameChange: (String) -> Unit) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = "Hello, $name",
- modifier = Modifier.padding(bottom = 8.dp),
- style = MaterialTheme.typography.h5
- )
- OutlinedTextField(
- value = name,
- onValueChange = onNameChange,
- label = { Text("Name") }
- )
- }
- }
-}
-
-private object StateSnippet5 {
- @Parcelize
- data class City(val name: String, val country: String) : Parcelable
-
- @Composable
- fun CityScreen() {
- var selectedCity = rememberSaveable {
- mutableStateOf(City("Madrid", "Spain"))
- }
- }
-}
-
-private object StateSnippet6 {
- data class City(val name: String, val country: String)
-
- val CitySaver = run {
- val nameKey = "Name"
- val countryKey = "Country"
- mapSaver(
- save = { mapOf(nameKey to it.name, countryKey to it.country) },
- restore = { City(it[nameKey] as String, it[countryKey] as String) }
- )
- }
-
- @Composable
- fun CityScreen() {
- var selectedCity = rememberSaveable(stateSaver = CitySaver) {
- mutableStateOf(City("Madrid", "Spain"))
- }
- }
-}
-
-@Composable
-private fun StateSnippets7() {
- data class City(val name: String, val country: String)
-
- val CitySaver = listSaver<City, Any>(
- save = { listOf(it.name, it.country) },
- restore = { City(it[0] as String, it[1] as String) }
- )
-
- @Composable
- fun CityScreen() {
- var selectedCity = rememberSaveable(stateSaver = CitySaver) {
- mutableStateOf(City("Madrid", "Spain"))
- }
- }
-}
-
-@Composable
-private fun StateSnippets8() {
- @Composable
- fun MyApp() {
- MyTheme {
- val scaffoldState = rememberScaffoldState()
- val coroutineScope = rememberCoroutineScope()
-
- Scaffold(scaffoldState = scaffoldState) { innerPadding ->
- MyContent(
- showSnackbar = { message ->
- coroutineScope.launch {
- scaffoldState.snackbarHostState.showSnackbar(message)
- }
- },
- modifier = Modifier.padding(innerPadding)
- )
- }
- }
- }
-}
-
-@Composable
-private fun StateSnippets9() {
-
- // Plain class that manages App's UI logic and UI elements' state
- class MyAppState(
- val scaffoldState: ScaffoldState,
- val navController: NavHostController,
- private val resources: Resources,
- /* ... */
- ) {
- val bottomBarTabs = /* State */
- // DO NOT COPY IN DAC
- Unit
-
- // Logic to decide when to show the bottom bar
- val shouldShowBottomBar: Boolean
- get() = /* ... */
- // DO NOT COPY IN DAC
- false
-
- // Navigation logic, which is a type of UI logic
- fun navigateToBottomBarRoute(route: String) { /* ... */ }
-
- // Show snackbar using Resources
- fun showSnackbar(message: String) { /* ... */ }
- }
-
- @Composable
- fun rememberMyAppState(
- scaffoldState: ScaffoldState = rememberScaffoldState(),
- navController: NavHostController = rememberNavController(),
- resources: Resources = LocalContext.current.resources,
- /* ... */
- ) = remember(scaffoldState, navController, resources, /* ... */) {
- MyAppState(scaffoldState, navController, resources, /* ... */)
- }
-}
-
-@Composable
-private fun StateSnippets10() {
- @Composable
- fun MyApp() {
- MyTheme {
- val myAppState = rememberMyAppState()
- Scaffold(
- scaffoldState = myAppState.scaffoldState,
- bottomBar = {
- if (myAppState.shouldShowBottomBar) {
- BottomBar(
- tabs = myAppState.bottomBarTabs,
- navigateToRoute = {
- myAppState.navigateToBottomBarRoute(it)
- }
- )
- }
- }
- ) { innerPadding ->
- NavHost(
- navController = myAppState.navController,
- startDestination = "initial",
- modifier = Modifier.padding(innerPadding)
- ) { /* ... */ }
- }
- }
- }
-}
-
-@Composable
-private fun StateSnippets11() {
- data class ExampleUiState(
- val dataToDisplayOnScreen: List<Example> = emptyList(),
- val userMessages: List<Message> = emptyList(),
- val loading: Boolean = false
- )
-
- class ExampleViewModel(
- private val repository: MyRepository,
- private val savedState: SavedStateHandle
- ) : ViewModel() {
-
- var uiState by mutableStateOf(ExampleUiState())
- private set
-
- // Business logic
- fun somethingRelatedToBusinessLogic() { /* ... */ }
- }
-
- @Composable
- fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
-
- val uiState = viewModel.uiState
- /* ... */
-
- ExampleReusableComponent(
- someData = uiState.dataToDisplayOnScreen,
- onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
- )
- }
-
- @Composable
- fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
- /* ... */
- Button(onClick = onDoSomething) {
- Text("Do something")
- }
- }
-}
-
-@Composable
-private fun StateSnippets12() {
- class ExampleState(
- val lazyListState: LazyListState,
- private val resources: Resources,
- private val expandedItems: List<Item> = emptyList()
- ) {
- fun isExpandedItem(item: Item): Boolean = TODO()
- /* ... */
- }
-
- @Composable
- fun rememberExampleState(/* ... */): ExampleState { TODO() }
-
- @Composable
- fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
-
- val uiState = viewModel.uiState
- val exampleState = rememberExampleState()
-
- LazyColumn(state = exampleState.lazyListState) {
- items(uiState.dataToDisplayOnScreen) { item ->
- if (exampleState.isExpandedItem(item)) {
- /* ... */
- }
- /* ... */
- }
- }
- }
-}
-
-/*
- * Fakes needed for snippets to build:
- */
-
-private object binding {
- object helloText {
- var text = ""
- }
-
- object textInput {
- fun doAfterTextChanged(function: () -> Unit) {}
- }
-}
-
-@Composable
-private fun MyTheme(content: @Composable () -> Unit) {}
-
-@Composable
-private fun MyContent(showSnackbar: (String) -> Unit, modifier: Modifier = Modifier) {}
-
-@Composable
-private fun BottomBar(tabs: Unit, navigateToRoute: (String) -> Unit) {}
-
-@Composable
-private fun rememberMyAppState(
- scaffoldState: ScaffoldState = rememberScaffoldState(),
- navController: NavHostController = rememberNavController(),
- resources: Resources = LocalContext.current.resources
-) = remember(scaffoldState, navController, resources) {
- MyAppState(scaffoldState, navController, resources)
-}
-
-private class MyAppState(
- val scaffoldState: ScaffoldState,
- val navController: NavHostController,
- private val resources: Resources,
-) {
- val shouldShowBottomBar: Boolean = false
- val bottomBarTabs = Unit
- fun navigateToBottomBarRoute(route: String) {}
-}
-
-/**
- * Add fake Parcelize and Parcelable to avoid adding AndroidX wide dependency on
- * kotlin-parcelize just for snippets
- */
-private annotation class Parcelize
-private interface Parcelable
-
-private class Example
-private class Item
-private class Message
-private class MyRepository
-
-@Composable
-private fun ExampleReusableComponent(someData: List<Example>, onDoSomething: () -> Unit) {}
-
-private class ExampleViewModel : ViewModel() {
- val uiState = ExampleUiState()
-}
-
-private data class ExampleUiState(
- val dataToDisplayOnScreen: List<Item> = emptyList(),
- val userMessages: List<Message> = emptyList(),
- val loading: Boolean = false
-)
\ No newline at end of file
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Custom.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Custom.kt
deleted file mode 100644
index beb52bb5..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Custom.kt
+++ /dev/null
@@ -1,364 +0,0 @@
-// ktlint-disable indent https://github.com/pinterest/ktlint/issues/967
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "RemoveEmptyParenthesesFromLambdaCall"
-)
-
-package androidx.compose.integration.docs.theming
-
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.shape.ZeroCornerSize
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Colors
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.ProvideTextStyle
-import androidx.compose.material.Shapes
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.compositeOver
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/themes/custom
- *
- * No action required if it's modified.
- */
-
-private object CustomSnippet1 {
- // Use with MaterialTheme.colors.snackbarAction
- val Colors.snackbarAction: Color
- get() = if (isLight) Red300 else Red700
-
- // Use with MaterialTheme.typography.textFieldInput
- val Typography.textFieldInput: TextStyle
- get() = TextStyle(/* ... */)
-
- // Use with MaterialTheme.shapes.card
- val Shapes.card: Shape
- get() = RoundedCornerShape(size = 20.dp)
-}
-
-private object CustomSnippet234 {
- // Start snippet 2
- @Immutable
- data class ExtendedColors(
- val tertiary: Color,
- val onTertiary: Color
- )
-
- val LocalExtendedColors = staticCompositionLocalOf {
- ExtendedColors(
- tertiary = Color.Unspecified,
- onTertiary = Color.Unspecified
- )
- }
-
- @Composable
- fun ExtendedTheme(
- /* ... */
- content: @Composable () -> Unit
- ) {
- val extendedColors = ExtendedColors(
- tertiary = Color(0xFFA8EFF0),
- onTertiary = Color(0xFF002021)
- )
- CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
- MaterialTheme(
- /* colors = ..., typography = ..., shapes = ... */
- content = content
- )
- }
- }
-
- // Use with eg. ExtendedTheme.colors.tertiary
- object ExtendedTheme {
- val colors: ExtendedColors
- @Composable
- get() = LocalExtendedColors.current
- }
- // End snippet 2
-
- // Start snippet 3
- @Composable
- fun ExtendedButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable RowScope.() -> Unit
- ) {
- Button(
- colors = ButtonDefaults.buttonColors(
- backgroundColor = ExtendedTheme.colors.tertiary,
- contentColor = ExtendedTheme.colors.onTertiary
- /* Other colors use values from MaterialTheme */
- ),
- onClick = onClick,
- modifier = modifier,
- content = content
- )
- }
- // End snippet 3
-
- // Start snippet 4
- @Composable
- fun ExtendedApp() {
- ExtendedTheme {
- /*...*/
- ExtendedButton(onClick = { /* ... */ }) {
- /* ... */
- }
- }
- }
- // End snippet 4
-}
-
-private object CustomSnippet567 {
- // Start snippet 5
- @Immutable
- data class ReplacementTypography(
- val body: TextStyle,
- val title: TextStyle
- )
-
- @Immutable
- data class ReplacementShapes(
- val component: Shape,
- val surface: Shape
- )
-
- val LocalReplacementTypography = staticCompositionLocalOf {
- ReplacementTypography(
- body = TextStyle.Default,
- title = TextStyle.Default
- )
- }
- val LocalReplacementShapes = staticCompositionLocalOf {
- ReplacementShapes(
- component = RoundedCornerShape(ZeroCornerSize),
- surface = RoundedCornerShape(ZeroCornerSize)
- )
- }
-
- @Composable
- fun ReplacementTheme(
- /* ... */
- content: @Composable () -> Unit
- ) {
- val replacementTypography = ReplacementTypography(
- body = TextStyle(fontSize = 16.sp),
- title = TextStyle(fontSize = 32.sp)
- )
- val replacementShapes = ReplacementShapes(
- component = RoundedCornerShape(percent = 50),
- surface = RoundedCornerShape(size = 40.dp)
- )
- CompositionLocalProvider(
- LocalReplacementTypography provides replacementTypography,
- LocalReplacementShapes provides replacementShapes
- ) {
- MaterialTheme(
- /* colors = ... */
- content = content
- )
- }
- }
-
- // Use with eg. ReplacementTheme.typography.body
- object ReplacementTheme {
- val typography: ReplacementTypography
- @Composable
- get() = LocalReplacementTypography.current
- val shapes: ReplacementShapes
- @Composable
- get() = LocalReplacementShapes.current
- }
- // End snippet 5
-
- // Start snippet 6
- @Composable
- fun ReplacementButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable RowScope.() -> Unit
- ) {
- Button(
- shape = ReplacementTheme.shapes.component,
- onClick = onClick,
- modifier = modifier,
- content = {
- ProvideTextStyle(
- value = ReplacementTheme.typography.body
- ) {
- content()
- }
- }
- )
- }
- // End snippet 6
-
- // Start snippet 7
- @Composable
- fun ReplacementApp() {
- ReplacementTheme {
- /*...*/
- ReplacementButton(onClick = { /* ... */ }) {
- /* ... */
- }
- }
- }
- // End snippet 7
-}
-
-private object CustomSnippet89 {
- // Start snippet 8
- @Immutable
- data class CustomColors(
- val content: Color,
- val component: Color,
- val background: List<Color>
- )
-
- @Immutable
- data class CustomTypography(
- val body: TextStyle,
- val title: TextStyle
- )
-
- @Immutable
- data class CustomElevation(
- val default: Dp,
- val pressed: Dp
- )
-
- val LocalCustomColors = staticCompositionLocalOf {
- CustomColors(
- content = Color.Unspecified,
- component = Color.Unspecified,
- background = emptyList()
- )
- }
- val LocalCustomTypography = staticCompositionLocalOf {
- CustomTypography(
- body = TextStyle.Default,
- title = TextStyle.Default
- )
- }
- val LocalCustomElevation = staticCompositionLocalOf {
- CustomElevation(
- default = Dp.Unspecified,
- pressed = Dp.Unspecified
- )
- }
-
- @Composable
- fun CustomTheme(
- /* ... */
- content: @Composable () -> Unit
- ) {
- val customColors = CustomColors(
- content = Color(0xFFDD0D3C),
- component = Color(0xFFC20029),
- background = listOf(Color.White, Color(0xFFF8BBD0))
- )
- val customTypography = CustomTypography(
- body = TextStyle(fontSize = 16.sp),
- title = TextStyle(fontSize = 32.sp)
- )
- val customElevation = CustomElevation(
- default = 4.dp,
- pressed = 8.dp
- )
- CompositionLocalProvider(
- LocalCustomColors provides customColors,
- LocalCustomTypography provides customTypography,
- LocalCustomElevation provides customElevation,
- content = content
- )
- }
-
- // Use with eg. CustomTheme.elevation.small
- object CustomTheme {
- val colors: CustomColors
- @Composable
- get() = LocalCustomColors.current
- val typography: CustomTypography
- @Composable
- get() = LocalCustomTypography.current
- val elevation: CustomElevation
- @Composable
- get() = LocalCustomElevation.current
- }
- // End snippet 8
-
- // Start snippet 9
- @Composable
- fun CustomButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable RowScope.() -> Unit
- ) {
- Button(
- colors = ButtonDefaults.buttonColors(
- backgroundColor = CustomTheme.colors.component,
- contentColor = CustomTheme.colors.content,
- disabledBackgroundColor = CustomTheme.colors.content
- .copy(alpha = 0.12f)
- .compositeOver(CustomTheme.colors.component),
- disabledContentColor = CustomTheme.colors.content
- .copy(alpha = ContentAlpha.disabled)
- ),
- shape = ButtonShape,
- elevation = ButtonDefaults.elevation(
- defaultElevation = CustomTheme.elevation.default,
- pressedElevation = CustomTheme.elevation.pressed
- /* disabledElevation = 0.dp */
- ),
- onClick = onClick,
- modifier = modifier,
- content = {
- ProvideTextStyle(
- value = CustomTheme.typography.body
- ) {
- content()
- }
- }
- )
- }
-
- val ButtonShape = RoundedCornerShape(percent = 50)
- // End snippet 9
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private val Red300 = Color(0xffff0000)
-private val Red700 = Color(0xffff0000)
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material.kt
deleted file mode 100644
index 8885249..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material.kt
+++ /dev/null
@@ -1,448 +0,0 @@
-// ktlint-disable indent https://github.com/pinterest/ktlint/issues/967
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "RemoveEmptyParenthesesFromLambdaCall"
-)
-
-package androidx.compose.integration.docs.theming
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Icon
-import androidx.compose.material.LocalContentAlpha
-import androidx.compose.material.LocalElevationOverlay
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Shapes
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.material.Typography
-import androidx.compose.material.contentColorFor
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-import androidx.compose.material.primarySurface
-import androidx.compose.material.ripple.LocalRippleTheme
-import androidx.compose.material.ripple.RippleTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.Immutable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.compositeOver
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/themes/material
- *
- * No action required if it's modified.
- */
-
-private object MaterialSnippet1 {
- /* Can't be compiled. See snippet below for changes.
- MaterialTheme(
- colors = ...,
- typography = ...,
- shapes = ...
- ) {
- // app content
- }
- */
- @Composable
- fun MaterialTheming() {
- MaterialTheme(
- colors = MaterialTheme.colors,
- typography = MaterialTheme.typography,
- shapes = MaterialTheme.shapes
- ) { }
- }
-}
-
-private object MaterialSnippet2 {
- val Red = Color(0xffff0000)
- val Blue = Color(red = 0f, green = 0f, blue = 1f)
-}
-
-private object MaterialSnippet3 {
- private val Yellow200 = Color(0xffffeb46)
- private val Blue200 = Color(0xff91a4fc)
- // ...
-
- private val DarkColors = darkColors(
- primary = Yellow200,
- secondary = Blue200,
- // ...
- )
- private val LightColors = lightColors(
- primary = Yellow500,
- primaryVariant = Yellow400,
- secondary = Blue700,
- // ...
- )
-}
-
-private object MaterialSnippet4 {
- @Composable
- fun MaterialTheming() {
- MaterialTheme(
- colors = if (darkTheme) DarkColors else LightColors
- ) {
- // app content
- }
- }
-}
-
-private object MaterialSnippet5 {
- @Composable
- fun MaterialTheming() {
- Text(
- text = "Hello theming",
- color = MaterialTheme.colors.primary
- )
- }
-}
-
-private object MaterialSnippet6 {
- @Composable
- fun MaterialTheming() {
- /* This snippet comes from the API. It needs to be updated if the snippet below is modified:
-Surface(
- color: Color = MaterialTheme.colors.surface,
- contentColor: Color = contentColorFor(color),
- ...
-
-TopAppBar(
- backgroundColor: Color = MaterialTheme.colors.primarySurface,
- contentColor: Color = contentColorFor(backgroundColor),
- ...
- */
- Column {
- Surface(
- color = MaterialTheme.colors.surface,
- contentColor = contentColorFor(MaterialTheme.colors.surface)
- ) {}
- TopAppBar(
- backgroundColor = MaterialTheme.colors.primarySurface,
- contentColor = contentColorFor(MaterialTheme.colors.primarySurface)
- ) {}
- }
- }
-}
-
-private object MaterialSnippet7 {
- @Composable
- fun MaterialTheming() {
- // By default, both Icon & Text use the combination of LocalContentColor &
- // LocalContentAlpha. De-emphasize content by setting content alpha
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Text(/*...*/)
- }
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
- Icon(/*...*/)
- Text(/*...*/)
- }
- }
- @Composable private fun Icon() { }
- @Composable private fun Text() { }
-}
-
-private object MaterialSnippet8 {
- @Composable
- fun MyTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- content: @Composable () -> Unit
- ) {
- MaterialTheme(
- colors = if (darkTheme) DarkColors else LightColors,
- /*...*/
- content = content
- )
- }
-}
-
-private object MaterialSnippet9 {
- @Composable
- fun MaterialTheming() {
- val isLightTheme = MaterialTheme.colors.isLight
- Icon(
- painterResource(
- id = if (isLightTheme) {
- R.drawable.ic_sun_24dp
- } else {
- R.drawable.ic_moon_24dp
- }
- ),
- contentDescription = "Theme"
- )
- }
-}
-
-private object MaterialSnippet10 {
- @Composable
- fun MaterialTheming() {
- Surface(
- elevation = 2.dp,
- color = MaterialTheme.colors.surface, // color will be adjusted for elevation
- /*...*/
- ) { /*...*/ }
- }
-}
-
-private object MaterialSnippet11 {
- @Composable
- fun MaterialTheming() {
- // Elevation overlays
- // Implemented in Surface (and any components that use it)
- val color = MaterialTheme.colors.surface
- val elevation = 4.dp
- val overlaidColor = LocalElevationOverlay.current?.apply(
- color, elevation
- )
- }
-}
-
-private object MaterialSnippet12 {
- @Composable
- fun MaterialTheming() {
- MyTheme {
- CompositionLocalProvider(LocalElevationOverlay provides null) {
- // Content without elevation overlays
- }
- }
- }
-}
-
-private object MaterialSnippet13 {
- @Composable
- fun MaterialTheming() {
- Surface(
- // Switches between primary in light theme and surface in dark theme
- color = MaterialTheme.colors.primarySurface,
- /*...*/
- ) { /*...*/ }
- }
-}
-
-private object MaterialSnippet14 {
- @Composable
- fun MaterialTheming() {
- val Rubik = FontFamily(
- Font(R.font.rubik_regular),
- Font(R.font.rubik_medium, FontWeight.W500),
- Font(R.font.rubik_bold, FontWeight.Bold)
- )
-
- val MyTypography = Typography(
- h1 = TextStyle(
- fontFamily = Rubik,
- fontWeight = FontWeight.W300,
- fontSize = 96.sp
- ),
- body1 = TextStyle(
- fontFamily = Rubik,
- fontWeight = FontWeight.W600,
- fontSize = 16.sp
- )
- /*...*/
- )
- MaterialTheme(typography = MyTypography, /*...*/)
- }
- @Composable private fun MaterialTheme(typography: Typography) { }
-}
-
-private object MaterialSnippet15 {
- @Composable
- fun MaterialTheming() {
- val typography = Typography(defaultFontFamily = Rubik)
- MaterialTheme(typography = typography, /*...*/)
- }
- @Composable private fun MaterialTheme(typography: Typography) { }
-}
-
-private object MaterialSnippet16 {
- @Composable
- fun MaterialTheming() {
- Text(
- text = "Subtitle2 styled",
- style = MaterialTheme.typography.subtitle2
- )
- }
-}
-
-private object MaterialSnippet17 {
- @Composable
- fun MaterialTheming() {
- val Shapes = Shapes(
- small = RoundedCornerShape(percent = 50),
- medium = RoundedCornerShape(0f),
- large = CutCornerShape(
- topStart = 16.dp,
- topEnd = 0.dp,
- bottomEnd = 0.dp,
- bottomStart = 16.dp
- )
- )
-
- MaterialTheme(shapes = Shapes, /*...*/)
- }
- @Composable private fun MaterialTheme(shapes: Shapes) { }
-}
-
-private object MaterialSnippet18 {
- @Composable
- fun MaterialTheming() {
- Surface(
- shape = MaterialTheme.shapes.medium, /*...*/
- ) {
- /*...*/
- }
- }
-}
-
-private object MaterialSnippet19 {
- @Composable
- fun MyButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- content: @Composable RowScope.() -> Unit
- ) {
- Button(
- colors = ButtonDefaults.buttonColors(
- backgroundColor = MaterialTheme.colors.secondary
- ),
- onClick = onClick,
- modifier = modifier,
- content = content
- )
- }
-}
-
-private object MaterialSnippet20 {
- @Composable
- fun DetailsScreen(/* ... */) {
- PinkTheme {
- // other content
- RelatedSection()
- }
- }
-
- @Composable
- fun RelatedSection(/* ... */) {
- BlueTheme {
- // content
- }
- }
-}
-
-private object MaterialSnippet21 {
- @Composable
- fun MaterialTheming() {
- Button(
- onClick = { /* ... */ },
- enabled = true,
- // Custom colors for different states
- colors = ButtonDefaults.buttonColors(
- backgroundColor = MaterialTheme.colors.secondary,
- disabledBackgroundColor = MaterialTheme.colors.onBackground
- .copy(alpha = 0.2f)
- .compositeOver(MaterialTheme.colors.background)
- // Also contentColor and disabledContentColor
- ),
- // Custom elevation for different states
- elevation = ButtonDefaults.elevation(
- defaultElevation = 8.dp,
- disabledElevation = 2.dp,
- // Also pressedElevation
- )
- ) { /* ... */ }
- }
-}
-
-private object MaterialSnippet22 {
- @Composable
- fun MyApp() {
- MaterialTheme {
- CompositionLocalProvider(
- LocalRippleTheme provides SecondaryRippleTheme
- ) {
- // App content
- }
- }
- }
-
- @Immutable
- private object SecondaryRippleTheme : RippleTheme {
- @Composable
- override fun defaultColor() = RippleTheme.defaultRippleColor(
- contentColor = MaterialTheme.colors.secondary,
- lightTheme = MaterialTheme.colors.isLight
- )
-
- @Composable
- override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
- contentColor = MaterialTheme.colors.secondary,
- lightTheme = MaterialTheme.colors.isLight
- )
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private val Yellow500 = Color(0xffffeb46)
-private val Yellow400 = Color(0xffffeb46)
-private val Blue700 = Color(0xffffeb46)
-
-private const val darkTheme = true
-private val DarkColors = darkColors()
-private val LightColors = lightColors()
-
-@Composable private fun MyTheme(content: @Composable () -> Unit) {}
-@Composable private fun PinkTheme(content: @Composable () -> Unit) {}
-@Composable private fun BlueTheme(content: @Composable () -> Unit) {}
-
-@Suppress("ClassName")
-internal object R {
- object drawable {
- const val ic_sun_24dp = 1
- const val ic_moon_24dp = 1
- }
- object font {
- const val rubik_regular = 1
- const val rubik_medium = 1
- const val rubik_bold = 1
- const val karla_regular = 1
- const val karla_bold = 1
- }
-}
-
-private val Rubik = FontFamily()
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material3.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material3.kt
deleted file mode 100644
index d10cdb2..0000000
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Material3.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-// ktlint-disable indent https://github.com/pinterest/ktlint/issues/967
-/*
- * 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.
- */
-
-// Ignore lint warnings in documentation snippets
-@file:Suppress(
- "unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE", "RemoveEmptyParenthesesFromLambdaCall"
-)
-
-package androidx.compose.integration.docs.theming
-
-import android.annotation.SuppressLint
-import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.Typography
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-/**
- * This file lets DevRel track changes to snippets present in
- * https://developer.android.com/jetpack/compose/themes/material#material3
- *
- * No action required if it's modified.
- */
-
-private object Material3Snippet1 {
- /* Can't be compiled. See snippet below for changes.
- MaterialTheme(
- colorScheme = …,
- typography = …
- // Updates to shapes coming soon
- ) {
- // M3 app content
- }
- */
- @Composable
- fun MaterialTheming() {
- MaterialTheme(
- colorScheme = MaterialTheme.colorScheme,
- typography = MaterialTheme.typography
- ) { }
- }
-}
-
-private object Material3Snippet2 {
- private val Blue40 = Color(0xff1e40ff)
- private val DarkBlue40 = Color(0xff3e41f4)
- private val Yellow40 = Color(0xff7d5700)
- // Remaining colors from tonal palettes
-
- private val LightColorScheme = lightColorScheme(
- primary = Blue40,
- secondary = DarkBlue40,
- tertiary = Yellow40,
- // error, primaryContainer, onSecondary, etc.
- )
- private val DarkColorScheme = darkColorScheme(
- primary = Blue80,
- secondary = DarkBlue80,
- tertiary = Yellow80,
- // error, primaryContainer, onSecondary, etc.
- )
-}
-
-private object Material3Snippet3 {
- @Composable
- fun Material3Theming() {
- val darkTheme = isSystemInDarkTheme()
- MaterialTheme(
- colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
- ) {
- // M3 app content
- }
- }
-}
-
-private object Material3Snippet4 {
- @SuppressLint("NewApi")
- @Composable
- fun Material3Theming() {
- // Dynamic color is available on Android 12+
- val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
- val colorScheme = when {
- dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
- dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
- }
-}
-
-private object Material3Snippet5 {
- @Composable
- fun Material3Theming() {
- Text(
- text = "Hello M3 theming",
- color = MaterialTheme.colorScheme.tertiary
- )
- }
-}
-
-private object Material3Snippet6 {
- val KarlaFontFamily = FontFamily(
- Font(R.font.karla_regular),
- Font(R.font.karla_bold, FontWeight.Bold)
- )
-
- val AppTypography = Typography(
- bodyLarge = TextStyle(
- fontFamily = KarlaFontFamily,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.15.sp
- ),
- // titleMedium, labelSmall, etc.
- )
-}
-
-private object Material3Snippet7 {
- @Composable
- fun Material3Theming() {
- MaterialTheme(
- typography = AppTypography
- ) {
- // M3 app content
- }
- }
-}
-
-private object Material3Snippet8 {
- @Composable
- fun Material3Theming() {
- Text(
- text = "Hello M3 theming",
- style = MaterialTheme.typography.bodyLarge
- )
- }
-}
-
-private object Material3Snippet9 {
- @Composable
- fun Material3Theming() {
- Surface(
- tonalElevation = 16.dp,
- shadowElevation = 16.dp
- ) {
- // Surface content
- }
- }
-}
-
-/*
-Fakes needed for snippets to build:
- */
-
-private val Blue80 = Color(0xff1e40ff)
-private val DarkBlue80 = Color(0xff3e41f4)
-private val Yellow80 = Color(0xff7d5700)
-
-private val DarkColorScheme = darkColorScheme()
-private val LightColorScheme = lightColorScheme()
-
-private const val darkTheme = true
-
-private val AppTypography = Typography()
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 5cb3474..7739110 100644
--- a/compose/integration-tests/macrobenchmark/build.gradle
+++ b/compose/integration-tests/macrobenchmark/build.gradle
@@ -14,11 +14,9 @@
* limitations under the License.
*/
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -27,25 +25,22 @@
minSdkVersion 23
}
namespace "androidx.compose.integration.macrobenchmark"
+ targetProjectPath = ":compose:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":compose:integration-tests:macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":compose:integration-tests:macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/AndroidViewListScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewPagerBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/AndroidViewPagerBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/AndroidViewPagerBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/AndroidViewPagerBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/DifferentTypesListScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FullyDrawnStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FullyDrawnStartupBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FullyDrawnStartupBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FullyDrawnStartupBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/GridBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/GridBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/GridBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/GridBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/IoSettingsStartupBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/NestedListsScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerAsCarouselBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/PagerBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewAsCarouselBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/RecyclerViewListScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListBaselineProfile.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/SmallListBaselineProfile.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListBaselineProfile.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/SmallListBaselineProfile.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
similarity index 100%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
rename to compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index c6afe8e..6fc989b 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -29,8 +29,8 @@
android {
defaultConfig {
applicationId "androidx.compose.material.catalog"
- versionCode 2100
- versionName "2.1.0"
+ versionCode 2200
+ versionName "2.2.0"
}
buildTypes {
release {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 1f5b379..b6e6d45 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -758,16 +758,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index b2471cb..a2231ee 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -1145,16 +1145,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 1f5b379..b6e6d45 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -758,16 +758,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 1d6480b..aef2f9c 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -101,6 +101,7 @@
import androidx.compose.material3.samples.PinnedTopAppBar
import androidx.compose.material3.samples.PlainTooltipSample
import androidx.compose.material3.samples.PlainTooltipWithManualInvocationSample
+import androidx.compose.material3.samples.PrimaryTabs
import androidx.compose.material3.samples.RadioButtonSample
import androidx.compose.material3.samples.RadioGroupSample
import androidx.compose.material3.samples.RangeSliderSample
@@ -113,8 +114,11 @@
import androidx.compose.material3.samples.ScaffoldWithMultilineSnackbar
import androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
import androidx.compose.material3.samples.ScrollingFancyIndicatorContainerTabs
+import androidx.compose.material3.samples.ScrollingPrimaryTabs
+import androidx.compose.material3.samples.ScrollingSecondaryTabs
import androidx.compose.material3.samples.ScrollingTextTabs
import androidx.compose.material3.samples.SearchBarSample
+import androidx.compose.material3.samples.SecondaryTabs
import androidx.compose.material3.samples.SimpleBottomAppBar
import androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample
import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
@@ -894,6 +898,20 @@
private const val TabsExampleSourceUrl = "$SampleSourceUrl/TabSamples.kt"
val TabsExamples = listOf(
Example(
+ name = ::PrimaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ PrimaryTabs()
+ },
+ Example(
+ name = ::SecondaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ SecondaryTabs()
+ },
+ Example(
name = ::TextTabs.name,
description = TabsExampleDescription,
sourceUrl = TabsExampleSourceUrl
@@ -922,6 +940,20 @@
LeadingIconTabs()
},
Example(
+ name = ::ScrollingPrimaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ ScrollingPrimaryTabs()
+ },
+ Example(
+ name = ::ScrollingSecondaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ ScrollingSecondaryTabs()
+ },
+ Example(
name = ::ScrollingTextTabs.name,
description = TabsExampleDescription,
sourceUrl = TabsExampleSourceUrl
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
index bbeb8b3..c19a692 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
@@ -19,6 +19,7 @@
import androidx.annotation.Sampled
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.BorderStroke
@@ -29,24 +30,25 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Icon
+import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRow
-import androidx.compose.material3.LeadingIconTab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -59,6 +61,58 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+@Composable
+fun PrimaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
+ Column {
+ TabRow(selectedTabIndex = state, indicator = @Composable { tabPositions ->
+ if (state < tabPositions.size) {
+ val width by animateDpAsState(targetValue = tabPositions[state].contentWidth)
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[state]),
+ width = width
+ )
+ }
+ }) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Primary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
+fun SecondaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
+ Column {
+ TabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Secondary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
@Preview
@Sampled
@Composable
@@ -160,6 +214,80 @@
}
@Composable
+fun ScrollingPrimaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf(
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
+ )
+ Column {
+ ScrollableTabRow(selectedTabIndex = state, indicator = @Composable { tabPositions ->
+ if (state < tabPositions.size) {
+ val width by animateDpAsState(targetValue = tabPositions[state].contentWidth)
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[state]),
+ width = width
+ )
+ }
+ }) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(title) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Scrolling primary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
+fun ScrollingSecondaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf(
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
+ )
+ Column {
+ ScrollableTabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(title) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Scrolling secondary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
fun ScrollingTextTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf(
@@ -201,7 +329,11 @@
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
- FancyTab(title = title, onClick = { state = index }, selected = (index == state))
+ FancyTab(
+ title = title,
+ onClick = { state = index },
+ selected = (index == state)
+ )
}
}
Text(
@@ -334,7 +466,8 @@
.align(Alignment.CenterHorizontally)
.background(
color = if (selected) MaterialTheme.colorScheme.primary
- else MaterialTheme.colorScheme.background)
+ else MaterialTheme.colorScheme.background
+ )
)
Text(
text = title,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
index cced64f..318b5c0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
@@ -59,7 +59,7 @@
val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
@Test
- fun lightTheme() {
+ fun lightTheme_primary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -67,7 +67,7 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultTabs(interactionSource)
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -75,12 +75,12 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = null,
- goldenIdentifier = "tabs_lightTheme"
+ goldenIdentifier = "tabs_lightTheme_primary"
)
}
@Test
- fun lightTheme_pressed() {
+ fun lightTheme_secondary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -88,7 +88,28 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultTabs(interactionSource)
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun lightTheme_primary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -96,54 +117,12 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = PressInteraction.Press(Offset(10f, 10f)),
- goldenIdentifier = "tabs_lightTheme_pressed"
+ goldenIdentifier = "tabs_lightTheme_primary_pressed"
)
}
@Test
- fun darkTheme() {
- val interactionSource = MutableInteractionSource()
-
- var scope: CoroutineScope? = null
-
- composeTestRule.setContent {
- scope = rememberCoroutineScope()
- MaterialTheme(darkColorScheme()) {
- DefaultTabs(interactionSource)
- }
- }
-
- assertTabsMatch(
- scope = scope!!,
- interactionSource = interactionSource,
- interaction = null,
- goldenIdentifier = "tabs_darkTheme"
- )
- }
-
- @Test
- fun darkTheme_pressed() {
- val interactionSource = MutableInteractionSource()
-
- var scope: CoroutineScope? = null
-
- composeTestRule.setContent {
- scope = rememberCoroutineScope()
- MaterialTheme(darkColorScheme()) {
- DefaultTabs(interactionSource)
- }
- }
-
- assertTabsMatch(
- scope = scope!!,
- interactionSource = interactionSource,
- interaction = PressInteraction.Press(Offset(10f, 10f)),
- goldenIdentifier = "tabs_darkTheme_pressed"
- )
- }
-
- @Test
- fun leadingIconTabs_lightTheme() {
+ fun lightTheme_secondary_pressed() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -151,20 +130,20 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultLeadingIconTabs(interactionSource)
+ DefaultSecondaryTabs(interactionSource)
}
}
assertTabsMatch(
scope = scope!!,
interactionSource = interactionSource,
- interaction = null,
- goldenIdentifier = "leadingIconTabs_lightTheme"
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_lightTheme_secondary_pressed"
)
}
@Test
- fun leadingIconTabs_darkTheme() {
+ fun darkTheme_primary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -172,7 +151,7 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(darkColorScheme()) {
- DefaultLeadingIconTabs(interactionSource)
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -180,7 +159,342 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = null,
- goldenIdentifier = "leadingIconTabs_darkTheme"
+ goldenIdentifier = "tabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun darkTheme_primary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_darkTheme_primary_pressed"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_darkTheme_secondary_pressed"
+ )
+ }
+
+ @Test
+ fun customTabs_lightTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ CustomPrimaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_lightTheme_primary"
+ )
+ }
+
+ @Test
+ fun customTabs_lightTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ CustomSecondaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun customTabs_darkTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ CustomPrimaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun customTabs_darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ CustomSecondaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_lightTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_lightTheme_primary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_lightTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultSecondaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_darkTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun lightTheme_primary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_primary_scrollable"
+ )
+ }
+
+ @Test
+ fun lightTheme_secondary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultSecondaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_secondary_scrollable"
+ )
+ }
+
+ @Test
+ fun darkTheme_primary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_primary_scrollable"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_secondary_scrollable"
)
}
@@ -213,23 +527,66 @@
}
// Capture and compare screenshots
- composeTestRule.onNodeWithTag(Tag)
+ composeTestRule.onNodeWithTag(TAG)
.captureToImage()
.assertAgainstGolden(screenshotRule, goldenIdentifier)
}
}
/**
- * Default colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ * Default primary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
* visual state.
*/
@Composable
-private fun DefaultTabs(
+private fun DefaultPrimaryTabs(
interactionSource: MutableInteractionSource
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0) {
Tab(
selected = true,
@@ -252,7 +609,7 @@
}
/**
- * Custom colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ * Custom primary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
* visual state.
@@ -261,17 +618,75 @@
* @param unselectedContentColor the content color for an unselected [Tab] (second and third tabs)
*/
@Composable
-private fun CustomTabs(
+private fun CustomPrimaryTabs(
interactionSource: MutableInteractionSource,
containerColor: Color,
selectedContentColor: Color,
unselectedContentColor: Color
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0,
containerColor = containerColor,
indicator = @Composable { tabPositions ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth,
+ color = selectedContentColor
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor,
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor
+ )
+ }
+ }
+}
+
+/**
+ * Custom secondary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ * @param containerColor the containerColor of the [TabRow]
+ * @param selectedContentColor the content color for a selected [Tab] (first tab)
+ * @param unselectedContentColor the content color for an unselected [Tab] (second and third tabs)
+ */
+@Composable
+private fun CustomSecondaryTabs(
+ interactionSource: MutableInteractionSource,
+ containerColor: Color,
+ selectedContentColor: Color,
+ unselectedContentColor: Color
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0,
+ containerColor = containerColor,
+ indicator = @Composable { tabPositions ->
+ TabRowDefaults.SecondaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
color = selectedContentColor
)
@@ -303,17 +718,64 @@
}
/**
- * Default colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * Default primary colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
* and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
* visual state.
*/
@Composable
-private fun DefaultLeadingIconTabs(
+private fun DefaultPrimaryLeadingIconTabs(
interactionSource: MutableInteractionSource
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ LeadingIconTab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+ interactionSource = interactionSource
+ )
+ LeadingIconTab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") }
+ )
+ LeadingIconTab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryLeadingIconTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0) {
LeadingIconTab(
selected = true,
@@ -338,4 +800,79 @@
}
}
-private const val Tag = "Tab"
+/**
+ * Default primary colored [ScrollableTabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultPrimaryScrollableTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ ScrollableTabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [ScrollableTabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryScrollableTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ ScrollableTabRow(selectedTabIndex = 0) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+private const val TAG = "Tab"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
index 4ff880a..7b9ed6e 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
@@ -27,7 +27,7 @@
import androidx.compose.material3.samples.LeadingIconTabs
import androidx.compose.material3.samples.ScrollingTextTabs
import androidx.compose.material3.samples.TextTabs
-import androidx.compose.material3.tokens.PrimaryNavigationTabTokens
+import androidx.compose.material3.tokens.DividerTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -326,9 +326,9 @@
rule.onNodeWithTag("divider", true)
.assertPositionInRootIsEqualTo(
expectedLeft = 0.dp,
- expectedTop = tabRowBounds.height - PrimaryNavigationTabTokens.DividerHeight
+ expectedTop = tabRowBounds.height - DividerTokens.Thickness
)
- .assertHeightIsEqualTo(PrimaryNavigationTabTokens.DividerHeight)
+ .assertHeightIsEqualTo(DividerTokens.Thickness)
}
@Test
@@ -435,7 +435,7 @@
}
@Test
- fun LeadingIconTab_textAndIconPosition() {
+ fun leadingIconTab_textAndIconPosition() {
rule.setMaterialContent(lightColorScheme()) {
Box {
TabRow(
@@ -564,9 +564,9 @@
rule.onNodeWithTag("divider", true)
.assertPositionInRootIsEqualTo(
expectedLeft = 0.dp,
- expectedTop = tabRowBounds.height - PrimaryNavigationTabTokens.DividerHeight,
+ expectedTop = tabRowBounds.height - DividerTokens.Thickness,
)
- .assertHeightIsEqualTo(PrimaryNavigationTabTokens.DividerHeight)
+ .assertHeightIsEqualTo(DividerTokens.Thickness)
}
@Test
@@ -593,8 +593,10 @@
TextTabs()
}
+ val nodes = rule.onAllNodes(isSelectable())
+
// Only the first tab should be selected
- rule.onAllNodes(isSelectable())
+ nodes
.assertCountEquals(3)
.apply {
get(0).assertIsSelected()
@@ -603,10 +605,10 @@
}
// Click the last tab
- rule.onAllNodes(isSelectable())[2].performClick()
+ nodes[2].performClick()
// Now only the last tab should be selected
- rule.onAllNodes(isSelectable())
+ nodes
.assertCountEquals(3)
.apply {
get(0).assertIsNotSelected()
@@ -747,7 +749,7 @@
val titles = listOf("TAB 1", "TAB 2", "TAB 3 WITH LOTS OF TEXT")
val indicator = @Composable { tabPositions: List<TabPosition> ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier
.tabIndicatorOffset(tabPositions[state])
.testTag("indicator")
@@ -791,7 +793,7 @@
@Test
fun testInspectorValue() {
- val pos = TabPosition(10.0.dp, 200.0.dp)
+ val pos = TabPosition(10.0.dp, 200.0.dp, 0.dp)
rule.setContent {
val modifier = Modifier.tabIndicatorOffset(pos) as InspectableValue
assertThat(modifier.nameFallback).isEqualTo("tabIndicatorOffset")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
index d726302..31ab35e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
@@ -315,7 +315,11 @@
) { text() }
}
if (icon != null) {
- Box(Modifier.layoutId("icon")) { icon() }
+ Box(
+ Modifier
+ .layoutId("icon")
+ .padding(horizontal = HorizontalTextPadding)
+ ) { icon() }
}
}
) { measurables, constraints ->
@@ -430,7 +434,7 @@
private const val TabFadeOutAnimationDuration = 100
// The horizontal padding on the left and right of text
-private val HorizontalTextPadding = 16.dp
+internal val HorizontalTextPadding = 16.dp
// Distance from the top of the indicator to the text baseline when there is one line of text and an
// icon
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index 6480116..82d1d85 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -24,9 +24,11 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
@@ -43,6 +45,8 @@
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
@@ -52,8 +56,9 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-// TODO: Provide M3 tab row asset and docs when available.
/**
+ * <a href="https://m3.material.io/components/tabs/overview" class="external" target="_blank">Material Design tabs</a>
+ *
* Material Design fixed tabs.
*
* Fixed tabs display all tabs in a set simultaneously. They are best for switching between related
@@ -113,7 +118,7 @@
* matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabRowDefaults.Indicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
+ * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
* animate its position. Note that this indicator will be forced to fill up the entire tab row, so
* you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual drawn
* indicator inside this space, and provide an offset from the start.
@@ -130,7 +135,7 @@
contentColor: Color = TabRowDefaults.contentColor,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
if (selectedTabIndex < tabPositions.size) {
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
}
@@ -140,6 +145,18 @@
},
tabs: @Composable () -> Unit
) {
+ TabRowImpl(modifier, containerColor, contentColor, indicator, divider, tabs)
+}
+
+@Composable
+private fun TabRowImpl(
+ modifier: Modifier,
+ containerColor: Color,
+ contentColor: Color,
+ indicator: @Composable (tabPositions: List<TabPosition>) -> Unit,
+ divider: @Composable () -> Unit,
+ tabs: @Composable () -> Unit
+) {
Surface(
modifier = modifier.selectableGroup(),
color = containerColor,
@@ -169,7 +186,10 @@
}
val tabPositions = List(tabCount) { index ->
- TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
+ var contentWidth =
+ minOf(tabMeasurables[index].maxIntrinsicWidth(tabRowHeight), tabWidth).toDp()
+ contentWidth -= HorizontalTextPadding * 2
+ TabPosition(tabWidth.toDp() * index, tabWidth.toDp(), contentWidth)
}
layout(tabRowWidth, tabRowHeight) {
@@ -192,8 +212,9 @@
}
}
-// TODO: Provide M3 tab row asset and docs when available.
/**
+ * <a href="https://m3.material.io/components/tabs/overview" class="external" target="_blank">Material Design tabs</a>
+ *
* Material Design scrollable tabs.
*
* When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text
@@ -215,7 +236,7 @@
* and the tabs inside the row. This padding helps inform the user that this tab row can be
* scrolled, unlike a [TabRow].
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabRowDefaults.Indicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
+ * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
* animate its position. Note that this indicator will be forced to fill up the entire tab row, so
* you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual drawn
* indicator inside this space, and provide an offset from the start.
@@ -232,7 +253,7 @@
contentColor: Color = TabRowDefaults.contentColor,
edgePadding: Dp = ScrollableTabRowPadding,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
@@ -276,8 +297,20 @@
minHeight = layoutHeight,
maxHeight = layoutHeight,
)
- val tabPlaceables = tabMeasurables
- .map { it.measure(tabConstraints) }
+
+ val tabPlaceables = mutableListOf<Placeable>()
+ val tabContentWidths = mutableListOf<Dp>()
+ tabMeasurables.forEach {
+ val placeable = it.measure(tabConstraints)
+ var contentWidth =
+ minOf(
+ it.maxIntrinsicWidth(placeable.height),
+ placeable.width
+ ).toDp()
+ contentWidth -= HorizontalTextPadding * 2
+ tabPlaceables.add(placeable)
+ tabContentWidths.add(contentWidth)
+ }
val layoutWidth = tabPlaceables.fold(initial = padding * 2) { curr, measurable ->
curr + measurable.width
@@ -288,10 +321,16 @@
// Place the tabs
val tabPositions = mutableListOf<TabPosition>()
var left = padding
- tabPlaceables.forEach {
- it.placeRelative(left, 0)
- tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
- left += it.width
+ tabPlaceables.forEachIndexed { index, placeable ->
+ placeable.placeRelative(left, 0)
+ tabPositions.add(
+ TabPosition(
+ left = left.toDp(),
+ width = placeable.width.toDp(),
+ contentWidth = tabContentWidths[index]
+ )
+ )
+ left += placeable.width
}
// The divider is measured with its own height, and width equal to the total width
@@ -333,9 +372,11 @@
* @property left the left edge's x position from the start of the [TabRow]
* @property right the right edge's x position from the start of the [TabRow]
* @property width the width of this tab
+ * @property contentWidth the content width of this tab
*/
@Immutable
-class TabPosition internal constructor(val left: Dp, val width: Dp) {
+class TabPosition internal constructor(val left: Dp, val width: Dp, val contentWidth: Dp) {
+
val right: Dp get() = left + width
override fun equals(other: Any?): Boolean {
@@ -344,6 +385,7 @@
if (left != other.left) return false
if (width != other.width) return false
+ if (contentWidth != other.contentWidth) return false
return true
}
@@ -351,11 +393,12 @@
override fun hashCode(): Int {
var result = left.hashCode()
result = 31 * result + width.hashCode()
+ result = 31 * result + contentWidth.hashCode()
return result
}
override fun toString(): String {
- return "TabPosition(left=$left, right=$right, width=$width)"
+ return "TabPosition(left=$left, right=$right, width=$width, contentWidth=$contentWidth)"
}
}
@@ -364,12 +407,14 @@
*/
object TabRowDefaults {
/** Default container color of a tab row. */
- val containerColor: Color @Composable get() =
- PrimaryNavigationTabTokens.ContainerColor.toColor()
+ val containerColor: Color
+ @Composable get() =
+ PrimaryNavigationTabTokens.ContainerColor.toColor()
/** Default content color of a tab row. */
- val contentColor: Color @Composable get() =
- PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
+ val contentColor: Color
+ @Composable get() =
+ PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
/**
* Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
@@ -380,6 +425,12 @@
* @param color color of the indicator
*/
@Composable
+ @Deprecated(
+ message = "Use SecondaryIndicator instead.",
+ replaceWith = ReplaceWith(
+ "SecondaryIndicator(modifier, height, color)"
+ )
+ )
fun Indicator(
modifier: Modifier = Modifier,
height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
@@ -395,6 +446,54 @@
}
/**
+ * Primary indicator, which will be positioned at the bottom of the [TabRow], on top of the
+ * divider.
+ *
+ * @param modifier modifier for the indicator's layout
+ * @param width width of the indicator
+ * @param height height of the indicator
+ * @param color color of the indicator
+ * @param shape shape of the indicator
+ */
+ @Composable
+ fun PrimaryIndicator(
+ modifier: Modifier = Modifier,
+ width: Dp = 0.dp,
+ height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
+ color: Color = PrimaryNavigationTabTokens.ActiveIndicatorColor.toColor(),
+ shape: Shape = PrimaryNavigationTabTokens.ActiveIndicatorShape
+ ) {
+ Spacer(
+ modifier
+ .requiredSize(width, height)
+ .background(color = color, shape = shape)
+ )
+ }
+
+ /**
+ * Secondary indicator, which will be positioned at the bottom of the [TabRow], on top of the
+ * divider.
+ *
+ * @param modifier modifier for the indicator's layout
+ * @param height height of the indicator
+ * @param color color of the indicator
+ */
+ @Composable
+ fun SecondaryIndicator(
+ modifier: Modifier = Modifier,
+ height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
+ color: Color =
+ MaterialTheme.colorScheme.fromToken(PrimaryNavigationTabTokens.ActiveIndicatorColor)
+ ) {
+ Box(
+ modifier
+ .fillMaxWidth()
+ .height(height)
+ .background(color = color)
+ )
+ }
+
+ /**
* [Modifier] that takes up all the available width inside the [TabRow], and then animates
* the offset of the indicator it is applied to, depending on the [currentTabPosition].
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
index 3fd0e91..4622e75 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_103
+
+// VERSION: v0_162
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -29,8 +30,6 @@
val ContainerElevation = ElevationTokens.Level0
val ContainerHeight = 48.0.dp
val ContainerShape = ShapeKeyTokens.CornerNone
- val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
- val DividerHeight = 1.0.dp
val ActiveFocusIconColor = ColorSchemeKeyTokens.Primary
val ActiveHoverIconColor = ColorSchemeKeyTokens.Primary
val ActiveIconColor = ColorSchemeKeyTokens.Primary
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt
new file mode 100644
index 0000000..6d34a48
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+// VERSION: v0_162
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SecondaryNavigationTabTokens {
+ val ActiveLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ContainerColor = ColorSchemeKeyTokens.Surface
+ val ContainerElevation = ElevationTokens.Level0
+ val ContainerHeight = 48.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerNone
+ val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
+ val DividerHeight = 1.0.dp
+ val FocusLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val HoverLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val InactiveLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val LabelTextFont = TypographyKeyTokens.TitleSmall
+ val PressedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ActiveIconColor = ColorSchemeKeyTokens.OnSurface
+ val FocusIconColor = ColorSchemeKeyTokens.OnSurface
+ val HoverIconColor = ColorSchemeKeyTokens.OnSurface
+ val IconSize = 24.0.dp
+ val InactiveIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val PressedIconColor = ColorSchemeKeyTokens.OnSurface
+}
\ No newline at end of file
diff --git a/compose/runtime/OWNERS b/compose/runtime/OWNERS
index a9c5fa6..1d23419 100644
--- a/compose/runtime/OWNERS
+++ b/compose/runtime/OWNERS
@@ -3,6 +3,7 @@
chuckj@google.com
lelandr@google.com
anbailey@google.com
+ashikov@google.com
# Per-file for Playground infra
per-file settings.gradle = yboyar@google.com, dustinlam@google.com, rahulrav@google.com
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 7aade6c..06356b3 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -673,35 +673,39 @@
override fun prepareCompose(block: () -> Unit) = composer.prepareCompose(block)
- private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) {
- var invalidated: HashSet<RecomposeScopeImpl>? = null
-
- fun invalidate(value: Any) {
- observations.forEachScopeOf(value) { scope ->
- if (
- !observationsProcessed.remove(value, scope) &&
- scope.invalidateForResult(value) != InvalidationResult.IGNORED
- ) {
- if (scope.isConditional && !forgetConditionalScopes) {
- conditionallyInvalidatedScopes.add(scope)
- } else {
- val set = invalidated
- ?: HashSet<RecomposeScopeImpl>().also {
- invalidated = it
- }
- set.add(scope)
- }
+ private fun HashSet<RecomposeScopeImpl>?.addPendingInvalidationsLocked(
+ value: Any,
+ forgetConditionalScopes: Boolean
+ ): HashSet<RecomposeScopeImpl>? {
+ var set = this
+ observations.forEachScopeOf(value) { scope ->
+ if (
+ !observationsProcessed.remove(value, scope) &&
+ scope.invalidateForResult(value) != InvalidationResult.IGNORED
+ ) {
+ if (scope.isConditional && !forgetConditionalScopes) {
+ conditionallyInvalidatedScopes.add(scope)
+ } else {
+ if (set == null) set = HashSet()
+ set?.add(scope)
}
}
}
+ return set
+ }
+
+ private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) {
+ var invalidated: HashSet<RecomposeScopeImpl>? = null
values.fastForEach { value ->
if (value is RecomposeScopeImpl) {
value.invalidateForResult(null)
} else {
- invalidate(value)
+ invalidated =
+ invalidated.addPendingInvalidationsLocked(value, forgetConditionalScopes)
derivedStates.forEachScopeOf(value) {
- invalidate(it)
+ invalidated =
+ invalidated.addPendingInvalidationsLocked(it, forgetConditionalScopes)
}
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
index 1254875..9efcc11 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
@@ -22,6 +22,7 @@
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CancellationException
/**
* Schedule [effect] to run when the current composition completes successfully and applies
@@ -284,17 +285,18 @@
private var job: Job? = null
override fun onRemembered() {
+ // This should never happen but is left here for safety
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
- job?.cancel()
+ job?.cancel(LeftCompositionCancellationException())
job = null
}
override fun onAbandoned() {
- job?.cancel()
+ job?.cancel(LeftCompositionCancellationException())
job = null
}
}
@@ -384,6 +386,12 @@
remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) }
}
+private class LeftCompositionCancellationException : CancellationException(
+ "The coroutine scope left the composition"
+) {
+ override fun fillInStackTrace(): Throwable = this
+}
+
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when
@@ -416,11 +424,11 @@
}
override fun onForgotten() {
- coroutineScope.cancel()
+ coroutineScope.cancel(LeftCompositionCancellationException())
}
override fun onAbandoned() {
- coroutineScope.cancel()
+ coroutineScope.cancel(LeftCompositionCancellationException())
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index cc4584c..faf8739 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -612,7 +612,12 @@
// Perform apply changes
try {
- toComplete += toApply
+ // We could do toComplete += toApply but doing it like below
+ // avoids unncessary allocations since toApply is a mutable list
+ // toComplete += toApply
+ toApply.fastForEach { composition ->
+ toComplete.add(composition)
+ }
toApply.fastForEach { composition ->
composition.applyChanges()
}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 18cdc50..55136ac 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -401,7 +401,7 @@
}
public fun interface ColorProducer {
- method public long produce();
+ method public operator long invoke();
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class FilterQuality {
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 75d0b80..6ee1568 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -401,7 +401,7 @@
}
public fun interface ColorProducer {
- method public long produce();
+ method public operator long invoke();
}
@kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGraphicsApi {
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index 08b7e05..785b8d7 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -432,7 +432,7 @@
}
public fun interface ColorProducer {
- method public long produce();
+ method public operator long invoke();
}
public final class DegreesKt {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index a17406a..bf442bc 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -668,5 +668,5 @@
/**
* Return the color
*/
- fun produce(): Color
+ operator fun invoke(): Color
}
\ No newline at end of file
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index 5e5e30e..a4ae8f0 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -21,6 +21,7 @@
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
+ method public static inline <T> T? fastLastOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/api/public_plus_experimental_current.txt b/compose/ui/ui-util/api/public_plus_experimental_current.txt
index 5e5e30e..a4ae8f0 100644
--- a/compose/ui/ui-util/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-util/api/public_plus_experimental_current.txt
@@ -21,6 +21,7 @@
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
+ method public static inline <T> T? fastLastOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 5e5e30e..a4ae8f0 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -21,6 +21,7 @@
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
+ method public static inline <T> T? fastLastOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index 25b8c3c..217cc13 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -203,3 +203,21 @@
}
return destination
}
+
+/**
+ * Returns the last element matching the given [predicate], or `null` if no such element was found.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+inline fun <T> List<T>.fastLastOrNull(predicate: (T) -> Boolean): T? {
+ contract { callsInPlace(predicate) }
+ for (index in indices.reversed()) {
+ val item = get(index)
+ if (predicate(item)) return item
+ }
+ return null
+}
diff --git a/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt b/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
index ec25815..d59b8b1 100644
--- a/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
+++ b/compose/ui/ui-util/src/test/kotlin/androidx/compose/ui/util/ListUtilsTest.kt
@@ -115,4 +115,16 @@
val list = listOf(0, -1, -500, 1)
assertEquals(1, list.fastFirstOrNull { it > 0 })
}
-}
\ No newline at end of file
+
+ @Test
+ fun lastOrNullFound() {
+ val list = listOf(1, -1, -500, 2)
+ assertEquals(2, list.fastLastOrNull { it > 0 })
+ }
+
+ @Test
+ fun lastOrNullNotFound() {
+ val list = listOf(-1, -1, -500, -2)
+ assertNull(list.fastLastOrNull { it > 0 })
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
index e583a5a..af1dbfc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
@@ -280,7 +280,7 @@
else -> PointerType.Unknown
}
- val historical = mutableListOf<HistoricalChange>()
+ val historical = ArrayList<HistoricalChange>(motionEvent.historySize)
with(motionEvent) {
repeat(historySize) { pos ->
val x = getHistoricalX(index, pos)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
index 73a1193..e13bda9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
@@ -21,7 +21,6 @@
import android.view.MotionEvent.ACTION_SCROLL
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
internal actual typealias NativePointerButtons = Int
internal actual typealias NativePointerKeyboardModifiers = Int
@@ -92,22 +91,22 @@
null -> PointerEvent(changes, null)
this.motionEvent -> PointerEvent(changes, internalPointerEvent)
else -> {
- val map = mutableMapOf<PointerId, PointerInputChange>()
+ val map = LinkedHashMap<PointerId, PointerInputChange>(changes.size)
+ val pointerEventData = ArrayList<PointerInputEventData>(changes.size)
changes.fastForEach { change ->
map[change.id] = change
- }
- val pointerEventData = changes.fastMap {
- PointerInputEventData(
- it.id,
- it.uptimeMillis,
- it.position,
- it.position,
- it.pressed,
- it.pressure,
- it.type,
- this.internalPointerEvent?.issuesEnterExitEvent(it.id) == true
+ pointerEventData += PointerInputEventData(
+ change.id,
+ change.uptimeMillis,
+ change.position,
+ change.position,
+ change.pressed,
+ change.pressure,
+ change.type,
+ this.internalPointerEvent?.issuesEnterExitEvent(change.id) == true
)
}
+
val pointerInputEvent =
PointerInputEvent(motionEvent.eventTime, pointerEventData, motionEvent)
val event = InternalPointerEvent(map, pointerInputEvent)
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 da63070..087323c 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
@@ -138,6 +138,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.trace
import androidx.compose.ui.viewinterop.AndroidViewHolder
import androidx.core.view.AccessibilityDelegateCompat
@@ -1375,7 +1376,7 @@
// methods use semantics data, and because semantics coordinates are local to
// this view, the pointer _position_, not _positionOnScreen_, is the offset that
// needs to be cached.
- pointerInputEvent.pointers.lastOrNull { it.down }?.position?.let {
+ pointerInputEvent.pointers.fastLastOrNull { it.down }?.position?.let {
lastDownPointerPosition = it
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
index 65ea0ca..d838abf 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
@@ -57,6 +57,10 @@
private val invalidateNodes: () -> Unit = {
// Process all the invalidated FocusProperties nodes.
focusPropertiesNodes.forEach {
+ // We don't need to invalidate a focus properties node if it was scheduled for
+ // invalidation earlier in the composition but was then removed.
+ if (!it.node.isAttached) return@forEach
+
it.visitSelfAndChildren(Nodes.FocusTarget) { focusTarget ->
focusTargetNodes.add(focusTarget)
}
@@ -128,9 +132,8 @@
focusTargetNodes.clear()
focusTargetsWithInvalidatedFocusEvents.clear()
- // TODO(b/280941088): Re-enable after we find the root issue.
- // check(focusPropertiesNodes.isEmpty())
- // check(focusEventNodes.isEmpty())
- // check(focusTargetNodes.isEmpty())
+ check(focusPropertiesNodes.isEmpty()) { "Unprocessed FocusProperties nodes" }
+ check(focusEventNodes.isEmpty()) { "Unprocessed FocusEvent nodes" }
+ check(focusTargetNodes.isEmpty()) { "Unprocessed FocusTarget nodes" }
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index d8b0359..4c6b373 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -55,7 +55,7 @@
// TODO(lmr): we might want to add some safety wheels to prevent this from being called
// while one of the chains is being diffed / updated. Although that might only be
// necessary for visiting subtree.
- check(node.isAttached)
+ check(node.isAttached) { "visitAncestors called on an unattached node" }
var node: Modifier.Node? = if (includeSelf) node else node.parent
var layout: LayoutNode? = requireLayoutNode()
while (layout != null) {
@@ -75,7 +75,7 @@
@Suppress("unused")
internal fun DelegatableNode.nearestAncestor(mask: Int): Modifier.Node? {
- check(node.isAttached)
+ check(node.isAttached) { "nearestAncestor called on an unattached node" }
var node: Modifier.Node? = node.parent
var layout: LayoutNode? = requireLayoutNode()
while (layout != null) {
@@ -97,7 +97,7 @@
internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
// TODO(lmr): we might want to add some safety wheels to prevent this from being called
// while one of the chains is being diffed / updated.
- check(node.isAttached)
+ check(node.isAttached) { "visitSubtree called on an unattached node" }
var node: Modifier.Node? = node.child
var layout: LayoutNode? = requireLayoutNode()
// we use this bespoke data structure here specifically for traversing children. In the
@@ -130,7 +130,7 @@
}
internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
- check(node.isAttached)
+ check(node.isAttached) { "visitChildren called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
if (child == null)
@@ -160,7 +160,7 @@
* traversing below it
*/
internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
- check(node.isAttached)
+ check(node.isAttached) { "visitSubtreeIf called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
if (child == null)
@@ -187,7 +187,7 @@
mask: Int,
block: (Modifier.Node) -> Unit
) {
- check(node.isAttached)
+ check(node.isAttached) { "visitLocalDescendants called on an unattached node" }
val self = node
if (self.aggregateChildKindSet and mask == 0) return
var next = self.child
@@ -203,7 +203,7 @@
mask: Int,
block: (Modifier.Node) -> Unit
) {
- check(node.isAttached)
+ check(node.isAttached) { "visitLocalAncestors called on an unattached node" }
var next = node.parent
while (next != null) {
if (next.kindSet and mask != 0) {
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 6519bf8..2a0b6e9 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
@@ -226,21 +226,25 @@
override val parentData: Any?
get() {
- var data: Any? = null
- val thisNode = tail
+ // NOTE: If you make changes to this getter, please check the generated bytecode to
+ // ensure no extra allocation is made. See the note below.
if (layoutNode.nodes.has(Nodes.ParentData)) {
- with(layoutNode.density) {
- layoutNode.nodes.tailToHead { node ->
- if (node.isKind(Nodes.ParentData)) {
- node.dispatchForKind(Nodes.ParentData) {
- data = with(it) { modifyParentData(data) }
- }
+ val thisNode = tail
+ // NOTE: Keep this mutable variable scoped inside the if statement. When moved
+ // to the outer scope of get(), this causes the compiler to generate a
+ // Ref$ObjectRef instance on every call of this getter.
+ var data: Any? = null
+ layoutNode.nodes.tailToHead { node ->
+ if (node.isKind(Nodes.ParentData)) {
+ node.dispatchForKind(Nodes.ParentData) {
+ data = with(it) { layoutNode.density.modifyParentData(data) }
}
- if (node === thisNode) return@tailToHead
}
+ if (node === thisNode) return@tailToHead
}
+ return data
}
- return data
+ return null
}
final override val parentLayoutCoordinates: LayoutCoordinates?
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
index 5824041..488e08f 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/build.gradle
@@ -14,11 +14,9 @@
* limitations under the License.
*/
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -30,26 +28,23 @@
// We need animations to work for MotionLayout
testOptions.animationsDisabled false
+ targetProjectPath = ":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(project(":internal-testutils-runtime"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(project(":internal-testutils-runtime"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":constraintlayout:constraintlayout-compose:integration-tests:macrobenchmark-target:installRelease"
- )
- )
-}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8e90956
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<manifest />
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
similarity index 100%
rename from constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
rename to constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index c5f7d8c..4e95224 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1078,7 +1078,7 @@
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
method public static java.io.File? getDataDir(android.content.Context);
- method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
+ method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
method public static java.io.File![] getExternalCacheDirs(android.content.Context);
method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index a9d425f..7c8a63c 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1078,7 +1078,7 @@
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
method public static java.io.File? getDataDir(android.content.Context);
- method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
+ method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
method public static java.io.File![] getExternalCacheDirs(android.content.Context);
method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 6a02dd0..945e47f 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1195,7 +1195,7 @@
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
method public static java.io.File? getDataDir(android.content.Context);
- method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
+ method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
method public static java.io.File![] getExternalCacheDirs(android.content.Context);
method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index 8e9f2ea..2688f36 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -137,6 +137,10 @@
android:exported="true" />
<activity
+ android:name="androidx.core.view.inputmethod.ImeSecondarySplitViewCompatTestActivity"
+ android:exported="true" />
+
+ <activity
android:name="androidx.core.view.inputmethod.ImeSecondarySplitTestActivity"
android:exported="true" />
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 42fce77..03b73b5 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -608,7 +608,7 @@
@Test
public void testGetDisplayFromActivity() {
- final Display actualDisplay = ContextCompat.getDisplay(mContext);
+ final Display actualDisplay = ContextCompat.getDisplayOrDefault(mContext);
if (Build.VERSION.SDK_INT >= 30) {
assertEquals(mContext.getDisplay(), actualDisplay);
} else {
@@ -626,7 +626,7 @@
final Display defaultDisplay = displayManagerCompat.getDisplay(Display.DEFAULT_DISPLAY);
final Context displayContext = mContext.createDisplayContext(defaultDisplay);
- assertEquals(ContextCompat.getDisplay(displayContext), defaultDisplay);
+ assertEquals(ContextCompat.getDisplayOrDefault(displayContext), defaultDisplay);
}
@Test
@@ -634,14 +634,14 @@
public void testGetDisplayFromWindowContext() {
final Context windowContext = mContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
- assertEquals(ContextCompat.getDisplay(windowContext), windowContext.getDisplay());
+ assertEquals(ContextCompat.getDisplayOrDefault(windowContext), windowContext.getDisplay());
}
@Test
public void testGetDisplayFromApplication() {
final Context applicationContext = ApplicationProvider.getApplicationContext();
final Context spyContext = spy(applicationContext);
- final Display actualDisplay = ContextCompat.getDisplay(spyContext);
+ final Display actualDisplay = ContextCompat.getDisplayOrDefault(spyContext);
if (Build.VERSION.SDK_INT >= 30) {
verify(spyContext).getSystemService(eq(DisplayManager.class));
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java
new file mode 100644
index 0000000..5bcd1e28
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeSecondarySplitViewCompatTestActivity.java
@@ -0,0 +1,56 @@
+/*
+ * 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.core.view.inputmethod;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.R;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+
+@RequiresApi(30)
+public class ImeSecondarySplitViewCompatTestActivity extends Activity {
+
+ EditText mEditText;
+
+ Button mHideImeButton;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.ime_secondary_split_test_activity);
+ mEditText = findViewById(R.id.edit_text_id);
+ mHideImeButton = findViewById(R.id.hide_ime_id);
+ mHideImeButton.setOnClickListener(view -> hideIme());
+ }
+
+ private void hideIme() {
+ // Use deprecated WindowInsetsControllerCompat method to attempt to hide ime.
+ WindowInsetsControllerCompat insetsController =
+ ViewCompat.getWindowInsetsController(mEditText);
+ if (insetsController != null) {
+ insetsController.hide(WindowInsetsCompat.Type.ime());
+ }
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeViewCompatMultiWindowTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeViewCompatMultiWindowTest.java
new file mode 100644
index 0000000..c5fa2ef
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/ImeViewCompatMultiWindowTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.core.view.inputmethod;
+
+import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.RemoteException;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.core.view.WindowInsetsCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+import androidx.testutils.PollingCheck;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@SdkSuppress(minSdkVersion = 30)
+public class ImeViewCompatMultiWindowTest extends BaseInstrumentationTestCase<ImeBaseSplitTestActivity> {
+
+ private static final long ACTIVITY_LAUNCH_TIMEOUT_MS = 10000;
+ private static final long VISIBILITY_TIMEOUT_MS = 2000;
+ private static final long FIND_OBJECT_TIMEOUT_MS = 5000;
+ private static final long CLICK_DURATION_MS = 200;
+
+ private static final String TEST_APP = "androidx.core.test";
+
+ private Activity mActivity;
+
+ private UiDevice mDevice;
+
+ public ImeViewCompatMultiWindowTest() {
+ super(ImeBaseSplitTestActivity.class);
+ }
+
+ @Before
+ public void setup() throws RemoteException {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.wakeUp();
+ mActivity = mActivityTestRule.getActivity();
+ }
+
+ /**
+ * This test is using a deprecated codepath that doesn't support the workaround, so it is
+ * expected to fail hiding the IME.
+ * If this test begins failing on a new API version (that is, an assertion error is no longer
+ * being thrown), it is likely that the workaround is no longer needed on that API version:
+ * b/280532442
+ */
+ @Test(expected = AssertionError.class)
+ @SdkSuppress(minSdkVersion = 30)
+ public void testImeShowAndHide_splitScreen() {
+ if (Build.VERSION.SDK_INT < 32) {
+ // FLAG_ACTIVITY_LAUNCH_ADJACENT is not support before Sdk 32, using the
+ // GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN instead.
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN);
+ }
+
+ // Launch ime test activity in secondary split.
+ Intent intent = new Intent(mActivity, ImeSecondarySplitViewCompatTestActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ mActivity.startActivity(intent);
+
+ assertTrue("Test app is not visible after launching activity",
+ mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), ACTIVITY_LAUNCH_TIMEOUT_MS));
+
+ UiObject2 editText = waitForFindObject("edit_text_id");
+ editText.click(CLICK_DURATION_MS);
+
+ WindowManager wm = mActivity.getSystemService(WindowManager.class);
+ PollingCheck.waitFor(VISIBILITY_TIMEOUT_MS, () -> {
+ WindowInsets insets = wm.getCurrentWindowMetrics().getWindowInsets();
+ return insets.isVisible(WindowInsetsCompat.Type.ime());
+ });
+
+ UiObject2 hideImeButton = waitForFindObject("hide_ime_id");
+ hideImeButton.click();
+
+ PollingCheck.waitFor(VISIBILITY_TIMEOUT_MS, () -> {
+ WindowInsets insets = wm.getCurrentWindowMetrics().getWindowInsets();
+ return !insets.isVisible(WindowInsetsCompat.Type.ime());
+ });
+ }
+
+ private UiObject2 waitForFindObject(String resId) {
+ final UiObject2 object =
+ mDevice.wait(Until.findObject(By.res(TEST_APP, resId)), FIND_OBJECT_TIMEOUT_MS);
+ assertNotNull("Find object fail", object);
+ return object;
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 172b452..8cd98dd 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -756,7 +756,9 @@
}
/**
- * Get the display this context is associated with.
+ * Get the display this context is associated with or the
+ * {@link Display#DEFAULT_DISPLAY default display} as the fallback if the context is not
+ * associated with any {@link Display}.
* <p>
* Applications must use this method with {@link Activity} or a context associated with a
* {@link Display} via {@link Context#createDisplayContext(Display)} or
@@ -764,12 +766,13 @@
* instance is not reliable. </p>
*
* @param context Context to obtain the associated display
- * @return The display associated with the Context.
+ * @return The display associated with the Context or the default display if the context
+ * doesn't associated with any display.
*/
@NonNull
- public static Display getDisplay(@NonNull @DisplayContext Context context) {
+ public static Display getDisplayOrDefault(@NonNull @DisplayContext Context context) {
if (Build.VERSION.SDK_INT >= 30) {
- return Api30Impl.getDisplayNoCrash(context);
+ return Api30Impl.getDisplayOrDefault(context);
} else {
final WindowManager windowManager =
(WindowManager) context.getSystemService(WINDOW_SERVICE);
@@ -1140,7 +1143,7 @@
}
@DoNotInline
- static Display getDisplayNoCrash(Context obj) {
+ static Display getDisplayOrDefault(Context obj) {
try {
return obj.getDisplay();
} catch (UnsupportedOperationException e) {
diff --git a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
index 5ccc392..dee46e1 100644
--- a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
@@ -215,7 +215,7 @@
// callback current controllable insets. Adding the listener here to check if
// ime inset is controllable.
insetsController.addOnControllableInsetsChangedListener(listener);
- if (!isImeInsetsControllable.get()) {
+ if (!isImeInsetsControllable.get() && mView != null) {
final InputMethodManager imm = (InputMethodManager) mView.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
// This is a backport when the app is in multi-windowing mode, it cannot
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index c221e4d..e62e0d7 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -53,9 +53,10 @@
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
docs(project(":camera:camera-mlkit-vision"))
docs(project(":camera:camera-lifecycle"))
- docs(project(":camera:camera-viewfinder"))
docs(project(":camera:camera-video"))
docs(project(":camera:camera-view"))
+ docs(project(":camera:camera-viewfinder"))
+ docs(project(":camera:camera-viewfinder-compose"))
docs(project(":camera:camera-viewfinder-core"))
docs(project(":car:app:app"))
docs(project(":car:app:app-automotive"))
diff --git a/docs/api_guidelines/modules.md b/docs/api_guidelines/modules.md
index 85259bd..c125cc5 100644
--- a/docs/api_guidelines/modules.md
+++ b/docs/api_guidelines/modules.md
@@ -40,6 +40,45 @@
should follow the naming convention `com.android.extensions.<feature-name>` to
avoid placing `androidx`-packaged code in the platform's boot classpath.
+#### Maven name and description
+
+The `name` and `description` fields of the `androidx` configuration block are
+used to generate Maven artifact metadata, which is displayed on the artifact's
+maven.google.com entry and d.android.com landing page.
+
+```
+androidx {
+ name = "WorkManager Kotlin Extensions"
+ description = "Kotlin-friendly extensions for WorkManager."
+}
+```
+
+The name should be a human-readable, title-cased representation of the
+artifact's Maven coordinate. All components of the name **must** appear in the
+artifact's Maven group or artifact ID, with some exceptions:
+
+- Marketing names may be shortened when used in the Maven group or artifact
+ ID, ex. "WorkManager" as `work`, "Android for Cars" as `car`, or "Kotlin
+ Extensions" as `ktx`
+- Long (>10 character) words may be truncated to a short (>5 character) prefix
+- Pluralization may be changed, ex. "Views" as `view`
+- The following descriptive terms may appear in the name:
+ - "extension(s)"
+ - "for"
+ - "integration"
+ - "with"
+
+**Do not** use the following terms in the name:
+
+- "AndroidX"
+- "Library"
+- "Implementation"
+
+The description should be a single phrase that completes the sentence, "This
+library provides ...". This phrase should provide enough description that a
+developer can decide whether they might want to learn more about using your
+library. **Do not** simply repeat the name of the library.
+
#### Project directory structure {#module-structure}
Libraries developed in AndroidX follow a consistent project naming and directory
@@ -284,7 +323,7 @@
...
androidx {
- name = "Android Support Library collections"
+ name = "Collection"
type = LibraryType.KMP_LIBRARY
mavenGroup = LibraryGroups.COLLECTION
mavenVersion = KmpPlatformsKt.enableNative(project) ? LibraryVersions.COLLECTION_KMP : LibraryVersions.KMP
diff --git a/emoji2/integration-tests/init-disabled-macrobenchmark/build.gradle b/emoji2/integration-tests/init-disabled-macrobenchmark/build.gradle
index 708a1b6..4a65042 100644
--- a/emoji2/integration-tests/init-disabled-macrobenchmark/build.gradle
+++ b/emoji2/integration-tests/init-disabled-macrobenchmark/build.gradle
@@ -16,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -25,23 +25,20 @@
minSdkVersion 23
}
namespace "androidx.emoji2.integration.macrobenchmark.disabled"
+ targetProjectPath = ":emoji2:integration-tests:init-disabled-macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":emoji2:integration-tests:init-disabled-macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":emoji2:integration-tests:init-disabled-macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
}
diff --git a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml b/emoji2/integration-tests/init-disabled-macrobenchmark/src/main/AndroidManifest.xml
similarity index 60%
rename from benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
rename to emoji2/integration-tests/init-disabled-macrobenchmark/src/main/AndroidManifest.xml
index 37fa920..5b41847 100644
--- a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
+++ b/emoji2/integration-tests/init-disabled-macrobenchmark/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,12 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <queries>
- <!--
- Enables querying application info with packageManager.getApplicationInfo, to verify
- the target package is present
- -->
- <package android:name="androidx.benchmark.integration.macrobenchmark.target" />
- </queries>
-</manifest>
+<manifest />
diff --git a/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-disabled-macrobenchmark/src/main/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
similarity index 100%
rename from emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
rename to emoji2/integration-tests/init-disabled-macrobenchmark/src/main/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
diff --git a/emoji2/integration-tests/init-enabled-macrobenchmark/build.gradle b/emoji2/integration-tests/init-enabled-macrobenchmark/build.gradle
index 1dee7de..c1f5ed9 100644
--- a/emoji2/integration-tests/init-enabled-macrobenchmark/build.gradle
+++ b/emoji2/integration-tests/init-enabled-macrobenchmark/build.gradle
@@ -16,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -25,24 +25,21 @@
minSdkVersion 23
}
namespace "androidx.emoji2.integration.macrobenchmark.enabled"
+ targetProjectPath = ":emoji2:integration-tests:init-enabled-macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":emoji2:emoji2"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":emoji2:integration-tests:init-enabled-macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":emoji2:integration-tests:init-enabled-macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":emoji2:emoji2"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
}
diff --git a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml b/emoji2/integration-tests/init-enabled-macrobenchmark/src/main/AndroidManifest.xml
similarity index 60%
copy from benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
copy to emoji2/integration-tests/init-enabled-macrobenchmark/src/main/AndroidManifest.xml
index 37fa920..5b41847 100644
--- a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
+++ b/emoji2/integration-tests/init-enabled-macrobenchmark/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,12 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <queries>
- <!--
- Enables querying application info with packageManager.getApplicationInfo, to verify
- the target package is present
- -->
- <package android:name="androidx.benchmark.integration.macrobenchmark.target" />
- </queries>
-</manifest>
+<manifest />
diff --git a/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-enabled-macrobenchmark/src/main/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
similarity index 100%
rename from emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
rename to emoji2/integration-tests/init-enabled-macrobenchmark/src/main/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index 85a163f..158c764 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -70,6 +70,7 @@
androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
+ androidTestImplementation("androidx.room:room-runtime:2.4.3")
androidTestImplementation('androidx.core:core-ktx:1.7.0')
androidTestImplementation("androidx.work:work-testing:2.7.1")
androidTestImplementation(libs.espressoCore)
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
index 8cdae1e..447d079 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
@@ -84,14 +84,15 @@
modifier = GlanceModifier.fillMaxSize()
.background(R.color.default_widget_background)
) {
+ val localSize = LocalSize.current
Text(
- text = "Fix header",
+ text = "Fix header, LocalSize: ${localSize.width}x${localSize.height}",
modifier = GlanceModifier
.fillMaxWidth()
.padding(16.dp)
.background(Color(0x0a000000))
)
- val width = LocalSize.current.width
+ val width = localSize.width
when {
width <= singleColumn.width -> ScrollColumn(GlanceModifier.fillMaxSize())
width <= doubleColumn.width -> Row {
@@ -107,9 +108,22 @@
@Composable
private fun ScrollColumn(modifier: GlanceModifier) {
+ val localSize = LocalSize.current
LazyColumn(modifier) {
item {
SectionHeading(
+ title = "LocalSize",
+ description = "inside lazyColumn"
+ )
+ }
+ item {
+ Text(
+ text = "${localSize.width}x${localSize.height}",
+ modifier = GlanceModifier.padding(10.dp)
+ )
+ }
+ item {
+ SectionHeading(
title = "Activities",
description = "Click the buttons to open activities"
)
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
index 9c4f452..23d3804 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
@@ -25,6 +25,7 @@
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.LocalContext
+import androidx.glance.LocalSize
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.actionStartActivity
@@ -66,6 +67,7 @@
@Composable
fun SampleGrid(cells: GridCells, modifier: GlanceModifier = GlanceModifier.fillMaxSize()) {
+ val localSize = LocalSize.current
LazyVerticalGrid(
modifier = modifier,
gridCells = cells
@@ -73,6 +75,9 @@
item {
Text("LazyVerticalGrid")
}
+ item {
+ Text("${localSize.width}x${localSize.height}")
+ }
items(count = 20, itemId = { it * 2L }) { index ->
Text("Item $index")
}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle b/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle
index 19b0df6..cfa7f61 100644
--- a/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle
@@ -16,31 +16,37 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
android {
+ defaultConfig {
+ minSdkVersion 23
+ }
namespace "androidx.glance.appwidget.macrobenchmark"
+ targetProjectPath = ":glance:glance-appwidget:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
-android.defaultConfig {
- minSdkVersion 23
-}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
dependencies {
implementation 'androidx.compose.ui:ui-unit:1.2.1'
- androidTestImplementation(project(':benchmark:benchmark-macro'))
- androidTestImplementation(project(':benchmark:benchmark-common'))
- androidTestImplementation(project(':benchmark:benchmark-macro-junit4'))
- androidTestImplementation('androidx.core:core-ktx:1.7.0')
- androidTestImplementation(project(":glance:glance"))
- androidTestImplementation(project(":glance:glance-appwidget"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
+ implementation(project(':benchmark:benchmark-macro'))
+ implementation(project(':benchmark:benchmark-common'))
+ implementation(project(':benchmark:benchmark-macro-junit4'))
+ implementation('androidx.core:core-ktx:1.7.0')
+ implementation(project(":glance:glance"))
+ implementation(project(":glance:glance-appwidget"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.kotlinTest)
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
similarity index 99%
rename from glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
rename to glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
index 05a1d72..e7eaa978 100644
--- a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
@@ -25,4 +25,4 @@
android:configChanges="orientation|screenLayout|screenSize"
android:exported="true"/>
</application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt
similarity index 100%
rename from glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt
rename to glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt
similarity index 100%
rename from glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt
rename to glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt
similarity index 100%
rename from glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt
rename to glance/glance-appwidget/integration-tests/macrobenchmark/src/main/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/res/layout/app_widget_host_activity.xml b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/res/layout/app_widget_host_activity.xml
similarity index 100%
rename from glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/res/layout/app_widget_host_activity.xml
rename to glance/glance-appwidget/integration-tests/macrobenchmark/src/main/res/layout/app_widget_host_activity.xml
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AndroidTestUtils.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AndroidTestUtils.kt
index fa6fcab..164ee74 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AndroidTestUtils.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AndroidTestUtils.kt
@@ -161,6 +161,17 @@
findViewById<View>(emptyLoadingViewID) != null
}
+fun ListView.isItemLoaded(text: String): Boolean {
+ if (childCount > 0 && adapter != null) {
+ return children.any {
+ val matches = arrayListOf<View>()
+ it.findViewsWithText(matches, text, View.FIND_VIEWS_WITH_TEXT)
+ matches.isNotEmpty()
+ }
+ }
+ return false
+}
+
/**
* Returns true if list items are fully loaded (i.e. not in loading... state).
*/
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
index af28347..927111b 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
@@ -27,6 +27,9 @@
import android.widget.TextView
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.glance.Button
@@ -332,6 +335,48 @@
}
@Test
+ fun adapter_emptyList() {
+ TestGlanceAppWidget.uiDefinition = {
+ LazyColumn { }
+ }
+
+ mHostRule.startHost()
+
+ mHostRule.waitForListView { list ->
+ assertThat(list.childCount).isEqualTo(0)
+ assertThat(list.adapter.count).isEqualTo(0)
+ assertThat(list.adapter.viewTypeCount).isAtLeast(1)
+ assertThat(list.adapter.hasStableIds()).isFalse()
+ }
+ }
+
+ @Test
+ fun adapter_itemContentChangesOnClick_appliedCorrectly() {
+ TestGlanceAppWidget.uiDefinition = {
+ var count by remember { mutableStateOf(1) }
+ LazyColumn {
+ item {
+ Text(
+ text = "Row item 0, count $count",
+ modifier = GlanceModifier.clickable {
+ count++
+ })
+ }
+ }
+ }
+
+ mHostRule.startHost()
+
+ mHostRule.waitForListViewChildren { list ->
+ val row = list.getUnboxedListItem<FrameLayout>(0)
+ val rowItem0 = row.notGoneChildren.first()
+ rowItem0.performClick()
+ }
+
+ mHostRule.waitForListViewChildWithText(text = "Row item 0, count 1") {}
+ }
+
+ @Test
@SdkSuppress(minSdkVersion = 31)
fun clickable_addsClickHandlers() {
TestGlanceAppWidget.uiDefinition = {
@@ -503,6 +548,23 @@
}
}
+/**
+ * Wait until the ListView is loaded and has an adapter (irrespective of whether it has children or
+ * not). Use waitForListViewChildren if the list is expected to have children.
+ */
+internal fun AppWidgetHostRule.waitForListView(action: (list: ListView) -> Unit = {}) {
+ onHostView { }
+
+ runAndObserveUntilDraw(condition = "ListView did not load in time") {
+ mHostView.let { host ->
+ val list = host.findChildByType<ListView>()
+ host.childCount > 0 && list != null && list.adapter != null
+ }
+ }
+
+ onUnboxedHostView(action)
+}
+
internal fun AppWidgetHostRule.waitForListViewChildren(action: (list: ListView) -> Unit = {}) {
onHostView { }
@@ -516,6 +578,22 @@
onUnboxedHostView(action)
}
+internal fun AppWidgetHostRule.waitForListViewChildWithText(
+ text: String,
+ action: (list: ListView) -> Unit = {}
+) {
+ onHostView { }
+
+ runAndObserveUntilDraw(condition = "List child with text '$text' not load in time") {
+ mHostView.let { host ->
+ val list = host.findChildByType<ListView>()
+ host.childCount > 0 && list?.isItemLoaded(text) ?: false
+ }
+ }
+
+ onUnboxedHostView(action)
+}
+
/**
* Wait until the first ListView child under the root AppWidgetHostView has [count] children.
*
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/RemoteCollectionItemsTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/RemoteCollectionItemsTest.kt
new file mode 100644
index 0000000..41561f8
--- /dev/null
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/RemoteCollectionItemsTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.appwidget
+
+import android.content.Context
+import android.widget.RemoteViews
+import androidx.glance.appwidget.test.R
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+
+@SdkSuppress(maxSdkVersion = 29)
+@MediumTest
+class RemoteCollectionItemsTest {
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val packageName = context.packageName
+
+ @Test
+ fun testBuilder_empty() {
+ val items = RemoteCollectionItems.Builder().build()
+
+ assertThat(items.itemCount).isEqualTo(0)
+ assertThat(items.viewTypeCount).isEqualTo(1)
+ assertThat(items.hasStableIds()).isFalse()
+ }
+
+ @Test
+ fun testBuilder_viewTypeCountUnspecified() {
+ val firstItem = RemoteViews(packageName, R.layout.list_view_row)
+ val secondItem = RemoteViews(packageName, R.layout.list_view_row_2)
+ val items = RemoteCollectionItems.Builder()
+ .setHasStableIds(true)
+ .addItem(id = 3, firstItem)
+ .addItem(id = 5, secondItem)
+ .build()
+
+ assertThat(items.itemCount).isEqualTo(2)
+ assertThat(items.getItemId(0)).isEqualTo(3)
+ assertThat(items.getItemId(1)).isEqualTo(5)
+ assertThat(items.getItemView(0).layoutId).isEqualTo(R.layout.list_view_row)
+ assertThat(items.getItemView(1).layoutId).isEqualTo(R.layout.list_view_row_2)
+ assertThat(items.hasStableIds()).isTrue()
+ // The view type count should be derived from the number of different layout ids if
+ // unspecified.
+ assertThat(items.viewTypeCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testBuilder_viewTypeCountSpecified() {
+ val firstItem = RemoteViews(packageName, R.layout.list_view_row)
+ val secondItem = RemoteViews(packageName, R.layout.list_view_row_2)
+ val items = RemoteCollectionItems.Builder()
+ .addItem(id = 3, firstItem)
+ .addItem(id = 5, secondItem)
+ .setViewTypeCount(15)
+ .build()
+
+ assertThat(items.viewTypeCount).isEqualTo(15)
+ }
+
+ @Test
+ fun testBuilder_repeatedIdsAndLayouts() {
+ val firstItem = RemoteViews(packageName, R.layout.list_view_row)
+ val secondItem = RemoteViews(packageName, R.layout.list_view_row)
+ val thirdItem = RemoteViews(packageName, R.layout.list_view_row)
+ val items = RemoteCollectionItems.Builder()
+ .setHasStableIds(false)
+ .addItem(id = 42, firstItem)
+ .addItem(id = 42, secondItem)
+ .addItem(id = 42, thirdItem)
+ .build()
+
+ assertThat(items.itemCount).isEqualTo(3)
+ assertThat(items.getItemId(0)).isEqualTo(42)
+ assertThat(items.getItemId(1)).isEqualTo(42)
+ assertThat(items.getItemId(2)).isEqualTo(42)
+ assertThat(items.getItemView(0)).isSameInstanceAs(firstItem)
+ assertThat(items.getItemView(1)).isSameInstanceAs(secondItem)
+ assertThat(items.getItemView(2)).isSameInstanceAs(thirdItem)
+ assertThat(items.hasStableIds()).isFalse()
+ assertThat(items.viewTypeCount).isEqualTo(1)
+ }
+
+ @Test
+ fun testBuilder_viewTypeCountLowerThanLayoutCount() {
+ assertFailsWith(IllegalArgumentException::class) {
+ RemoteCollectionItems.Builder()
+ .setHasStableIds(true)
+ .setViewTypeCount(1)
+ .addItem(3, RemoteViews(packageName, R.layout.list_view_row))
+ .addItem(5, RemoteViews(packageName, R.layout.list_view_row_2))
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/StrictModeTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/StrictModeTest.kt
index 6a3e51f..0caab64 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/StrictModeTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/StrictModeTest.kt
@@ -168,4 +168,4 @@
Truth.assertThat(CallbackTest.latch.await(5, TimeUnit.SECONDS)).isTrue()
Truth.assertThat(CallbackTest.received.get()).containsExactly(1, 2)
}
-}
\ No newline at end of file
+}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row.xml b/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row.xml
new file mode 100644
index 0000000..73eeb18
--- /dev/null
+++ b/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"/>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row_2.xml b/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row_2.xml
new file mode 100644
index 0000000..7d2ccd3
--- /dev/null
+++ b/glance/glance-appwidget/src/androidAndroidTest/res/layout/list_view_row_2.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidAndroidTest/res/layout/remote_views_list.xml b/glance/glance-appwidget/src/androidAndroidTest/res/layout/remote_views_list.xml
new file mode 100644
index 0000000..8d0b5c9
--- /dev/null
+++ b/glance/glance-appwidget/src/androidAndroidTest/res/layout/remote_views_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_view"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/AndroidManifest.xml b/glance/glance-appwidget/src/androidMain/AndroidManifest.xml
index 9bcafb2..a78692f 100644
--- a/glance/glance-appwidget/src/androidMain/AndroidManifest.xml
+++ b/glance/glance-appwidget/src/androidMain/AndroidManifest.xml
@@ -41,5 +41,9 @@
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
+ <service
+ android:name="androidx.glance.appwidget.GlanceRemoteViewsService"
+ android:permission="android.permission.BIND_REMOTEVIEWS"
+ android:exported="true" />
</application>
</manifest>
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
index 92f688c..74bc2e5 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
@@ -42,6 +42,7 @@
import androidx.glance.state.ConfigManager
import androidx.glance.state.GlanceState
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.channels.Channel
/**
* A session that composes UI for a single app widget.
@@ -73,6 +74,7 @@
private val glanceState = mutableStateOf<Any?>(null, neverEqualPolicy())
private val options = mutableStateOf(Bundle(), neverEqualPolicy())
private var lambdas = mapOf<String, List<LambdaAction>>()
+
@VisibleForTesting
internal var lastRemoteViews: RemoteViews? = null
private set
@@ -184,6 +186,7 @@
lambdas[event.key]?.forEach { it.block() }
} ?: Log.w(TAG, "Triggering Action(${event.key}) for session($key) failed")
}
+ is WaitForReady -> event.resume.send(Unit)
else -> {
throw IllegalArgumentException(
"Sent unrecognized event type ${event.javaClass} to AppWidgetSession"
@@ -204,6 +207,13 @@
sendEvent(RunLambda(key))
}
+ suspend fun waitForReady() {
+ WaitForReady().let {
+ sendEvent(it)
+ it.resume.receive()
+ }
+ }
+
// Event types that this session supports.
@VisibleForTesting
internal object UpdateGlanceState
@@ -211,4 +221,8 @@
internal class UpdateAppWidgetOptions(val newOptions: Bundle)
@VisibleForTesting
internal class RunLambda(val key: String)
+ @VisibleForTesting
+ internal class WaitForReady(
+ val resume: Channel<Unit> = Channel(Channel.CONFLATED)
+ )
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceRemoteViewsService.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceRemoteViewsService.kt
new file mode 100644
index 0000000..a9b49c7
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceRemoteViewsService.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.appwidget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.glance.session.GlanceSessionManager
+import kotlinx.coroutines.runBlocking
+
+/**
+ * [RemoteViewsService] to be connected to for a remote adapter that returns RemoteViews for lazy
+ * lists / grids.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class GlanceRemoteViewsService : RemoteViewsService() {
+ override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
+ val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
+ check(appWidgetId != -1) { "No app widget id was present in the intent" }
+
+ val viewId = intent.getIntExtra(EXTRA_VIEW_ID, -1)
+ check(viewId != -1) { "No view id was present in the intent" }
+
+ val sizeInfo = intent.getStringExtra(EXTRA_SIZE_INFO)
+ check(!sizeInfo.isNullOrEmpty()) { "No size info was present in the intent" }
+
+ return GlanceRemoteViewsFactory(this, appWidgetId, viewId, sizeInfo)
+ }
+
+ companion object {
+ const val EXTRA_VIEW_ID = "androidx.glance.widget.extra.view_id"
+ const val EXTRA_SIZE_INFO = "androidx.glance.widget.extra.size_info"
+
+ // An in-memory store containing items to be returned via the adapter when requested.
+ private val InMemoryStore = RemoteCollectionItemsInMemoryStore()
+
+ // Adds items to the store for later use by the list adapter to display the items.
+ internal fun saveItems(
+ appWidgetId: Int,
+ viewId: Int,
+ sizeInfo: String,
+ remoteCollectionItems: RemoteCollectionItems
+ ) {
+ synchronized(InMemoryStore) {
+ InMemoryStore.save(appWidgetId, viewId, sizeInfo, remoteCollectionItems)
+ }
+ }
+
+ // Returns items in the store for the requested view in appwidget for the specified size.
+ private fun getItems(
+ appWidgetId: Int,
+ viewId: Int,
+ sizeInfo: String
+ ): RemoteCollectionItems {
+ return synchronized(InMemoryStore) {
+ InMemoryStore.getItems(appWidgetId, viewId, sizeInfo)
+ }
+ }
+
+ // Removes items in the store for the requested view in appwidget for the specified size.
+ private fun removeItems(appWidgetId: Int, viewId: Int, sizeInfo: String) {
+ synchronized(InMemoryStore) {
+ InMemoryStore.removeItems(appWidgetId, viewId, sizeInfo)
+ }
+ }
+ }
+
+ /**
+ * A RemoteViewsFactory that holds items in memory and provides it to the host when requested.
+ *
+ * <p>Starts glance session if needed to reload items in memory e.g. when app process was killed
+ * and user scrolled on an existing list / grid view.
+ */
+ internal class GlanceRemoteViewsFactory(
+ private val context: Context,
+ private val appWidgetId: Int,
+ private val viewId: Int,
+ private val size: String
+ ) : RemoteViewsFactory {
+ override fun onCreate() {
+ // OnDataSetChanged is always called even onCreate, so we don't need to load data here.
+ }
+
+ override fun onDataSetChanged() = loadData()
+
+ private fun loadData() {
+ runBlocking {
+ val glanceId = AppWidgetId(appWidgetId)
+ // If session is already running, data must have already been loaded into the store
+ // during composition.
+ if (!GlanceSessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
+ startSessionAndWaitUntilReady(glanceId)
+ }
+ }
+ }
+
+ private suspend fun startSessionAndWaitUntilReady(glanceId: AppWidgetId) {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
+ if (providerInfo?.provider != null) {
+ val receiverClass = Class.forName(providerInfo.provider.className)
+ val glanceAppWidget =
+ (receiverClass.getDeclaredConstructor()
+ .newInstance() as GlanceAppWidgetReceiver).glanceAppWidget
+ AppWidgetSession(glanceAppWidget, glanceId)
+ .also { GlanceSessionManager.startSession(context, it) }
+ .waitForReady()
+ }
+ }
+
+ override fun onDestroy() {
+ removeItems(appWidgetId, viewId, size)
+ }
+
+ private fun items() = getItems(appWidgetId, viewId, size)
+
+ override fun getCount(): Int {
+ return items().itemCount
+ }
+
+ override fun getViewAt(position: Int): RemoteViews {
+ return items().getItemView(position)
+ }
+
+ override fun getLoadingView() = null
+
+ override fun getViewTypeCount(): Int = items().viewTypeCount
+
+ override fun getItemId(position: Int): Long = items().getItemId(position)
+
+ override fun hasStableIds(): Boolean = items().hasStableIds()
+ }
+}
+
+/**
+ * An in-memory store holding [RemoteCollectionItems] for each sized lazy view in appWidgets.
+ */
+private class RemoteCollectionItemsInMemoryStore {
+ private val items = mutableMapOf<String, RemoteCollectionItems>()
+
+ fun save(
+ appWidgetId: Int,
+ viewId: Int,
+ sizeInfo: String,
+ remoteCollectionItems: RemoteCollectionItems
+ ) {
+ items[key(appWidgetId, viewId, sizeInfo)] = remoteCollectionItems
+ }
+
+ /**
+ * Returns the collection items corresponding to the requested view in appwidget and size.
+ */
+ fun getItems(appWidgetId: Int, viewId: Int, sizeInfo: String): RemoteCollectionItems {
+ return items[key(appWidgetId, viewId, sizeInfo)] ?: RemoteCollectionItems.Empty
+ }
+
+ /**
+ * Removes the collection items corresponding to the requested view in appwidget and size.
+ */
+ fun removeItems(appWidgetId: Int, viewId: Int, sizeInfo: String) {
+ items.remove(key(appWidgetId, viewId, sizeInfo))
+ }
+
+ // A unique key for RemoteCollectionItems in the store. Including size info allows us to compose
+ // for different sizes and maintain separate collection items for each size.
+ private fun key(appWidgetId: Int, viewId: Int, sizeInfo: String): String {
+ return "$appWidgetId-$viewId-$sizeInfo"
+ }
+}
+
+/**
+ * Sets remote views adapter.
+ *
+ * <p>For SDKs higher than S, passes the items in the adapter. For S and below SDKs, connects to a
+ * GlanceRemoteViewsService using an intent.
+ */
+@Suppress("DEPRECATION")
+@DoNotInline
+internal fun RemoteViews.setRemoteAdapter(
+ context: Context,
+ appWidgetId: Int,
+ viewId: Int,
+ sizeInfo: String,
+ items: RemoteCollectionItems
+) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
+ CollectionItemsApi31Impl.setRemoteAdapter(this, viewId, items)
+ } else {
+ val intent = Intent(context, GlanceRemoteViewsService::class.java)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ .putExtra(GlanceRemoteViewsService.EXTRA_VIEW_ID, viewId)
+ .putExtra(GlanceRemoteViewsService.EXTRA_SIZE_INFO, sizeInfo)
+ .apply {
+ // Set a data Uri to disambiguate Intents for different widget/view ids.
+ data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
+ }
+ check(context.packageManager.resolveService(intent, 0) != null) {
+ "GlanceRemoteViewsService could not be resolved, check the app manifest."
+ }
+ setRemoteAdapter(viewId, intent)
+ GlanceRemoteViewsService.saveItems(
+ appWidgetId,
+ viewId,
+ sizeInfo,
+ items
+ )
+ AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, viewId)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object CollectionItemsApi31Impl {
+ @DoNotInline
+ fun setRemoteAdapter(remoteViews: RemoteViews, viewId: Int, items: RemoteCollectionItems) {
+ remoteViews.setRemoteAdapter(viewId, toPlatformCollectionItems(items))
+ }
+
+ @DoNotInline
+ fun toPlatformCollectionItems(items: RemoteCollectionItems):
+ RemoteViews.RemoteCollectionItems {
+ return RemoteViews.RemoteCollectionItems.Builder()
+ .setHasStableIds(items.hasStableIds())
+ .setViewTypeCount(items.viewTypeCount)
+ .also { builder ->
+ repeat(items.itemCount) { index ->
+ builder.addItem(items.getItemId(index), items.getItemView(index))
+ }
+ }
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteCollectionItems.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteCollectionItems.kt
new file mode 100644
index 0000000..61c8bcb
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteCollectionItems.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.appwidget
+
+import android.annotation.SuppressLint
+import android.widget.RemoteViews
+
+/** Representation of a fixed list of items to be displayed in a RemoteViews collection. */
+internal class RemoteCollectionItems private constructor(
+ private val ids: LongArray,
+ private val views: Array<RemoteViews>,
+ private val hasStableIds: Boolean,
+ private val _viewTypeCount: Int
+) {
+ init {
+ require(ids.size == views.size) {
+ "RemoteCollectionItems has different number of ids and views"
+ }
+ require(_viewTypeCount >= 1) { "View type count must be >= 1" }
+ val layoutIdCount = views.map { it.layoutId }.distinct().count()
+ require(layoutIdCount <= _viewTypeCount) {
+ "View type count is set to $_viewTypeCount, but the collection contains " +
+ "$layoutIdCount different layout ids"
+ }
+ }
+
+ /**
+ * Returns the id for [position]. See [hasStableIds] for whether this id should be
+ * considered meaningful across collection updates.
+ *
+ * @return Id for the position.
+ */
+ fun getItemId(position: Int): Long = ids[position]
+
+ /**
+ * Returns the [RemoteViews] to display at [position].
+ *
+ * @return RemoteViews for the position.
+ */
+ fun getItemView(position: Int): RemoteViews = views[position]
+
+ /**
+ * Returns the number of elements in the collection.
+ *
+ * @return Count of items.
+ */
+ val itemCount: Int
+ get() = ids.size
+
+ /**
+ * Returns the view type count for the collection when used in an adapter
+ *
+ * @return Count of view types for the collection when used in an adapter.
+ * @see android.widget.Adapter.getViewTypeCount
+ */
+ val viewTypeCount: Int
+ get() = _viewTypeCount
+
+ /**
+ * Indicates whether the item ids are stable across changes to the underlying data.
+ *
+ * @return True if the same id always refers to the same object.
+ * @see android.widget.Adapter.hasStableIds
+ */
+ fun hasStableIds(): Boolean = hasStableIds
+
+ /** Builder class for [RemoteCollectionItems] objects. */
+ class Builder {
+ private val ids = arrayListOf<Long>()
+ private val views = arrayListOf<RemoteViews>()
+ private var hasStableIds = false
+ private var viewTypeCount = 0
+
+ /**
+ * Adds a [RemoteViews] to the collection.
+ *
+ * @param id Id to associate with the row. Use [.setHasStableIds] to indicate that ids are
+ * stable across changes to the collection.
+ * @param view RemoteViews to display for the row.
+ */
+ // Covered by getItemId, getItemView, getItemCount.
+ @SuppressLint("MissingGetterMatchingBuilder")
+ fun addItem(id: Long, view: RemoteViews): Builder {
+ ids.add(id)
+ views.add(view)
+ return this
+ }
+
+ /**
+ * Sets whether the item ids are stable across changes to the underlying data.
+ *
+ * @see android.widget.Adapter.hasStableIds
+ */
+ fun setHasStableIds(hasStableIds: Boolean): Builder {
+ this.hasStableIds = hasStableIds
+ return this
+ }
+
+ /**
+ * Sets the view type count for the collection when used in an adapter. This can be set
+ * to the maximum number of different layout ids that will be used by RemoteViews in
+ * this collection.
+ *
+ * If this value is not set, then a value will be inferred from the provided items. As
+ * a result, the adapter may need to be recreated when the list is updated with
+ * previously unseen RemoteViews layouts for new items.
+ *
+ * @see android.widget.Adapter.getViewTypeCount
+ */
+ fun setViewTypeCount(viewTypeCount: Int): Builder {
+ this.viewTypeCount = viewTypeCount
+ return this
+ }
+
+ /** Creates the [RemoteCollectionItems] defined by this builder. */
+ fun build(): RemoteCollectionItems {
+ if (viewTypeCount < 1) {
+ // If a view type count wasn't specified, set it to be the number of distinct
+ // layout ids used in the items.
+ viewTypeCount = views.map { it.layoutId }.distinct().count()
+ }
+ return RemoteCollectionItems(
+ ids.toLongArray(),
+ views.toTypedArray(),
+ hasStableIds,
+ maxOf(viewTypeCount, 1)
+ )
+ }
+ }
+
+ companion object {
+ val Empty = RemoteCollectionItems(
+ ids = longArrayOf(),
+ views = emptyArray(),
+ hasStableIds = false,
+ _viewTypeCount = 1
+ )
+ }
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index e6a5fed..2618cbc 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -30,6 +30,7 @@
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.isSpecified
import androidx.core.widget.RemoteViewsCompat.setLinearLayoutGravity
import androidx.glance.Emittable
import androidx.glance.EmittableButton
@@ -114,7 +115,10 @@
val size = (child as EmittableSizeBox).size
val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
val rv = remoteViewsInfo.remoteViews.apply {
- translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
+ translateChild(
+ translationContext.forRootAndSize(root = remoteViewsInfo, size),
+ child
+ )
}
size.toSizeF() to rv
}
@@ -146,7 +150,7 @@
else -> throw IllegalArgumentException("There must be between 1 and 2 views.")
}
-private const val LastInvalidViewId = 1
+private const val LAST_INVALID_VIEW_ID = 1
internal data class TranslationContext(
val context: Context,
@@ -155,7 +159,7 @@
val layoutConfiguration: LayoutConfiguration?,
val itemPosition: Int,
val isLazyCollectionDescendant: Boolean = false,
- val lastViewId: AtomicInteger = AtomicInteger(LastInvalidViewId),
+ val lastViewId: AtomicInteger = AtomicInteger(LAST_INVALID_VIEW_ID),
val parentContext: InsertedViewInfo = InsertedViewInfo(),
val isBackgroundSpecified: AtomicBoolean = AtomicBoolean(false),
val layoutSize: DpSize = DpSize.Zero,
@@ -174,7 +178,15 @@
forChild(pos = 0, parent = root.view)
.copy(
isBackgroundSpecified = AtomicBoolean(false),
- lastViewId = AtomicInteger(LastInvalidViewId),
+ lastViewId = AtomicInteger(LAST_INVALID_VIEW_ID),
+ )
+
+ fun forRootAndSize(root: RemoteViewsInfo, layoutSize: DpSize): TranslationContext =
+ forChild(pos = 0, parent = root.view)
+ .copy(
+ isBackgroundSpecified = AtomicBoolean(false),
+ lastViewId = AtomicInteger(LAST_INVALID_VIEW_ID),
+ layoutSize = layoutSize
)
fun resetViewId(newViewId: Int = 0) = copy(lastViewId = AtomicInteger(newViewId))
@@ -190,6 +202,14 @@
fun forActionTargetId(viewId: Int) = copy(actionTargetId = viewId)
}
+internal fun DpSize.toSizeString(): String {
+ return if (isSpecified) {
+ "${width}x$height"
+ } else {
+ "Unspecified"
+ }
+}
+
internal fun RemoteViews.translateChild(
translationContext: TranslationContext,
element: Emittable
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
index 6c02991..2698f76 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -22,9 +22,9 @@
import android.content.Intent
import android.content.Intent.FILL_IN_COMPONENT
import android.widget.RemoteViews
-import androidx.core.widget.RemoteViewsCompat
import androidx.glance.appwidget.InsertedViewInfo
import androidx.glance.appwidget.LayoutType
+import androidx.glance.appwidget.RemoteCollectionItems
import androidx.glance.appwidget.TopLevelLayoutsCount
import androidx.glance.appwidget.TranslationContext
import androidx.glance.appwidget.applyModifiers
@@ -33,6 +33,8 @@
import androidx.glance.appwidget.lazy.EmittableLazyList
import androidx.glance.appwidget.lazy.EmittableLazyListItem
import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.setRemoteAdapter
+import androidx.glance.appwidget.toSizeString
import androidx.glance.appwidget.translateChild
import androidx.glance.appwidget.translateComposition
import androidx.glance.layout.Alignment
@@ -67,7 +69,7 @@
FILL_IN_COMPONENT or FLAG_MUTABLE or FLAG_UPDATE_CURRENT,
)
)
- val items = RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
+ val items = RemoteCollectionItems.Builder().apply {
val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
element.children.foldIndexed(false) { position, previous, itemEmittable ->
itemEmittable as EmittableLazyListItem
@@ -85,11 +87,11 @@
}.let { setHasStableIds(it) }
setViewTypeCount(TopLevelLayoutsCount)
}.build()
- RemoteViewsCompat.setRemoteAdapter(
+ setRemoteAdapter(
translationContext.context,
- this,
translationContext.appWidgetId,
viewDef.mainViewId,
+ translationContext.layoutSize.toSizeString(),
items
)
applyModifiers(translationContext, this, element.modifier, viewDef)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
index b81a8c9..805c1f7 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -16,17 +16,17 @@
package androidx.glance.appwidget.translators
-import android.os.Build
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Intent
import android.content.Intent.FILL_IN_COMPONENT
+import android.os.Build
import android.widget.RemoteViews
-import androidx.core.widget.RemoteViewsCompat
import androidx.core.widget.RemoteViewsCompat.setGridViewColumnWidth
import androidx.glance.appwidget.InsertedViewInfo
import androidx.glance.appwidget.LayoutType
+import androidx.glance.appwidget.RemoteCollectionItems
import androidx.glance.appwidget.TopLevelLayoutsCount
import androidx.glance.appwidget.TranslationContext
import androidx.glance.appwidget.applyModifiers
@@ -35,6 +35,8 @@
import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
import androidx.glance.appwidget.lazy.GridCells
import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.setRemoteAdapter
+import androidx.glance.appwidget.toSizeString
import androidx.glance.appwidget.translateChild
import androidx.glance.appwidget.translateComposition
import androidx.glance.layout.Alignment
@@ -80,7 +82,7 @@
FILL_IN_COMPONENT or FLAG_MUTABLE or FLAG_UPDATE_CURRENT,
)
)
- val items = RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
+ val items = RemoteCollectionItems.Builder().apply {
val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
element.children.foldIndexed(false) { position, previous, itemEmittable ->
itemEmittable as EmittableLazyVerticalGridListItem
@@ -98,11 +100,11 @@
}.let { setHasStableIds(it) }
setViewTypeCount(TopLevelLayoutsCount)
}.build()
- RemoteViewsCompat.setRemoteAdapter(
+ setRemoteAdapter(
translationContext.context,
- this,
translationContext.appWidgetId,
viewDef.mainViewId,
+ translationContext.layoutSize.toSizeString(),
items
)
if (Build.VERSION.SDK_INT >= 31 && gridCells is GridCells.Adaptive) {
diff --git a/libraryversions.toml b/libraryversions.toml
index e865369..413f3f3 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -21,7 +21,7 @@
COLLECTION = "1.3.0-alpha05"
COMPOSE = "1.5.0-alpha04"
COMPOSE_COMPILER = "1.4.7"
-COMPOSE_MATERIAL3 = "1.2.0-alpha01"
+COMPOSE_MATERIAL3 = "1.2.0-alpha02"
COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha03"
CONSTRAINTLAYOUT = "2.2.0-alpha10"
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index f05b834..7de1624 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -75,6 +75,7 @@
// MissingJvmDefaultWithCompatibilityDetector is intentionally left out of the
// registry, see comments on the class for more details.
BanVisibleForTestingParams.ISSUE,
+ PrereleaseSdkCoreDependencyDetector.ISSUE
)
}
}
diff --git a/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt b/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt
new file mode 100644
index 0000000..fc50974
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.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.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.model.LintModelAndroidLibrary
+import com.android.tools.lint.model.LintModelLibrary
+import org.jetbrains.uast.UCallExpression
+
+class PrereleaseSdkCoreDependencyDetector : Detector(), Detector.UastScanner {
+
+ override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return CallChecker(context)
+ }
+
+ private inner class CallChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ // Check that this is a prerelease SDK check
+ val method = node.resolve() ?: return
+ val containingClass = method.containingClass ?: return
+ if (containingClass.qualifiedName != BUILD_COMPAT) return
+
+ if (method.annotations.none { it.hasQualifiedName(PRERELEASE_SDK_CHECK) }) return
+
+ // Check if the project is using a versioned dependency on core
+ val dependencies = context.project.buildVariant.mainArtifact.dependencies.getAll()
+ if (dependencies.any { it.isInvalidCoreDependency() }) {
+ val incident = Incident(context)
+ .issue(ISSUE)
+ .location(context.getLocation(node))
+ .message(
+ "Prelease SDK check ${method.name} cannot be called as this project has " +
+ "a versioned dependency on androidx.core:core"
+ )
+ .scope(node)
+ context.report(incident)
+ }
+ }
+
+ /**
+ * Checks whether this library is a dependency on a specific version of androidx.core:core
+ */
+ private fun LintModelLibrary.isInvalidCoreDependency(): Boolean {
+ val library = this as? LintModelAndroidLibrary ?: return false
+ val coordinates = library.resolvedCoordinates
+ return coordinates.artifactId == "core" &&
+ coordinates.groupId == "androidx.core" &&
+ coordinates.version != "unspecified"
+ }
+ }
+
+ companion object {
+ val ISSUE = Issue.create(
+ "PrereleaseSdkCoreDependency",
+ "Prerelease SDK checks can only be used by projects with a TOT dependency on " +
+ "androidx.core:core",
+ """
+ The implementation of a prerelease SDK check will change when the SDK is finalized,
+ so projects using these checks must have a tip-of-tree dependency on core to ensure
+ the check stays up-to-date.
+
+ This error means that the `androidx.core:core` dependency in this project's
+ `build.gradle` file should be replaced with `implementation(project(":core:core"))`
+
+ See go/androidx-api-guidelines#compat-sdk for more information.
+ """,
+ Category.CORRECTNESS, 5, Severity.ERROR,
+ Implementation(
+ PrereleaseSdkCoreDependencyDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private const val BUILD_COMPAT = "androidx.core.os.BuildCompat"
+ private const val PRERELEASE_SDK_CHECK = "$BUILD_COMPAT.PrereleaseSdkCheck"
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetectorTest.kt
new file mode 100644
index 0000000..b6d3422
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetectorTest.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PrereleaseSdkCoreDependencyDetectorTest : AbstractLintDetectorTest(
+ useDetector = PrereleaseSdkCoreDependencyDetector(),
+ useIssues = listOf(PrereleaseSdkCoreDependencyDetector.ISSUE),
+ stubs = arrayOf(
+ Stubs.BuildCompat,
+ Stubs.ChecksSdkIntAtLeast,
+ Stubs.JetpackRequiresOptIn,
+ Stubs.RestrictTo
+ )
+) {
+ @Test
+ fun `Versioned dependency with isAtLeastU is flagged`() {
+ val input = arrayOf(
+ kotlin(
+ """
+ package androidx.test
+
+ import androidx.core.os.BuildCompat
+
+ fun callIsAtLeastU() {
+ return BuildCompat.isAtLeastU()
+ }
+ """.trimIndent()
+ ),
+ gradle("""
+ dependencies {
+ implementation("androidx.core:core:1.9.0")
+ }
+ """.trimIndent()),
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+ src/main/kotlin/androidx/test/test.kt:6: Error: Prelease SDK check isAtLeastU cannot be called as this project has a versioned dependency on androidx.core:core [PrereleaseSdkCoreDependency]
+ return BuildCompat.isAtLeastU()
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected)
+ }
+
+ @Test
+ fun `Tip-of-tree dependency with isAtLeastU is not flagged`() {
+ val input = arrayOf(
+ kotlin(
+ """
+ package androidx.test
+
+ import androidx.core.os.BuildCompat
+
+ fun callIsAtLeastU() {
+ return BuildCompat.isAtLeastU()
+ }
+ """.trimIndent()
+ ),
+ gradle("""
+ dependencies {
+ implementation(project(":core:core"))
+ }
+ """.trimIndent()),
+ )
+
+ check(*input).expectClean()
+ }
+
+ @Test
+ fun `Versioned dependency with isAtLeastSv2 is flagged`() {
+ val input = arrayOf(
+ kotlin(
+ """
+ package androidx.test
+
+ import androidx.core.os.BuildCompat
+
+ fun callIsAtLeastSv2() {
+ return BuildCompat.isAtLeastSv2()
+ }
+ """.trimIndent()
+ ),
+ gradle("""
+ dependencies {
+ implementation("androidx.core:core:1.9.0")
+ }
+ """.trimIndent()),
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+ src/main/kotlin/androidx/test/test.kt:6: Error: Prelease SDK check isAtLeastSv2 cannot be called as this project has a versioned dependency on androidx.core:core [PrereleaseSdkCoreDependency]
+ return BuildCompat.isAtLeastSv2()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected)
+ }
+
+ @Test
+ fun `Versioned dependency with non-annotated isAtLeastN is not flagged`() {
+ val input = arrayOf(
+ kotlin(
+ """
+ package androidx.test
+
+ import androidx.core.os.BuildCompat
+
+ fun callIsAtLeastN() {
+ return BuildCompat.isAtLeastN()
+ }
+ """.trimIndent()
+ ),
+ gradle("""
+ dependencies {
+ implementation("androidx.core:core:1.9.0")
+ }
+ """.trimIndent()),
+ )
+
+ check(*input).expectClean()
+ }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index c4aad8a..108e776 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -386,6 +386,59 @@
}
""".trimIndent()
)
+
+ /**
+ * Contains only a few of the isAtLeastX implementations from BuildCompat for testing
+ */
+ val BuildCompat: TestFile = LintDetectorTest.java("""
+package androidx.core.os;
+
+import android.os.Build;
+import android.os.Build.VERSION;
+
+import androidx.annotation.ChecksSdkIntAtLeast;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresOptIn;
+import androidx.annotation.RestrictTo;
+
+import java.util.Locale;
+
+public class BuildCompat {
+ private BuildCompat() {}
+
+ @RestrictTo(RestrictTo.Scope.TESTS)
+ protected static boolean isAtLeastPreReleaseCodename(@NonNull String codename, @NonNull String buildCodename) {
+ if ("REL".equals(buildCodename)) {
+ return false;
+ }
+ final String buildCodenameUpper = buildCodename.toUpperCase(Locale.ROOT);
+ final String codenameUpper = codename.toUpperCase(Locale.ROOT);
+ return buildCodenameUpper.compareTo(codenameUpper) >= 0;
+ }
+
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
+ @Deprecated
+ public static boolean isAtLeastN() {
+ return VERSION.SDK_INT >= 24;
+ }
+
+ @PrereleaseSdkCheck
+ @ChecksSdkIntAtLeast(api = 32, codename = "Sv2")
+ @Deprecated
+ public static boolean isAtLeastSv2() {
+ return VERSION.SDK_INT >= 32 || (VERSION.SDK_INT >= 31 && isAtLeastPreReleaseCodename("Sv2", VERSION.CODENAME));
+ }
+
+ @PrereleaseSdkCheck
+ @ChecksSdkIntAtLeast(codename = "UpsideDownCake")
+ public static boolean isAtLeastU() {
+ return VERSION.SDK_INT >= 33 && isAtLeastPreReleaseCodename("UpsideDownCake", VERSION.CODENAME);
+ }
+
+ @RequiresOptIn
+ public @interface PrereleaseSdkCheck { }
+}
+ """.trimIndent())
/* ktlint-enable max-line-length */
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
index 2f7f395..51e6cb0 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
@@ -28,6 +28,7 @@
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -35,11 +36,16 @@
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialog;
@@ -50,6 +56,8 @@
import java.io.IOException;
import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -69,7 +77,12 @@
// Do not update the route list immediately to avoid unnatural dialog change.
private static final long UPDATE_ROUTES_DELAY_MS = 300L;
- static final int MSG_UPDATE_ROUTES = 1;
+ private static final int MSG_UPDATE_ROUTES = 1;
+ private static final int MSG_SHOW_WIFI_HINT = 2;
+ private static final int MSG_SHOW_NO_ROUTES = 3;
+
+ private static final int SHOW_WIFI_HINT_DELAY_MS = 5000;
+ private static final int SHOW_NO_ROUTES_DELAY_MS = 15000;
private final MediaRouter mRouter;
private final MediaRouterCallback mCallback;
@@ -79,6 +92,11 @@
private ArrayList<MediaRouter.RouteInfo> mRoutes;
private RouteAdapter mAdapter;
private ListView mListView;
+ private RelativeLayout mEmptyView;
+ private LinearLayout mSearchingRoutesView;
+ private FrameLayout mNoRoutesView;
+ private FrameLayout mWifiWarningView;
+ private FrameLayout mFooterView;
private boolean mAttachedToWindow;
private long mLastUpdateTime;
@SuppressWarnings({"unchecked", "deprecation"})
@@ -87,12 +105,28 @@
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_ROUTES:
- updateRoutes((List<MediaRouter.RouteInfo>) message.obj);
+ handleUpdateRoutes((List<MediaRouter.RouteInfo>) message.obj);
+ break;
+ case MSG_SHOW_WIFI_HINT:
+ handleShowNoWifiWarning();
+ break;
+ case MSG_SHOW_NO_ROUTES:
+ handleShowNoRoutes();
break;
}
}
};
+ private static final int FINDING_DEVICES = 0;
+ private static final int SHOWING_ROUTES = 1;
+ private static final int NO_DEVICES_NO_WIFI_HINT = 2;
+ private static final int NO_ROUTES = 3;
+
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FINDING_DEVICES, SHOWING_ROUTES, NO_DEVICES_NO_WIFI_HINT, NO_ROUTES})
+ private @interface MediaRouterChooserDialogState {}
+
public MediaRouteChooserDialog(@NonNull Context context) {
this(context, 0);
}
@@ -196,6 +230,20 @@
mListView.setEmptyView(findViewById(android.R.id.empty));
mTitleView = findViewById(R.id.mr_chooser_title);
+ mEmptyView = findViewById(R.id.mr_empty_view);
+ mSearchingRoutesView = findViewById(R.id.mr_chooser_searching);
+ mNoRoutesView = findViewById(R.id.mr_chooser_no_routes);
+ mWifiWarningView = findViewById(R.id.mr_chooser_wifi_warning);
+ mFooterView = findViewById(R.id.mr_chooser_footer);
+
+ TextView zeroRoutesDescription = findViewById(R.id.mr_chooser_zero_routes_description);
+ TextView wifiWarningDescription = findViewById(R.id.mr_chooser_wifi_warning_description);
+ Button doneButton = findViewById(R.id.mr_chooser_done_button);
+
+ zeroRoutesDescription.setMovementMethod(LinkMovementMethod.getInstance());
+ wifiWarningDescription.setMovementMethod(LinkMovementMethod.getInstance());
+ doneButton.setOnClickListener(view -> dismiss());
+
updateLayout();
}
@@ -210,17 +258,26 @@
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
-
mAttachedToWindow = true;
mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
refreshRoutes();
+
+ mHandler.removeMessages(MSG_SHOW_WIFI_HINT);
+ mHandler.removeMessages(MSG_SHOW_NO_ROUTES);
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_SHOW_WIFI_HINT), SHOW_WIFI_HINT_DELAY_MS);
}
@Override
public void onDetachedFromWindow() {
mAttachedToWindow = false;
+
mRouter.removeCallback(mCallback);
mHandler.removeMessages(MSG_UPDATE_ROUTES);
+ mHandler.removeMessages(MSG_SHOW_WIFI_HINT);
+ mHandler.removeMessages(MSG_SHOW_NO_ROUTES);
super.onDetachedFromWindow();
}
@@ -234,7 +291,7 @@
onFilterRoutes(routes);
Collections.sort(routes, RouteComparator.sInstance);
if (SystemClock.uptimeMillis() - mLastUpdateTime >= UPDATE_ROUTES_DELAY_MS) {
- updateRoutes(routes);
+ handleUpdateRoutes(routes);
} else {
mHandler.removeMessages(MSG_UPDATE_ROUTES);
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ROUTES, routes),
@@ -243,11 +300,105 @@
}
}
- void updateRoutes(List<MediaRouter.RouteInfo> routes) {
+ void handleUpdateRoutes(List<MediaRouter.RouteInfo> routes) {
mLastUpdateTime = SystemClock.uptimeMillis();
mRoutes.clear();
mRoutes.addAll(routes);
mAdapter.notifyDataSetChanged();
+
+ mHandler.removeMessages(MSG_SHOW_NO_ROUTES);
+ mHandler.removeMessages(MSG_SHOW_WIFI_HINT);
+
+ if (routes.isEmpty()) {
+ // When all routes are removed or disconnected
+ updateViewForState(FINDING_DEVICES);
+
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_SHOW_WIFI_HINT), SHOW_WIFI_HINT_DELAY_MS);
+ } else {
+ updateViewForState(SHOWING_ROUTES);
+ }
+ }
+
+ void handleShowNoWifiWarning() {
+ if (mRoutes.isEmpty()) {
+ updateViewForState(NO_DEVICES_NO_WIFI_HINT);
+ mHandler.removeMessages(MSG_SHOW_WIFI_HINT);
+ mHandler.removeMessages(MSG_SHOW_NO_ROUTES);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_NO_ROUTES),
+ SHOW_NO_ROUTES_DELAY_MS);
+ }
+ }
+
+ void handleShowNoRoutes() {
+ if (mRoutes.isEmpty()) {
+ updateViewForState(NO_ROUTES);
+ mHandler.removeMessages(MSG_SHOW_WIFI_HINT);
+ mHandler.removeMessages(MSG_SHOW_NO_ROUTES);
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+ mRouter.removeCallback(mCallback);
+ }
+ }
+
+ void updateViewForState(@MediaRouterChooserDialogState int state) {
+ switch (state) {
+ case NO_ROUTES:
+ updateViewForNoRoutes();
+ break;
+ case NO_DEVICES_NO_WIFI_HINT:
+ updateViewForNoDevicesNoWifiHint();
+ break;
+ case SHOWING_ROUTES:
+ updateViewForShowingRoutes();
+ break;
+ case FINDING_DEVICES:
+ updateViewForFindingDevices();
+ break;
+ }
+ }
+
+ private void updateViewForNoRoutes() {
+ setTitle(R.string.mr_chooser_zero_routes_found_title);
+ mTitleView.setVisibility(View.VISIBLE);
+ mListView.setVisibility(View.GONE);
+ mEmptyView.setVisibility(View.VISIBLE);
+ mFooterView.setVisibility(View.VISIBLE);
+ mNoRoutesView.setVisibility(View.VISIBLE);
+ mSearchingRoutesView.setVisibility(View.GONE);
+ mWifiWarningView.setVisibility(View.GONE);
+ }
+
+ private void updateViewForNoDevicesNoWifiHint() {
+ setTitle(R.string.mr_chooser_title);
+ mTitleView.setVisibility(View.VISIBLE);
+ mListView.setVisibility(View.GONE);
+ mEmptyView.setVisibility(View.VISIBLE);
+ mFooterView.setVisibility(View.GONE);
+ mNoRoutesView.setVisibility(View.GONE);
+ mSearchingRoutesView.setVisibility(View.VISIBLE);
+ mWifiWarningView.setVisibility(View.VISIBLE);
+ }
+
+ private void updateViewForShowingRoutes() {
+ setTitle(R.string.mr_chooser_title);
+ mTitleView.setVisibility(View.VISIBLE);
+ mListView.setVisibility(View.VISIBLE);
+ mEmptyView.setVisibility(View.GONE);
+ mFooterView.setVisibility(View.GONE);
+ mNoRoutesView.setVisibility(View.GONE);
+ mSearchingRoutesView.setVisibility(View.GONE);
+ mWifiWarningView.setVisibility(View.GONE);
+ }
+
+ private void updateViewForFindingDevices() {
+ setTitle(R.string.mr_chooser_title);
+ mTitleView.setVisibility(View.VISIBLE);
+ mListView.setVisibility(View.GONE);
+ mEmptyView.setVisibility(View.VISIBLE);
+ mFooterView.setVisibility(View.GONE);
+ mNoRoutesView.setVisibility(View.GONE);
+ mSearchingRoutesView.setVisibility(View.VISIBLE);
+ mWifiWarningView.setVisibility(View.GONE);
}
private static final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
@@ -288,8 +439,9 @@
return getItem(position).isEnabled();
}
+ @NonNull
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.mr_chooser_list_item, parent, false);
@@ -356,7 +508,7 @@
private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) {
// If the type of the receiver device is specified, use it.
switch (route.getDeviceType()) {
- case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
+ case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
return mTvIcon;
case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER:
return mSpeakerIcon;
@@ -407,4 +559,4 @@
return lhs.getName().compareToIgnoreCase(rhs.getName());
}
}
-}
+}
\ No newline at end of file
diff --git a/mediarouter/mediarouter/src/main/res/layout/mr_chooser_dialog.xml b/mediarouter/mediarouter/src/main/res/layout/mr_chooser_dialog.xml
index 966e42c..c8978f7 100644
--- a/mediarouter/mediarouter/src/main/res/layout/mr_chooser_dialog.xml
+++ b/mediarouter/mediarouter/src/main/res/layout/mr_chooser_dialog.xml
@@ -15,44 +15,111 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <TextView android:id="@+id/mr_chooser_title"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:minHeight="52dp"
- android:paddingTop="12dp"
- android:paddingLeft="24dp"
- android:paddingRight="24dp"
- android:gravity="center_vertical"
- android:text="@string/mr_chooser_title"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAppearance="@style/TextAppearance.MediaRouter.Title" />
- <ListView android:id="@+id/mr_chooser_list"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:paddingBottom="24dp"
- android:divider="@android:color/transparent"
- android:dividerHeight="0dp" />
- <LinearLayout android:id="@android:id/empty"
- android:layout_width="fill_parent"
- android:layout_height="240dp"
- android:orientation="vertical"
- android:paddingTop="90dp"
- android:paddingLeft="16dp"
- android:paddingRight="16dp"
- android:visibility="gone">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:text="@string/mr_chooser_searching"
- android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
- <ProgressBar android:layout_width="150dp"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:indeterminate="true"
- style="?android:attr/progressBarStyleHorizontal" />
- </LinearLayout>
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="24dp">
+
+ <TextView
+ android:id="@+id/mr_chooser_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="12dp"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:text="@string/mr_chooser_title"
+ android:textAppearance="@style/TextAppearance.MediaRouter.Title" />
+
+ <ListView
+ android:id="@+id/mr_chooser_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"
+ android:visibility="gone" />
+
+ <RelativeLayout
+ android:id="@+id/mr_empty_view"
+ android:layout_width="fill_parent"
+ android:layout_height="240dp"
+ android:visibility="visible">
+
+ <LinearLayout
+ android:id="@+id/mr_chooser_searching"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:orientation="vertical"
+ android:visibility="visible">
+
+ <ProgressBar
+ android:id="@+id/mr_chooser_search_progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="150dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true" />
+
+ <TextView
+ android:id="@+id/mr_chooser_search_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/mr_chooser_searching"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/mr_chooser_no_routes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/mr_chooser_zero_routes_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/mr_chooser_zero_routes_description"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/mr_chooser_wifi_warning"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/mr_chooser_wifi_warning_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/mr_chooser_wifi_warning_description"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/mr_chooser_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:gravity="bottom"
+ android:visibility="gone">
+
+ <Button
+ android:id="@+id/mr_chooser_done_button"
+ style="?android:attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|right"
+ android:text="@string/mr_chooser_done_button_label" />
+
+ </FrameLayout>
+
+ </RelativeLayout>
+
</LinearLayout>
diff --git a/mediarouter/mediarouter/src/main/res/values/strings.xml b/mediarouter/mediarouter/src/main/res/values/strings.xml
index 42d088d..8b54407 100644
--- a/mediarouter/mediarouter/src/main/res/values/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values/strings.xml
@@ -94,4 +94,16 @@
<!-- Placeholder text for cast dialog title view [CHAR LIMIT=50] -->
<string name="mr_cast_dialog_title_view_placeholder">No info available</string>
+
+ <!-- Title of the media route chooser dialog when zero routes have been found after searching. [CHAR_LIMIT=NONE] -->
+ <string name="mr_chooser_zero_routes_found_title">No devices available</string>
+
+ <!-- Text displayed when no routes have been found after searching. [CHAR_LIMIT=NONE] -->
+ <string name="mr_chooser_zero_routes_description">Unable to find devices, make sure your device and the Cast device are on the same Wi-Fi network and try again.</string>
+
+ <!-- Text displayed to hint making sure device and cast device are connected to the same Wi-Fi. [CHAR_LIMIT=NONE] -->
+ <string name="mr_chooser_wifi_warning_description">Make sure your device and the Cast device are on the same Wi-Fi network.</string>
+
+ <!-- The label of the Done button that closes the Cast media route chooser dialog when tapped. [CHAR_LIMIT=30] -->
+ <string name="mr_chooser_done_button_label">Done</string>
</resources>
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/DelayedView.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/DelayedView.kt
index f447748..0129f1f 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/DelayedView.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/DelayedView.kt
@@ -2,6 +2,8 @@
import android.content.Context
import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
import android.os.Build
import android.util.AttributeSet
import android.view.View
@@ -15,6 +17,11 @@
var repetitions: Int = 0
var maxReps: Int = 0
var perFrameStateData: List<JankStatsTest.FrameStateInputData> = listOf()
+ val textPaint = Paint()
+
+ init {
+ textPaint.textSize = 50f
+ }
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
override fun onDraw(canvas: Canvas) {
@@ -29,8 +36,10 @@
(((Math.random() * 127) + 128).toInt() shl 16) or
(((Math.random() * 127) + 128).toInt() shl 8) or
((Math.random() * 127) + 128).toInt()
+ textPaint.setColor(Color.BLACK)
canvas.drawColor(randomColor)
+ canvas.drawText("Frame ${repetitions - 1}", 200f, 200f, textPaint)
if (perFrameStateData.isNotEmpty()) {
val metricsState = PerformanceMetricsState.getHolderForHierarchy(this).state!!
val stateData = perFrameStateData[repetitions - 1]
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
index 5351167..5bb3c0d 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
@@ -185,15 +185,11 @@
jankStats.jankHeuristicMultiplier = 0f
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
// FrameMetrics sometimes drops a frame, so the total number of
- // jankData items might be less than NUM_FRAMES
+ // jankData items might be less than NUM_FRAMES. Check against actual
+ // number of frames received instead.
assertEquals(
- "jank frames != NUMFRAMES",
- NUM_FRAMES, latchedListener.numJankFrames
- )
- assertTrue(
- "With heuristicMultiplier 0, should be at least ${NUM_FRAMES - 1} " +
- "frames with jank data, not ${latchedListener.numJankFrames}",
- latchedListener.numJankFrames >= (NUM_FRAMES - 1)
+ "numJankFrames != numFrames",
+ latchedListener.numFrames, latchedListener.numJankFrames
)
}
@@ -275,12 +271,12 @@
frameInit.initFramePipeline()
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
+
// FrameMetrics sometimes drops a frame, so the total number of
// jankData items might be less than NUM_FRAMES
- assertTrue(
- "There should be at least ${NUM_FRAMES - 1} frames with jank data, " +
- "not ${latchedListener.jankData.size}",
- latchedListener.jankData.size >= (NUM_FRAMES - 1)
+ assertEquals("There should be ${latchedListener.numFrames} frames " +
+ "with jank data, not ${latchedListener.jankData.size}",
+ latchedListener.numFrames, latchedListener.jankData.size
)
latchedListener.reset()
@@ -307,8 +303,8 @@
metricsState.putSingleFrameState(state2.key, state2.value)
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
assertEquals(
- "frameDelay 100: There should be $NUM_FRAMES frames with jank data", NUM_FRAMES,
- latchedListener.jankData.size
+ "frameDelay 100: There should be ${latchedListener.numFrames} frames with" +
+ "jank data", latchedListener.numFrames, latchedListener.jankData.size
)
var item0: FrameData = latchedListener.jankData[0]
assertEquals("There should be 3 states at frame 0", 3,
@@ -323,7 +319,7 @@
assertThat(state2, Matchers.isIn(item0.states))
// Now test the rest of the frames, which should not include singleFrameState state2
- for (i in 1 until NUM_FRAMES) {
+ for (i in 1 until latchedListener.numFrames) {
val item = latchedListener.jankData[i]
assertEquals("There should be 2 states at frame $i", 2,
item.states.size)
@@ -503,18 +499,46 @@
runDelayTest(frameDelay = 0, numFrames = perFrameStateData.size,
latchedListener, perFrameStateData)
- assertEquals("There should be ${expectedResults.size} frames of data",
- expectedResults.size, latchedListener.jankData.size)
- for (i in 0 until expectedResults.size) {
- val testResultStates = latchedListener.jankData[i].states
- val expectedResult = expectedResults[i]
- assertEquals("There should be ${expectedResult.size} states",
- expectedResult.size, testResultStates.size)
- for (state in testResultStates) {
- assertEquals("State value not correct",
- state.value, expectedResult.get(state.key))
+ // There might be one or two dropped frames, check that we have nearly the number
+ // expected
+ assertTrue("There should be at least ${expectedResults.size - 2} frames of data",
+ (latchedListener.jankData.size > expectedResults.size - 2))
+
+ /*
+ Ideally, we would check each frame's result states against the expected results.
+ But the system sometimes drops frames, causing the jankData to be a subset of
+ the expectedResults set from above. This is fine, for testing purposes, but that
+ means we should check the current result against the expected result of this and
+ the next frame, to account for these skips. when this happens, we increment the
+ expected index since all results will be offset by that skip.
+ */
+ var expectedIndex = 0
+ var resultIndex = 0
+ while (expectedIndex < expectedResults.size) {
+ val testResultStates = latchedListener.jankData[resultIndex].states
+ // Test against this and next expected result, in case system skipped a frame
+ var matched = checkFrameStates(expectedResults[expectedIndex], testResultStates)
+ if (!matched) {
+ expectedIndex++
+ matched = checkFrameStates(expectedResults[expectedIndex], testResultStates)
+ }
+ assertTrue("States do not match at frame $expectedIndex", matched)
+ expectedIndex++
+ resultIndex++
+ }
+ }
+
+ private fun checkFrameStates(
+ expectedResult: Map<String, String>,
+ testResultStates: List<StateInfo>
+ ): Boolean {
+ if (expectedResult.size != testResultStates.size) return false
+ for (state in testResultStates) {
+ if (state.value != expectedResult.get(state.key)) {
+ return false
}
}
+ return true
}
private fun runDelayTest(
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 15a2734..d000958 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -300,6 +300,60 @@
assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("first")
}
+ @LargeTest
+ @Test
+ fun testSystemBackPressAfterPopUpToStartDestinationOffBackStack() = withNavigationActivity {
+ navController.graph = navController.createGraph("first") {
+ fragment<EmptyFragment>("first")
+ fragment<EmptyFragment>("second")
+ fragment<EmptyFragment>("third")
+ }
+ navController.navigate("second")
+ val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+ fm?.executePendingTransactions()
+
+ navController.navigate("third", navOptions {
+ popUpTo("first") { inclusive = true }
+ })
+ fm?.executePendingTransactions()
+
+ navController.navigate("first")
+ fm?.executePendingTransactions()
+
+ onBackPressedDispatcher.onBackPressed()
+
+ assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("third")
+ }
+
+ @LargeTest
+ @Test
+ fun testSystemBackPressAfterPopUpToOffBackStack() = withNavigationActivity {
+ navController.graph = navController.createGraph("first") {
+ fragment<EmptyFragment>("first")
+ fragment<EmptyFragment>("second")
+ fragment<EmptyFragment>("third")
+ fragment<EmptyFragment>("fourth")
+ }
+ navController.navigate("second")
+ val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+ fm?.executePendingTransactions()
+
+ navController.navigate("third")
+ fm?.executePendingTransactions()
+
+ navController.navigate("fourth", navOptions {
+ popUpTo("second") { inclusive = true }
+ })
+ fm?.executePendingTransactions()
+
+ navController.navigate("second")
+ fm?.executePendingTransactions()
+
+ onBackPressedDispatcher.onBackPressed()
+
+ assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("fourth")
+ }
+
private fun withNavigationActivity(
block: NavigationActivity.() -> Unit
) {
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index f92e322..8bb7b8c 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -67,7 +67,10 @@
/**
* List of entries that were popped by direct calls to popBackStack (i.e. from NavController)
*/
- private val entriesToPop = mutableSetOf<String>()
+ internal val entriesToPop: Set<String>
+ get() = (state.transitionsInProgress.value - state.backStack.value.toSet())
+ .map { it.id }
+ .toSet()
/**
* Get the back stack from the [state].
@@ -81,7 +84,6 @@
entry.id == fragment.tag
}
if (entry != null) {
- entriesToPop.remove(entry.id)
if (!state.backStack.value.contains(entry)) {
state.markTransitionComplete(entry)
}
@@ -99,7 +101,6 @@
// Once the lifecycle reaches DESTROYED, if the entry is not in the back stack, we can
// mark the transition complete
if (event == Lifecycle.Event.ON_DESTROY) {
- entriesToPop.remove(entry.id)
if (!state.backStack.value.contains(entry)) {
state.markTransitionComplete(entry)
}
@@ -150,16 +151,10 @@
// we need to make sure we still return the entries to their proper final state.
attachClearViewModel(fragment, entry, state)
if (pop) {
- // The entry has already been removed from the back stack so just remove it
- // from the list
- if (!state.backStack.value.contains(entry)) {
- // remove it so we don't falsely identify a direct call to popBackStack
- entriesToPop.remove(entry.id)
- }
// This is the case of system back where we will need to make the call to
// popBackStack. Otherwise, popBackStack was called directly and this should
// end up being a no-op.
- else if (entriesToPop.isEmpty() && fragment.isRemoving) {
+ if (entriesToPop.isEmpty() && fragment.isRemoving) {
state.popWithTransition(entry, false)
}
}
@@ -249,10 +244,6 @@
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
}
- // Add all of the entries that are going to be popped to our set of entries to pop
- poppedList.forEach {
- entriesToPop.add(it.id)
- }
state.popWithTransition(popUpTo, savedState)
}
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
index 1948415..32d7230 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
@@ -16,6 +16,8 @@
package androidx.paging
+import kotlin.test.Ignore
+import kotlin.test.Test
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
@@ -25,8 +27,6 @@
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.coroutines.EmptyCoroutineContext
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowTest.kt
index 77e0ce1..1f3cda9 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/CachedPageEventFlowTest.kt
@@ -17,6 +17,7 @@
package androidx.paging
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -32,7 +33,6 @@
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/CachingTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/CachingTest.kt
index 5eb154c..ac6e3e3 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/CachingTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/CachingTest.kt
@@ -19,6 +19,7 @@
import androidx.paging.ActiveFlowTracker.FlowType.PAGED_DATA_FLOW
import androidx.paging.ActiveFlowTracker.FlowType.PAGE_EVENT_FLOW
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,7 +38,6 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.atomic.AtomicInteger
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/ConflatedEventBusTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/ConflatedEventBusTest.kt
index c5bde13..174df10 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/ConflatedEventBusTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/ConflatedEventBusTest.kt
@@ -17,12 +17,12 @@
package androidx.paging
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
-import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ConflatedEventBusTest {
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
index 1a4637b..af77e26 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
@@ -34,12 +34,12 @@
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertSame
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertSame
import kotlin.test.assertTrue
@RunWith(Parameterized::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/DataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/DataSourceTest.kt
index bd2a9fb..b5635bb 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/DataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/DataSourceTest.kt
@@ -17,7 +17,7 @@
package androidx.paging
import com.google.common.truth.Truth.assertThat
-import org.junit.Test
+import kotlin.test.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/FailDispatcher.kt b/paging/paging-common/src/test/kotlin/androidx/paging/FailDispatcher.kt
index 9e41203..2d24170 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/FailDispatcher.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/FailDispatcher.kt
@@ -18,8 +18,8 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
-import org.junit.Assert.fail
import kotlin.coroutines.CoroutineContext
+import kotlin.test.fail
class FailDispatcher(
val string: String = "Executor expected to be unused"
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
index 35cf04d..44bd3bb 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
@@ -25,7 +25,7 @@
import androidx.paging.PageEvent.Drop
import androidx.paging.PageEvent.StaticList
import com.google.common.truth.Truth.assertThat
-import org.junit.Test
+import kotlin.test.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/FlowExtTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/FlowExtTest.kt
index 5073e746..cae22ba 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/FlowExtTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/FlowExtTest.kt
@@ -20,6 +20,7 @@
import androidx.paging.CombineSource.OTHER
import androidx.paging.CombineSource.RECEIVER
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
@@ -38,7 +39,6 @@
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
-import org.junit.Test
import kotlin.random.Random
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
index 4af5089..240507e 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
@@ -23,9 +23,9 @@
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertEquals
/**
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/HintHandlerTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/HintHandlerTest.kt
index 6ab268b..16bc08f 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/HintHandlerTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/HintHandlerTest.kt
@@ -20,6 +20,7 @@
import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -30,7 +31,6 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
index a3b431a..28b9cb4 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
@@ -16,7 +16,7 @@
package androidx.paging
-import org.junit.Test
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
index dc2c9fb..27ad27e 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -21,17 +21,17 @@
import org.mockito.kotlin.mock
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import kotlin.test.fail
@Suppress("DEPRECATION")
@RunWith(JUnit4::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/ItemSnapshotListTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/ItemSnapshotListTest.kt
index 7b6d6f9..4688ad0 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/ItemSnapshotListTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/ItemSnapshotListTest.kt
@@ -16,7 +16,7 @@
package androidx.paging
-import org.junit.Test
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
index 78598f9..536e5d4 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
@@ -28,14 +28,14 @@
import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingSource.LoadResult.Page
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
index 1ffe862..f70dde3 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
@@ -27,14 +27,14 @@
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
-import org.junit.Assert
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.Executors
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
+import kotlin.test.fail
import kotlinx.coroutines.ExperimentalCoroutinesApi
@OptIn(ExperimentalCoroutinesApi::class)
@@ -93,21 +93,21 @@
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<String>
) {
- Assert.fail("loadInitial not expected")
+ fail("loadInitial not expected")
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<String>
) {
- Assert.fail("loadAfter not expected")
+ fail("loadAfter not expected")
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<String>
) {
- Assert.fail("loadBefore not expected")
+ fail("loadBefore not expected")
}
override fun getKey(item: String) = item.hashCode()
@@ -141,21 +141,21 @@
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, String>
) {
- Assert.fail("loadInitial not expected")
+ fail("loadInitial not expected")
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<Int, String>
) {
- Assert.fail("loadBefore not expected")
+ fail("loadBefore not expected")
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Int, String>
) {
- Assert.fail("loadAfter not expected")
+ fail("loadAfter not expected")
}
}
val pagingSource = LegacyPagingSource(
@@ -391,14 +391,14 @@
callback: LoadInitialCallback<String>
) {
if (!expectInitialLoad) {
- Assert.fail("loadInitial not expected")
+ fail("loadInitial not expected")
} else {
callback.onResult(listOf(), 0)
}
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
- Assert.fail("loadRange not expected")
+ fail("loadRange not expected")
}
}
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
index 20a1116..9fd2f20 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
@@ -23,14 +23,14 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.Parameterized
+import kotlin.test.BeforeTest
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertSame
-import org.junit.Before
internal fun <T : Any> adjacentInsertEvent(
isPrepend: Boolean,
@@ -293,7 +293,7 @@
private val differ = TestPagingDataDiffer<String>(EmptyCoroutineContext)
private lateinit var pagingData: PagingData<String>
- @Before
+ @BeforeTest
fun init() {
pagingData = if (data.isNotEmpty()) {
PagingData.from(data)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
index 5006894..741f2ba 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
@@ -27,9 +27,9 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
index 62ffbb6..f02de71 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
@@ -29,6 +29,7 @@
import androidx.paging.RemoteMediatorMock.LoadEvent
import androidx.paging.TestPagingSource.Companion.LOAD_ERROR
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
@@ -66,7 +67,6 @@
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
index 1a3a4c4..5f75646 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -26,6 +26,7 @@
import androidx.paging.RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
import androidx.paging.RemoteMediator.InitializeAction.SKIP_INITIAL_REFRESH
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
@@ -51,7 +52,6 @@
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
index 6f89ef0..9728146 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
@@ -20,18 +20,18 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
import kotlinx.coroutines.test.StandardTestDispatcher
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagePresenterTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagePresenterTest.kt
index e979bcc..951c308 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagePresenterTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagePresenterTest.kt
@@ -22,9 +22,9 @@
import androidx.paging.PagePresenter.ProcessPageEventCallback
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import com.google.common.truth.Truth.assertThat
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.fail
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigBuilderTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigBuilderTest.kt
index ad107b9..c830172 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigBuilderTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigBuilderTest.kt
@@ -16,8 +16,8 @@
package androidx.paging
-import org.junit.Assert
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -29,12 +29,12 @@
val config = PagedList.Config.Builder()
.setPageSize(10)
.build()
- Assert.assertEquals(10, config.pageSize)
- Assert.assertEquals(30, config.initialLoadSizeHint)
- Assert.assertEquals(true, config.enablePlaceholders)
- Assert.assertEquals(10, config.prefetchDistance)
+ assertEquals(10, config.pageSize)
+ assertEquals(30, config.initialLoadSizeHint)
+ assertEquals(true, config.enablePlaceholders)
+ assertEquals(10, config.prefetchDistance)
@Suppress("DEPRECATION")
- Assert.assertEquals(PagedList.Config.MAX_SIZE_UNBOUNDED, config.maxSize)
+ assertEquals(PagedList.Config.MAX_SIZE_UNBOUNDED, config.maxSize)
}
@Test(expected = IllegalArgumentException::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigTest.kt
index 6746965..c34f7bf 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListConfigTest.kt
@@ -16,8 +16,8 @@
package androidx.paging
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListTest.kt
index 3d7479b..93130fe 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagedListTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagedListTest.kt
@@ -24,13 +24,13 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.Executor
import kotlin.concurrent.thread
import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.test.fail
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagedStorageTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagedStorageTest.kt
index 88d8315..d0efec19 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagedStorageTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagedStorageTest.kt
@@ -17,16 +17,16 @@
package androidx.paging
import androidx.paging.PagingSource.LoadResult.Page
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import kotlin.test.assertNotNull
@RunWith(JUnit4::class)
@@ -41,7 +41,7 @@
fun construct() {
val storage = PagedStorage(2, pageOf("a", "b"), 2)
- assertArrayEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
+ assertContentEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
assertEquals(6, storage.size)
}
@@ -52,7 +52,7 @@
val storage = PagedStorage(2, pageOf("a", "b"), 2)
storage.appendPage(pageOf("c", "d"), callback)
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ assertContentEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
verify(callback).onPageAppended(4, 2, 0)
verifyNoMoreInteractions(callback)
}
@@ -64,7 +64,7 @@
val storage = PagedStorage(2, pageOf("a", "b"), 0)
storage.appendPage(pageOf("c", "d"), callback)
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ assertContentEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
verify(callback).onPageAppended(4, 0, 2)
verifyNoMoreInteractions(callback)
}
@@ -78,14 +78,14 @@
// change 2 nulls into c, d
storage.appendPage(pageOf("c", "d"), callback)
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ assertContentEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
verify(callback).onPageAppended(4, 2, 0)
verifyNoMoreInteractions(callback)
// append e, f
storage.appendPage(pageOf("e", "f"), callback)
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
+ assertContentEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
verify(callback).onPageAppended(6, 0, 2)
verifyNoMoreInteractions(callback)
}
@@ -97,7 +97,7 @@
val storage = PagedStorage(2, pageOf("c", "d"), 2)
storage.prependPage(pageOf("a", "b"), callback)
- assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ assertContentEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
verify(callback).onPagePrepended(0, 2, 0)
verifyNoMoreInteractions(callback)
}
@@ -109,7 +109,7 @@
val storage = PagedStorage(0, pageOf("c", "d"), 2)
storage.prependPage(pageOf("a", "b"), callback)
- assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ assertContentEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
verify(callback).onPagePrepended(0, 0, 2)
verifyNoMoreInteractions(callback)
}
@@ -123,14 +123,14 @@
// change 2 nulls into c, d
storage.prependPage(pageOf("c", "d"), callback)
- assertArrayEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
+ assertContentEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
verify(callback).onPagePrepended(0, 2, 0)
verifyNoMoreInteractions(callback)
// prepend a, b
storage.prependPage(pageOf("a", "b"), callback)
- assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
+ assertContentEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
verify(callback).onPagePrepended(0, 0, 2)
verifyNoMoreInteractions(callback)
}
@@ -141,7 +141,7 @@
val storage = PagedStorage(1, pageOf("a"), 6)
storage.appendPage(pageOf("b", "c"), callback)
storage.appendPage(pageOf("d", "e", "f"), callback)
- assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+ assertContentEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
}
@Test
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingConfigTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingConfigTest.kt
index fa70f64..534d46b 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingConfigTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingConfigTest.kt
@@ -17,10 +17,10 @@
package androidx.paging
import androidx.paging.PagingConfig.Companion.MAX_SIZE_UNBOUNDED
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import kotlin.test.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@RunWith(JUnit4::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
index 893b89c..1ee318c 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -24,6 +24,7 @@
import androidx.testutils.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.ContinuationInterceptor
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
@@ -56,7 +57,6 @@
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Rule
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index bf9b097..5857c18 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -21,13 +21,13 @@
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
@RunWith(JUnit4::class)
class PagingSourceTest {
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PagingStateTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PagingStateTest.kt
index 23a69fc..070ada4 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PagingStateTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PagingStateTest.kt
@@ -17,9 +17,9 @@
package androidx.paging
import androidx.paging.PagingSource.LoadResult.Page
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
index 3c428c1..ea729a5 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
@@ -21,16 +21,16 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import java.util.concurrent.Executor
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
@Suppress("DEPRECATION")
@RunWith(JUnit4::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
index c59ebc0..775a5e9 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
@@ -35,10 +35,10 @@
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
index 88e807e..3dbe2f1 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
@@ -28,9 +28,9 @@
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertEquals
private fun <T : Any> List<PageEvent<T>>.getItems() = mapNotNull { event ->
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
index ec78e09..e85ecc9 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
@@ -25,9 +25,9 @@
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertFailsWith
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/SimpleChannelFlowTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/SimpleChannelFlowTest.kt
index 237e540..8ba4a77 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/SimpleChannelFlowTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/SimpleChannelFlowTest.kt
@@ -40,9 +40,9 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import kotlin.test.Test
import kotlin.test.fail
@RunWith(Parameterized::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/SimpleTransformLatestTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/SimpleTransformLatestTest.kt
index 7cc311e..624b8c4 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/SimpleTransformLatestTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/SimpleTransformLatestTest.kt
@@ -17,6 +17,7 @@
package androidx.paging
import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
@@ -28,7 +29,6 @@
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/SingleRunnerTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/SingleRunnerTest.kt
index 9e80ac8..b6da643 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/SingleRunnerTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/SingleRunnerTest.kt
@@ -32,11 +32,11 @@
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.Collections
import java.util.concurrent.CountDownLatch
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
index f642d6c..3db8fd2 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
@@ -16,9 +16,9 @@
package androidx.paging
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertTrue
@RunWith(JUnit4::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
index cf4ef24..3018e9d 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
@@ -16,9 +16,9 @@
package androidx.paging
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertTrue
@RunWith(JUnit4::class)
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
index b56fe139..228388e 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
@@ -16,9 +16,9 @@
package androidx.paging
-import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import kotlin.test.Test
import kotlin.test.assertTrue
@RunWith(JUnit4::class)
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt b/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
index af09d964..08550a5 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
@@ -90,27 +90,6 @@
)
}
-private class OffsettingListUpdateCallback internal constructor(
- private val offset: Int,
- private val callback: ListUpdateCallback
-) : ListUpdateCallback {
- override fun onInserted(position: Int, count: Int) {
- callback.onInserted(position + offset, count)
- }
-
- override fun onRemoved(position: Int, count: Int) {
- callback.onRemoved(position + offset, count)
- }
-
- override fun onMoved(fromPosition: Int, toPosition: Int) {
- callback.onMoved(fromPosition + offset, toPosition + offset)
- }
-
- override fun onChanged(position: Int, count: Int, payload: Any?) {
- callback.onChanged(position + offset, count, payload)
- }
-}
-
/**
* See NullPaddedDiffing.md for how this works and why it works that way :).
*
diff --git a/paging/paging-rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt b/paging/paging-rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
index 3d3c5c2..b28a8b4 100644
--- a/paging/paging-rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
+++ b/paging/paging-rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
@@ -329,10 +329,10 @@
// execute first load, represents load attempt on first paging source
//
- // using poll().run() instead of executeAll(), because executeAll() + immediate schedulers
+ // using removeFirst().run() instead of executeAll(), because executeAll() + immediate schedulers
// result in first load + subsequent loads executing immediately and we won't be able to
// assert the pagedLists/loads incrementally
- loadDispatcher.queue.poll()?.run()
+ loadDispatcher.queue.removeFirst().run()
// the load failed so there should still be only one PagedList, but the first
// pagingSource should invalidated, and the second pagingSource is created
@@ -354,7 +354,7 @@
)
// execute the load attempt on second pagingSource which succeeds
- loadDispatcher.queue.poll()?.run()
+ loadDispatcher.queue.removeFirst().run()
// ensure second pagedList created with the correct data loaded
observer.assertValueCount(2)
diff --git a/paging/paging-rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt b/paging/paging-rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
index 0a3317b..06a869e 100644
--- a/paging/paging-rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
+++ b/paging/paging-rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
@@ -331,10 +331,10 @@
// execute first load, represents load attempt on first paging source
//
- // using poll().run() instead of executeAll(), because executeAll() + immediate schedulers
+ // using removeFirst().run() instead of executeAll(), because executeAll() + immediate schedulers
// result in first load + subsequent loads executing immediately and we won't be able to
// assert the pagedLists/loads incrementally
- loadDispatcher.queue.poll()?.run()
+ loadDispatcher.queue.removeFirst().run()
// the load failed so there should still be only one PagedList, but the first
// pagingSource should invalidated, and the second pagingSource is created
@@ -356,7 +356,7 @@
)
// execute the load attempt on second pagingSource which succeeds
- loadDispatcher.queue.poll()?.run()
+ loadDispatcher.queue.removeFirst().run()
// ensure second pagedList created with the correct data loaded
observer.assertValueCount(2)
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 91eeba55..62159d25 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -31,7 +31,7 @@
}
public final class TestPager<Key, Value> {
- ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+ ctor public TestPager(androidx.paging.PagingConfig config, androidx.paging.PagingSource<Key,Value> pagingSource);
method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
method public suspend Object? getPages(kotlin.coroutines.Continuation<java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index 91eeba55..62159d25 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -31,7 +31,7 @@
}
public final class TestPager<Key, Value> {
- ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+ ctor public TestPager(androidx.paging.PagingConfig config, androidx.paging.PagingSource<Key,Value> pagingSource);
method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
method public suspend Object? getPages(kotlin.coroutines.Continuation<java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 91eeba55..62159d25 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -31,7 +31,7 @@
}
public final class TestPager<Key, Value> {
- ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+ ctor public TestPager(androidx.paging.PagingConfig config, androidx.paging.PagingSource<Key,Value> pagingSource);
method public suspend Object? append(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult<Key,Value>>);
method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
method public suspend Object? getPages(kotlin.coroutines.Continuation<java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
index 85e3efb..e650e77 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
@@ -41,12 +41,12 @@
* multi-generational Paging behavior, you must create a new [TestPager] by supplying a
* new instance of [PagingSource].
*
- * @param pagingSource the [PagingSource] to load data from.
* @param config the [PagingConfig] to configure this TestPager's loading behavior.
+ * @param pagingSource the [PagingSource] to load data from.
*/
public class TestPager<Key : Any, Value : Any>(
- private val pagingSource: PagingSource<Key, Value>,
private val config: PagingConfig,
+ private val pagingSource: PagingSource<Key, Value>,
) {
private val hasRefreshed = AtomicBoolean(false)
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
index c293ed0..c327ff5 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
@@ -50,7 +50,7 @@
val factory: PagingSourceFactory<Int, Int> =
flowOf<List<Int>>().asPagingSourceFactory(testScope)
val pagingSource = factory()
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
runTest {
val result = pager.refresh() as Page
@@ -67,7 +67,7 @@
val factory: PagingSourceFactory<Int, Int> =
flow.asPagingSourceFactory(testScope)
val pagingSource = factory()
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
runTest {
val result = pager.refresh() as Page
@@ -92,7 +92,7 @@
// first gen
val pagingSource1 = factory()
- val pager1 = TestPager(pagingSource1, CONFIG)
+ val pager1 = TestPager(CONFIG, pagingSource1)
val result1 = pager1.refresh() as Page
assertThat(result1.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
@@ -105,7 +105,7 @@
// second gen
val pagingSource2 = factory()
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
val result2 = pager2.refresh() as Page
assertThat(result2.data).containsExactlyElementsIn(
listOf(30, 31, 32, 33, 34)
@@ -125,7 +125,7 @@
advanceUntilIdle()
val pagingSource = factory()
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
val result = pager.refresh() as Page
assertThat(result.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
@@ -158,7 +158,7 @@
// factory 1 first gen
val pagingSource = factory1()
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
val result = pager.refresh() as Page
assertThat(result.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
@@ -166,7 +166,7 @@
// factory 2 first gen
val pagingSource2 = factory2()
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
val result2 = pager2.refresh() as Page
assertThat(result2.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
@@ -182,7 +182,7 @@
// factory 1 second gen
val pagingSource3 = factory1()
- val pager3 = TestPager(pagingSource3, CONFIG)
+ val pager3 = TestPager(CONFIG, pagingSource3)
val result3 = pager3.refresh() as Page
assertThat(result3.data).containsExactlyElementsIn(
listOf(30, 31, 32, 33, 34)
@@ -190,7 +190,7 @@
// factory 2 second gen
val pagingSource4 = factory2()
- val pager4 = TestPager(pagingSource4, CONFIG)
+ val pager4 = TestPager(CONFIG, pagingSource4)
val result4 = pager4.refresh() as Page
assertThat(result4.data).containsExactlyElementsIn(
listOf(30, 31, 32, 33, 34)
@@ -203,14 +203,14 @@
val factory = data.asPagingSourceFactory()
val pagingSource1 = factory()
- val pager1 = TestPager(pagingSource1, CONFIG)
+ val pager1 = TestPager(CONFIG, pagingSource1)
val refresh1 = pager1.refresh() as Page
assertThat(refresh1.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
)
val pagingSource2 = factory()
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
val refresh2 = pager2.refresh() as Page
assertThat(refresh2.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
@@ -223,12 +223,12 @@
val factory = data.asPagingSourceFactory()
val pagingSource1 = factory()
- val pager1 = TestPager(pagingSource1, CONFIG)
+ val pager1 = TestPager(CONFIG, pagingSource1)
val refresh1 = pager1.refresh() as Page
assertThat(refresh1.data).isEmpty()
val pagingSource2 = factory()
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
val refresh2 = pager2.refresh() as Page
assertThat(refresh2.data).isEmpty()
}
@@ -238,7 +238,7 @@
val data = List(20) { it }
val factory = data.asPagingSourceFactory()
val pagingSource1 = factory()
- val pager1 = TestPager(pagingSource1, CONFIG)
+ val pager1 = TestPager(CONFIG, pagingSource1)
pager1.refresh() as Page
pager1.append()
@@ -257,7 +257,7 @@
val data = List(20) { it }
val factory = data.asPagingSourceFactory()
val pagingSource1 = factory()
- val pager1 = TestPager(pagingSource1, CONFIG)
+ val pager1 = TestPager(CONFIG, pagingSource1)
pager1.refresh(initialKey = 10) as Page
pager1.prepend()
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
index 0ab9d08..e93e9d9 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceTest.kt
@@ -271,7 +271,7 @@
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
private fun runPagingSourceTest(
source: PagingSource<Int, Int> = StaticListPagingSource(DATA),
- pager: TestPager<Int, Int> = TestPager(source, CONFIG),
+ pager: TestPager<Int, Int> = TestPager(CONFIG, source),
block: suspend (pagingSource: PagingSource<Int, Int>, pager: TestPager<Int, Int>) -> Unit
) {
runTest {
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
index 72b5b48..6caaea9 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
@@ -39,7 +39,7 @@
@Test
fun refresh_nullKey() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val result = pager.refresh(null) as LoadResult.Page
@@ -51,7 +51,7 @@
@Test
fun refresh_withInitialKey() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val result = pager.refresh(50) as LoadResult.Page
@@ -63,7 +63,7 @@
@Test
fun refresh_returnError() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
source.errorNextLoad = true
@@ -78,7 +78,7 @@
@Test
fun refresh_returnInvalid() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
source.nextLoadResult = LoadResult.Invalid()
@@ -93,7 +93,7 @@
@Test
fun refresh_invalidPagingSource() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
source.invalidate()
@@ -108,7 +108,7 @@
@Test
fun refresh_getLastLoadedPage() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val page: LoadResult.Page<Int, Int>? = pager.run {
@@ -123,7 +123,7 @@
@Test
fun getLastLoadedPage_afterInvalidPagingSource() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val page = pager.run {
@@ -141,7 +141,7 @@
@Test
fun refresh_getPages() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val pages = pager.run {
@@ -160,7 +160,7 @@
@Test
fun getPages_multiplePages() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
pager.run {
refresh(20)
@@ -179,7 +179,7 @@
@Test
fun getPages_fromEmptyList() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val pages = pager.getPages()
assertThat(pages).isEmpty()
}
@@ -187,7 +187,7 @@
@Test
fun getPages_afterInvalidPagingSource() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
val pages = pager.run {
@@ -209,7 +209,7 @@
@Test
fun getPages_multiThread() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
var pages: List<LoadResult.Page<Int, Int>>? = null
val job = launch {
@@ -250,7 +250,7 @@
@Test
fun multipleRefresh_onSinglePager_throws() {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
runTest {
pager.run {
@@ -269,7 +269,7 @@
@Test
fun multipleRefresh_onMultiplePagers() = runTest {
val source1 = TestPagingSource()
- val pager1 = TestPager(source1, CONFIG)
+ val pager1 = TestPager(CONFIG, source1)
// first gen
val result1 = pager1.run {
@@ -280,7 +280,7 @@
// second gen
val source2 = TestPagingSource()
- val pager2 = TestPager(source2, CONFIG)
+ val pager2 = TestPager(CONFIG, source2)
val result2 = pager2.run {
refresh()
@@ -292,7 +292,7 @@
@Test
fun simpleAppend() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val result = pager.run {
refresh(null)
@@ -311,7 +311,7 @@
@Test
fun simplePrepend() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val result = pager.run {
refresh(30)
@@ -333,7 +333,7 @@
@Test
fun append_beforeRefresh_throws() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
assertFailsWith<IllegalStateException> {
pager.append()
}
@@ -342,7 +342,7 @@
@Test
fun prepend_beforeRefresh_throws() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
assertFailsWith<IllegalStateException> {
pager.prepend()
}
@@ -351,7 +351,7 @@
@Test
fun append_invalidPagingSource() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val result = pager.run {
refresh()
@@ -367,7 +367,7 @@
@Test
fun prepend_invalidPagingSource() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val result = pager.run {
refresh(initialKey = 20)
@@ -383,7 +383,7 @@
@Test
fun consecutive_append() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
pager.run {
refresh(20)
@@ -403,7 +403,7 @@
@Test
fun consecutive_prepend() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
pager.run {
refresh(20)
@@ -427,7 +427,7 @@
@Test
fun append_then_prepend() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
pager.run {
refresh(20)
@@ -450,7 +450,7 @@
@Test
fun prepend_then_append() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
pager.run {
refresh(20)
@@ -473,7 +473,7 @@
@Test
fun multiThread_loads() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
// load operations upon completion add an int to the list.
// after all loads complete, we assert the order that the ints were added.
val loadOrder = mutableListOf<Int>()
@@ -511,7 +511,7 @@
@Test
fun multiThread_operations() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
// operations upon completion add an int to the list.
// after all operations complete, we assert the order that the ints were added.
val loadOrder = mutableListOf<Int>()
@@ -559,7 +559,7 @@
@Test
fun getPagingStateWithAnchorPosition_placeHoldersEnabled() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val state = pager.run {
refresh(20)
@@ -583,7 +583,7 @@
)
)
val source2 = TestPagingSource()
- val pager2 = TestPager(source, CONFIG)
+ val pager2 = TestPager(CONFIG, source)
val page = pager2.run {
refresh(source2.getRefreshKey(state))
}
@@ -598,7 +598,7 @@
initialLoadSize = 5,
enablePlaceholders = false
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
val state = pager.run {
refresh(20)
@@ -621,7 +621,7 @@
)
)
val source2 = TestPagingSource()
- val pager2 = TestPager(source, CONFIG)
+ val pager2 = TestPager(CONFIG, source)
val page = pager2.run {
refresh(source2.getRefreshKey(state))
}
@@ -634,7 +634,7 @@
@Test
fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithPlaceholders() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val msg = assertFailsWith<IllegalStateException> {
pager.run {
@@ -661,12 +661,12 @@
fun getPagingStateWithAnchorPosition_indexOutOfBoundsWithoutPlaceholders() = runTest {
val source = TestPagingSource()
val pager = TestPager(
- source,
PagingConfig(
pageSize = 3,
initialLoadSize = 5,
enablePlaceholders = false
- )
+ ),
+ source
)
val msg = assertFailsWith<IllegalStateException> {
@@ -694,7 +694,7 @@
@Test
fun getPagingStateWithAnchorLookup_placeHoldersEnabled() = runTest {
val source = TestPagingSource()
- val pager = TestPager(source, CONFIG)
+ val pager = TestPager(CONFIG, source)
val state = pager.run {
refresh(20)
@@ -718,7 +718,7 @@
)
// use state to getRefreshKey
val source2 = TestPagingSource()
- val pager2 = TestPager(source, CONFIG)
+ val pager2 = TestPager(CONFIG, source)
val page = pager2.run {
refresh(source2.getRefreshKey(state))
}
@@ -733,7 +733,7 @@
initialLoadSize = 5,
enablePlaceholders = false
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
val state = pager.run {
refresh(20)
@@ -757,7 +757,7 @@
)
// use state to getRefreshKey
val source2 = TestPagingSource()
- val pager2 = TestPager(source, CONFIG)
+ val pager2 = TestPager(CONFIG, source)
val page = pager2.run {
refresh(source2.getRefreshKey(state))
}
@@ -775,7 +775,7 @@
initialLoadSize = 5,
enablePlaceholders = false
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
val msg = assertFailsWith<IllegalArgumentException> {
pager.run {
@@ -801,7 +801,7 @@
enablePlaceholders = false,
maxSize = 10
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
pager.run {
refresh(20)
prepend()
@@ -833,7 +833,7 @@
enablePlaceholders = false,
maxSize = 10
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
pager.run {
refresh(20)
append()
@@ -864,7 +864,7 @@
enablePlaceholders = false,
maxSize = 10
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
pager.run {
refresh(20)
append()
@@ -896,7 +896,7 @@
maxSize = 5,
prefetchDistance = 2
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
pager.refresh(20)
assertThat(pager.getPages()).containsExactlyElementsIn(
listOf(
@@ -931,7 +931,7 @@
maxSize = 3,
prefetchDistance = 1
)
- val pager = TestPager(source, config)
+ val pager = TestPager(config, source)
val result = pager.refresh() as LoadResult.Page
assertThat(result.data).containsExactlyElementsIn(
listOf(0, 1, 2, 3, 4)
diff --git a/profileinstaller/integration-tests/init-macrobenchmark/build.gradle b/profileinstaller/integration-tests/init-macrobenchmark/build.gradle
index 0d054c7..7088271 100644
--- a/profileinstaller/integration-tests/init-macrobenchmark/build.gradle
+++ b/profileinstaller/integration-tests/init-macrobenchmark/build.gradle
@@ -16,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -25,24 +25,21 @@
minSdkVersion 23
}
namespace "androidx.profileinstaller.integration.macrobenchmark"
+ targetProjectPath = ":profileinstaller:integration-tests:init-macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":profileinstaller:profileinstaller"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":profileinstaller:integration-tests:init-macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":profileinstaller:integration-tests:init-macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":profileinstaller:profileinstaller"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
}
diff --git a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/init-macrobenchmark/src/main/AndroidManifest.xml
similarity index 60%
copy from benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
copy to profileinstaller/integration-tests/init-macrobenchmark/src/main/AndroidManifest.xml
index 37fa920..5b41847 100644
--- a/benchmark/integration-tests/test-module-sample/src/main/AndroidManifest.xml
+++ b/profileinstaller/integration-tests/init-macrobenchmark/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2021 The Android Open Source Project
+ ~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,12 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <queries>
- <!--
- Enables querying application info with packageManager.getApplicationInfo, to verify
- the target package is present
- -->
- <package android:name="androidx.benchmark.integration.macrobenchmark.target" />
- </queries>
-</manifest>
+<manifest />
diff --git a/profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt b/profileinstaller/integration-tests/init-macrobenchmark/src/main/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
similarity index 100%
rename from profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
rename to profileinstaller/integration-tests/init-macrobenchmark/src/main/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
diff --git a/room/integration-tests/incremental-annotation-processing/src/test/test-data/simple-project/src/main/AndroidManifest.xml b/room/integration-tests/incremental-annotation-processing/src/test/test-data/simple-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..809c0d9
--- /dev/null
+++ b/room/integration-tests/incremental-annotation-processing/src/test/test-data/simple-project/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest/>
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1e3e702
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest/>
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1e3e702
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest/>
\ No newline at end of file
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index 09a9564..e4bcd8b 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -573,7 +573,7 @@
val refreshKey = 85 - (15 / 2)
val pagingSource2 = LimitOffsetPagingSourceImpl(database)
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
val result = pager2.refresh(initialKey = refreshKey) as LoadResult.Page
// database should only have 40 items left. Refresh key is invalid at this point
@@ -616,7 +616,7 @@
dao.deleteTestItems(0, 29)
val pagingSource2 = LimitOffsetPagingSourceImpl(database)
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
// assume user was viewing first few items with anchorPosition = 0 and refresh key
// clips to 0
val result = pager2.refresh(initialKey = 0) as LoadResult.Page
@@ -648,7 +648,7 @@
dao.deleteTestItems(0, 94)
val pagingSource2 = LimitOffsetPagingSourceImpl(database)
- val pager2 = TestPager(pagingSource2, CONFIG)
+ val pager2 = TestPager(CONFIG, pagingSource2)
// assume user was viewing first few items with anchorPosition = 0 and refresh key
// clips to 0
val result = pager2.refresh(initialKey = 0) as LoadResult.Page
@@ -682,7 +682,7 @@
) -> Unit
) {
runBlocking {
- block(TestPager(pagingSource, config), pagingSource)
+ block(TestPager(config, pagingSource), pagingSource)
}
}
}
@@ -736,7 +736,7 @@
@Test
fun invalid_append() = runTest {
val pagingSource = LimitOffsetPagingSourceImpl(db)
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
dao.addAllItems(ITEMS_LIST)
val result = pager.refresh() as LoadResult.Page
@@ -769,7 +769,7 @@
@Test
fun invalid_prepend() = runTest {
val pagingSource = LimitOffsetPagingSourceImpl(db)
- val pager = TestPager(pagingSource, CONFIG)
+ val pager = TestPager(CONFIG, pagingSource)
dao.addAllItems(ITEMS_LIST)
val result = pager.refresh(initialKey = 20) as LoadResult.Page
diff --git a/settings.gradle b/settings.gradle
index 9abd3a2..0f11a3a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -472,7 +472,6 @@
includeProject(":benchmark:integration-tests:dry-run-benchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
-includeProject(":benchmark:integration-tests:test-module-sample", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:startup-benchmark", [BuildType.MAIN])
includeProject(":biometric:biometric", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx", [BuildType.MAIN])
@@ -503,6 +502,7 @@
includeProject(":camera:camera-video", [BuildType.CAMERA])
includeProject(":camera:camera-view", [BuildType.CAMERA])
includeProject(":camera:camera-viewfinder", [BuildType.CAMERA])
+includeProject(":camera:camera-viewfinder-compose", [BuildType.CAMERA])
includeProject(":camera:camera-viewfinder-core", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-avsync", "camera/integration-tests/avsynctestapp", [BuildType.CAMERA])
includeProject(":camera:integration-tests:camera-testapp-camera2-pipe", "camera/integration-tests/camerapipetestapp", [BuildType.CAMERA])
diff --git a/slice/slice-benchmark/build.gradle b/slice/slice-benchmark/build.gradle
index e353c79..1e151d0 100644
--- a/slice/slice-benchmark/build.gradle
+++ b/slice/slice-benchmark/build.gradle
@@ -42,10 +42,14 @@
androidx {
name = "Slices Benchmarks"
publish = Publish.NONE // Library is deprecated pending removal.
- disableDeviceTests = true
mavenVersion = LibraryVersions.SLICE_BENCHMARK
inceptionYear = "2018"
description = "RecyclerView Benchmarks"
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slice/slice-builders-ktx/build.gradle b/slice/slice-builders-ktx/build.gradle
index 7ef627a..cbd1af6 100644
--- a/slice/slice-builders-ktx/build.gradle
+++ b/slice/slice-builders-ktx/build.gradle
@@ -47,11 +47,15 @@
androidx {
name = "Slice builders KTX"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
- disableDeviceTests = true // Pending removal, don't run tests.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE_BUILDERS_KTX
inceptionYear = "2018"
description = "A set of Kotlin extension methods built on top of slice-builders APIs."
failOnDeprecationWarnings = false
legacyDisableKotlinStrictApiMode = true
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
diff --git a/slice/slice-builders/build.gradle b/slice/slice-builders/build.gradle
index d0ba1a4..a8c0dbb 100644
--- a/slice/slice-builders/build.gradle
+++ b/slice/slice-builders/build.gradle
@@ -33,12 +33,16 @@
androidx {
name = "Slice builders"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
- disableDeviceTests = true // Pending removal, don't run tests.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
inceptionYear = "2017"
description = "A set of builders to create templates using SliceProvider APIs"
failOnDeprecationWarnings = false
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slice/slice-core/build.gradle b/slice/slice-core/build.gradle
index 2bab0a9..03a0441 100644
--- a/slice/slice-core/build.gradle
+++ b/slice/slice-core/build.gradle
@@ -39,12 +39,16 @@
androidx {
name = "Common utilities for slices"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
- disableDeviceTests = true // Pending removal, don't run tests.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
inceptionYear = "2017"
description = "The slices core library provides utilities for the slices view and provider libraries"
failOnDeprecationWarnings = false
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slice/slice-remotecallback/build.gradle b/slice/slice-remotecallback/build.gradle
index 56db28d..edd99ea 100644
--- a/slice/slice-remotecallback/build.gradle
+++ b/slice/slice-remotecallback/build.gradle
@@ -39,12 +39,16 @@
androidx {
name = "Slice Remote Callback"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
- disableDeviceTests = true // Pending removal, don't run tests.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE_REMOTECALLBACK
inceptionYear = "2019"
description = "A library that handles PendingIntents in slices as remote callbacks"
failOnDeprecationWarnings = false
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index ff04df0..a7330d5 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -41,11 +41,15 @@
name = "Slice test code"
type = LibraryType.INTERNAL_TEST_LIBRARY
publish = Publish.NONE // Library is deprecated pending removal.
- disableDeviceTests = true
mavenVersion = LibraryVersions.SLICE
inceptionYear = "2017"
description = "A library that holds common code for testing slices"
failOnDeprecationWarnings = false
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slice/slice-view/build.gradle b/slice/slice-view/build.gradle
index 5dc4379..8f8c858 100644
--- a/slice/slice-view/build.gradle
+++ b/slice/slice-view/build.gradle
@@ -43,12 +43,16 @@
androidx {
name = "Slice views"
publish = Publish.SNAPSHOT_ONLY // Library is deprecated pending removal.
- disableDeviceTests = true // Pending removal, don't run tests.
runApiTasks = new RunApiTasks.Yes() // Pending removal, but keep API files for now.
mavenVersion = LibraryVersions.SLICE
inceptionYear = "2017"
description = "A library that handles rendering of slice content into supported templates"
failOnDeprecationWarnings = false
+
+ deviceTests {
+ // Pending removal, don't run tests.
+ enabled = false
+ }
}
android {
diff --git a/slidingpanelayout/slidingpanelayout/api/current.txt b/slidingpanelayout/slidingpanelayout/api/current.txt
index 9340904..eecf512 100644
--- a/slidingpanelayout/slidingpanelayout/api/current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/current.txt
@@ -6,6 +6,7 @@
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void addSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method protected boolean canScroll(android.view.View, boolean, int, int, int);
method @Deprecated public boolean canSlide();
method public void close();
@@ -19,6 +20,7 @@
method public void open();
method public boolean openPane();
method public void removePanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void removeSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method @Deprecated public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
@@ -61,5 +63,9 @@
method public void onPanelSlide(android.view.View, float);
}
+ public static interface SlidingPaneLayout.SlideableStateListener {
+ method public void onSlideableStateChanged(boolean);
+ }
+
}
diff --git a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
index 9340904..eecf512 100644
--- a/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/public_plus_experimental_current.txt
@@ -6,6 +6,7 @@
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void addSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method protected boolean canScroll(android.view.View, boolean, int, int, int);
method @Deprecated public boolean canSlide();
method public void close();
@@ -19,6 +20,7 @@
method public void open();
method public boolean openPane();
method public void removePanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void removeSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method @Deprecated public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
@@ -61,5 +63,9 @@
method public void onPanelSlide(android.view.View, float);
}
+ public static interface SlidingPaneLayout.SlideableStateListener {
+ method public void onSlideableStateChanged(boolean);
+ }
+
}
diff --git a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
index 9340904..eecf512 100644
--- a/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
+++ b/slidingpanelayout/slidingpanelayout/api/restricted_current.txt
@@ -6,6 +6,7 @@
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?);
ctor public SlidingPaneLayout(android.content.Context, android.util.AttributeSet?, int);
method public void addPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void addSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method protected boolean canScroll(android.view.View, boolean, int, int, int);
method @Deprecated public boolean canSlide();
method public void close();
@@ -19,6 +20,7 @@
method public void open();
method public boolean openPane();
method public void removePanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener);
+ method public void removeSlideableStateListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener);
method @Deprecated public void setCoveredFadeColor(@ColorInt int);
method public final void setLockMode(int);
method @Deprecated public void setPanelSlideListener(androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener?);
@@ -61,5 +63,9 @@
method public void onPanelSlide(android.view.View, float);
}
+ public static interface SlidingPaneLayout.SlideableStateListener {
+ method public void onSlideableStateChanged(boolean);
+ }
+
}
diff --git a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/SlideableStateListenerTest.kt b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/SlideableStateListenerTest.kt
new file mode 100644
index 0000000..490a911
--- /dev/null
+++ b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/SlideableStateListenerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.slidingpanelayout.widget
+
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.slidingpanelayout.test.R
+import androidx.slidingpanelayout.widget.SlidingPaneLayout.SlideableStateListener
+import androidx.slidingpanelayout.widget.helpers.TestActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SlideableStateListenerTest {
+
+ @After
+ public fun tearDown() {
+ TestActivity.onActivityCreated = {}
+ }
+
+ @Test
+ fun testAddSlideableStateListener() {
+ var isSlideableCalled = false
+ TestActivity.onActivityCreated = { activity ->
+ val container = FrameLayout(activity)
+ val layout = activity.layoutInflater.inflate(
+ R.layout.activity_test_layout, null, false
+ )
+ container.addView(
+ layout,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ val slidingPaneLayout = container
+ .findViewById<SlidingPaneLayout>(R.id.sliding_pane_layout)
+ slidingPaneLayout.addSlideableStateListener { _ ->
+ isSlideableCalled = true
+ }
+
+ activity.setContentView(container)
+ }
+
+ with(ActivityScenario.launch(TestActivity::class.java)) {
+ assertWithMessage(
+ "isSlideable should be called when measuring the layout has completed")
+ .that(isSlideableCalled)
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun testRemoveSlideableStateListener() {
+ var isSlideableCalled: Boolean? = null
+
+ TestActivity.onActivityCreated = { activity ->
+ val container = FrameLayout(activity)
+ val layout = activity.layoutInflater.inflate(
+ R.layout.activity_test_layout, null, false
+ )
+ container.addView(
+ layout,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ )
+ val slideableStateListener = SlideableStateListener {
+ isSlideableCalled = true
+ }
+
+ val slidingPaneLayout = container
+ .findViewById<SlidingPaneLayout>(R.id.sliding_pane_layout)
+ slidingPaneLayout.addSlideableStateListener(slideableStateListener)
+
+ activity.setContentView(container)
+ slidingPaneLayout.removeSlideableStateListener(slideableStateListener)
+ }
+
+ with(ActivityScenario.launch(TestActivity::class.java)) {
+ assertWithMessage("isSlideable should not be called if the listener has been removed")
+ .that(isSlideableCalled)
+ .isNull()
+ }
+ }
+}
\ No newline at end of file
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 27478c9..4c826d2 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -178,6 +178,9 @@
private float mInitialMotionX;
private float mInitialMotionY;
+ private final List<SlideableStateListener> mSlideableStateListeners =
+ new CopyOnWriteArrayList<>();
+
private final List<PanelSlideListener> mPanelSlideListeners = new CopyOnWriteArrayList<>();
private @Nullable PanelSlideListener mPanelSlideListener;
@@ -249,6 +252,20 @@
}
/**
+ * Listener to whether the SlidingPaneLayout is slideable or is a fixed width.
+ */
+ public interface SlideableStateListener {
+
+ /**
+ * Called when onMeasure has measured out the total width of the added layouts
+ * within SlidingPaneLayout
+ * @param isSlideable Returns true if the current SlidingPaneLayout has the ability to
+ * slide, returns false if the SlidingPaneLayout is a fixed width.
+ */
+ void onSlideableStateChanged(boolean isSlideable);
+ }
+
+ /**
* Listener for monitoring events about sliding panes.
*/
public interface PanelSlideListener {
@@ -436,6 +453,25 @@
}
/**
+ * Adds the specified listener to the list of listeners that will be notified of sliding
+ * state events.
+ * @param listener Listener to notify when sliding state events occur.
+ * @see #removeSlideableStateListener(SlideableStateListener)
+ */
+ public void addSlideableStateListener(@NonNull SlideableStateListener listener) {
+ mSlideableStateListeners.add(listener);
+ }
+
+ /**
+ * Removes the specified listener from the list of listeners that will be notified of sliding
+ * state events.
+ * @param listener Listener to notify when sliding state events occur
+ */
+ public void removeSlideableStateListener(@NonNull SlideableStateListener listener) {
+ mSlideableStateListeners.remove(listener);
+ }
+
+ /**
* Adds the specified listener to the list of listeners that will be notified of
* panel slide events.
*
@@ -777,7 +813,12 @@
final int measuredHeight = layoutHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(measuredWidth, measuredHeight);
- mCanSlide = canSlide;
+ if (canSlide != mCanSlide) {
+ mCanSlide = canSlide;
+ for (SlideableStateListener listener : mSlideableStateListeners) {
+ listener.onSlideableStateChanged(mCanSlide);
+ }
+ }
if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) {
// Cancel scrolling in progress, it's no longer relevant.
@@ -1056,6 +1097,15 @@
}
/**
+ * @return true if content in this layout can be slid open and closed
+ * @deprecated Renamed to {@link #isSlideable()} - this method is going away soon!
+ */
+ @Deprecated
+ public boolean canSlide() {
+ return mCanSlide;
+ }
+
+ /**
* @deprecated Renamed to {@link #closePane()} - this method is going away soon!
*/
@Deprecated
@@ -1094,15 +1144,6 @@
}
/**
- * @return true if content in this layout can be slid open and closed
- * @deprecated Renamed to {@link #isSlideable()} - this method is going away soon!
- */
- @Deprecated
- public boolean canSlide() {
- return mCanSlide;
- }
-
- /**
* Check if both the list and detail view panes in this layout can fully fit side-by-side. If
* not, the content pane has the capability to slide back and forth. Note that the lock mode
* is not taken into account in this method. This method is typically used to determine
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
index 67decb1..da92d0f 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
@@ -22,6 +22,7 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
@@ -34,9 +35,14 @@
import androidx.test.uiautomator.Until;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.function.ThrowingRunnable;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
import org.junit.runner.RunWith;
+import java.io.ByteArrayOutputStream;
+
@RunWith(AndroidJUnit4.class)
public abstract class BaseTest {
@@ -45,6 +51,20 @@
protected static final int DEFAULT_FLAGS =
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK;
+ // Dumps the UI hierarchy to logcat on failure.
+ @Rule
+ public TestWatcher mDumpHierarchyWatcher = new TestWatcher() {
+ @Override
+ protected void failed(Throwable t, Description description) {
+ try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+ mDevice.dumpWindowHierarchy(stream);
+ Log.w(description.getTestClass().getSimpleName(), stream.toString());
+ } catch (Exception e) {
+ Log.e(description.getTestClass().getSimpleName(), "Failed to dump hierarchy", e);
+ }
+ }
+ };
+
protected UiDevice mDevice;
@Before
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
index f842714..89114369 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
@@ -42,3 +42,6 @@
fun <T> assertThat(actual: Iterable<T>?): IterableSubject<T> =
IterableSubject(actual)
+
+fun <K, V> assertThat(actual: Map<K, V>?): MapSubject<K, V> =
+ MapSubject(actual)
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
new file mode 100644
index 0000000..c24141c
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.kruth
+
+import kotlin.test.fail
+
+class MapSubject<K, V>(actual: Map<K, V>?) : Subject<Map<K, V>>(actual) {
+
+ /** Fails if the map is not empty. */
+ fun isEmpty() {
+ requireNonNull(actual) { "Expected to be empty, but was null" }
+
+ if (actual.isNotEmpty()) {
+ fail("Expected to be empty, but was $actual")
+ }
+ }
+}
diff --git a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
new file mode 100644
index 0000000..586efc9
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.kruth
+
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class MapSubjectTest {
+
+ @Test
+ fun isEmpty() {
+ assertThat(mapOf<Any, Any>()).isEmpty()
+ }
+
+ @Test
+ fun isEmptyWithFailure() {
+ assertFailsWith<AssertionError> {
+ assertThat(mapOf(1 to 5)).isEmpty()
+ }
+ }
+}
diff --git a/testutils/testutils-ktx/build.gradle b/testutils/testutils-ktx/build.gradle
index 499d1ed..21ef343d 100644
--- a/testutils/testutils-ktx/build.gradle
+++ b/testutils/testutils-ktx/build.gradle
@@ -15,18 +15,35 @@
*/
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
- id("kotlin")
}
-dependencies {
- api(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
- api(libs.kotlinCoroutinesTest)
- api(libs.junit)
+androidXMultiplatform {
+ ios()
+ js {
+ nodejs()
+ binaries.executable()
+ }
+ jvm()
+ linux()
+ mac()
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ api(libs.kotlinCoroutinesTest)
+ }
+ }
+ jvmMain {
+ dependencies {
+ api(libs.junit)
+ }
+ }
+ }
}
androidx {
diff --git a/testutils/testutils-ktx/src/main/java/androidx/testutils/DirectDispatcher.kt b/testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/DirectDispatcher.kt
similarity index 95%
rename from testutils/testutils-ktx/src/main/java/androidx/testutils/DirectDispatcher.kt
rename to testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/DirectDispatcher.kt
index 6c961b7..5942228 100644
--- a/testutils/testutils-ktx/src/main/java/androidx/testutils/DirectDispatcher.kt
+++ b/testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/DirectDispatcher.kt
@@ -17,6 +17,7 @@
package androidx.testutils
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext
object DirectDispatcher : CoroutineDispatcher() {
diff --git a/testutils/testutils-ktx/src/main/java/androidx/testutils/TestDispatcher.kt b/testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/TestDispatcher.kt
similarity index 81%
rename from testutils/testutils-ktx/src/main/java/androidx/testutils/TestDispatcher.kt
rename to testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/TestDispatcher.kt
index be8b54f..9769c3a 100644
--- a/testutils/testutils-ktx/src/main/java/androidx/testutils/TestDispatcher.kt
+++ b/testutils/testutils-ktx/src/commonMain/kotlin/androidx/testutils/TestDispatcher.kt
@@ -17,23 +17,23 @@
package androidx.testutils
import kotlinx.coroutines.CoroutineDispatcher
-import java.util.concurrent.ConcurrentLinkedQueue
+import kotlinx.coroutines.Runnable
import kotlin.coroutines.CoroutineContext
/**
* [CoroutineDispatcher] which keeps track of all its queued jobs.
*/
class TestDispatcher : CoroutineDispatcher() {
- val queue = ConcurrentLinkedQueue<Runnable>()
+ val queue = ArrayDeque<Runnable>()
override fun dispatch(context: CoroutineContext, block: Runnable) {
queue.add(block)
}
fun executeAll() {
- do {
- val runnable = queue.poll()
- runnable?.run()
- } while (runnable != null)
+ while (queue.isNotEmpty()) {
+ val runnable = queue.removeFirst()
+ runnable.run()
+ }
}
}
diff --git a/testutils/testutils-ktx/src/main/java/androidx/testutils/MainDispatcherRule.kt b/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.kt
similarity index 100%
rename from testutils/testutils-ktx/src/main/java/androidx/testutils/MainDispatcherRule.kt
rename to testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.kt
diff --git a/testutils/testutils-ktx/src/main/java/androidx/testutils/VerifyWithPolling.kt b/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt
similarity index 93%
rename from testutils/testutils-ktx/src/main/java/androidx/testutils/VerifyWithPolling.kt
rename to testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt
index c6155b2..258a218 100644
--- a/testutils/testutils-ktx/src/main/java/androidx/testutils/VerifyWithPolling.kt
+++ b/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt
@@ -16,8 +16,10 @@
package androidx.testutils
+import java.lang.SuppressWarnings
import org.junit.Assert
+@SuppressWarnings("BanThreadSleep")
fun verifyWithPolling(
message: String,
periodMs: Long,
diff --git a/tv/samples/src/main/java/androidx/tv/samples/ButtonSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/ButtonSamples.kt
new file mode 100644
index 0000000..0b3501b
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/ButtonSamples.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.tv.material3.Button
+import androidx.tv.material3.ButtonDefaults
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
+import androidx.tv.material3.OutlinedButton
+import androidx.tv.material3.Text
+
+@Sampled
+@Composable
+fun ButtonSample() {
+ Button(onClick = { }) {
+ Text("Button")
+ }
+}
+
+@Sampled
+@Composable
+fun OutlinedButtonSample() {
+ OutlinedButton(onClick = {}) {
+ Text("Outlined Button")
+ }
+}
+
+@Sampled
+@Composable
+fun LikeButtonSample() {
+ Button(
+ onClick = { /* Do something! */ },
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding
+ ) {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "Localized description",
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("Like")
+ }
+}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CardLayoutSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CardLayoutSamples.kt
new file mode 100644
index 0000000..c4eed33
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/CardLayoutSamples.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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.CardLayoutDefaults
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.StandardCardLayout
+import androidx.tv.material3.Text
+import androidx.tv.material3.WideCardLayout
+
+@Sampled
+@Composable
+fun StandardCardLayoutSample() {
+ StandardCardLayout(
+ modifier = Modifier.size(150.dp, 120.dp),
+ imageCard = { interactionSource ->
+ CardLayoutDefaults.ImageCard(
+ onClick = { },
+ interactionSource = interactionSource
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(Color.Blue)
+ )
+ }
+ },
+ title = { Text("Standard Card") }
+ )
+}
+
+@Sampled
+@Composable
+fun WideCardLayoutSample() {
+ WideCardLayout(
+ modifier = Modifier.size(180.dp, 100.dp),
+ imageCard = { interactionSource ->
+ CardLayoutDefaults.ImageCard(
+ onClick = { },
+ interactionSource = interactionSource
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(90.dp)
+ .background(Color.Blue)
+ )
+ }
+ },
+ title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+ )
+}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CardSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CardSamples.kt
new file mode 100644
index 0000000..ae8440b
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/CardSamples.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Card
+import androidx.tv.material3.ClassicCard
+import androidx.tv.material3.CompactCard
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Text
+import androidx.tv.material3.WideClassicCard
+
+@Sampled
+@Composable
+fun CardSample() {
+ Card(
+ modifier = Modifier.size(150.dp, 120.dp),
+ onClick = { }
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ Text(
+ text = "Card",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun ClassicCardSample() {
+ ClassicCard(
+ modifier = Modifier.size(150.dp, 120.dp),
+ image = {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(Color.Blue)
+ )
+ },
+ title = {
+ Text("Classic Card")
+ },
+ contentPadding = PaddingValues(8.dp),
+ onClick = { }
+ )
+}
+
+@Sampled
+@Composable
+fun CompactCardSample() {
+ CompactCard(
+ modifier = Modifier.size(150.dp, 120.dp),
+ image = {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(Color.Blue)
+ )
+ },
+ title = {
+ Text(
+ text = "Compact Card",
+ modifier = Modifier.padding(8.dp)
+ )
+ },
+ onClick = { }
+ )
+}
+
+@Sampled
+@Composable
+fun WideClassicCardSample() {
+ WideClassicCard(
+ modifier = Modifier.size(180.dp, 100.dp),
+ image = {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .background(Color.Blue)
+ )
+ },
+ title = {
+ Text(
+ text = "Wide Classic Card",
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ },
+ contentPadding = PaddingValues(8.dp),
+ onClick = { }
+ )
+}
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CheckboxSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CheckboxSamples.kt
new file mode 100644
index 0000000..f6af570
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/CheckboxSamples.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.Checkbox
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+
+@Sampled
+@Composable
+fun CheckboxSample() {
+ Checkbox(checked = true, onCheckedChange = { })
+}
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/IconButtonSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/IconButtonSamples.kt
new file mode 100644
index 0000000..8694f8f
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/IconButtonSamples.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
+import androidx.tv.material3.IconButton
+import androidx.tv.material3.OutlinedIconButton
+
+@Sampled
+@Composable
+fun IconButtonSample() {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+}
+
+@Sampled
+@Composable
+fun OutlinedIconButtonSample() {
+ OutlinedIconButton(onClick = { /* doSomething() */ }) {
+ Icon(
+ Icons.Outlined.FavoriteBorder,
+ contentDescription = "Localized description"
+ )
+ }
+}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/RadioButtonSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/RadioButtonSamples.kt
new file mode 100644
index 0000000..e9896b5
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/RadioButtonSamples.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.RadioButton
+
+@Sampled
+@Composable
+fun RadioButtonSample() {
+ RadioButton(selected = true, onClick = {})
+}
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/SwitchSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/SwitchSamples.kt
new file mode 100644
index 0000000..2e6b5da
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/SwitchSamples.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Switch
+
+@Sampled
+@Composable
+fun SwitchSample() {
+ Switch(checked = true, onCheckedChange = { })
+}
\ No newline at end of file
diff --git a/tv/samples/src/main/java/androidx/tv/samples/WideButtonSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/WideButtonSamples.kt
new file mode 100644
index 0000000..46e2f94
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/WideButtonSamples.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
+import androidx.tv.material3.Text
+import androidx.tv.material3.WideButton
+
+@Sampled
+@Composable
+fun WideButtonSample() {
+ WideButton(onClick = { }) {
+ Text("Settings")
+ }
+}
+
+@Sampled
+@Composable
+fun WideButtonWithIcon() {
+ WideButton(
+ onClick = { },
+ title = { Text("Settings") },
+ icon = {
+ Icon(
+ imageVector = Icons.Default.Settings,
+ contentDescription = "Settings"
+ )
+ }
+ )
+}
+
+@Sampled
+@Composable
+fun WideButtonWithSubtitle() {
+ WideButton(
+ onClick = { },
+ title = { Text("Settings") },
+ subtitle = { Text(text = "Update device preferences") },
+ )
+}
+
+@Sampled
+@Composable
+fun WideButtonWithIconAndSubtitle() {
+ WideButton(
+ onClick = { },
+ title = { Text("Settings") },
+ subtitle = { Text(text = "Update device preferences") },
+ icon = {
+ Icon(
+ imageVector = Icons.Default.Settings,
+ contentDescription = "Settings"
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 093044e..16c8b91 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -141,13 +141,12 @@
@androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceBorder {
}
- @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceColor {
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceColors {
}
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceDefaults {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceColor color(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
method public androidx.tv.material3.ClickableSurfaceGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow);
method public androidx.tv.material3.ClickableSurfaceScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale, optional @FloatRange(from=0.0) float disabledScale, optional @FloatRange(from=0.0) float focusedDisabledScale);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape);
@@ -356,12 +355,12 @@
method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
}
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
+ }
+
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceDefaults {
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getColor();
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public long getContentColor();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.NonInteractiveSurfaceColors colors(optional long containerColor, optional long contentColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long color;
- property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final long contentColor;
property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.tv.material3.NonInteractiveSurfaceDefaults INSTANCE;
}
@@ -450,9 +449,9 @@
}
public final class SurfaceKt {
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(optional androidx.compose.ui.Modifier modifier, optional float tonalElevation, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional androidx.tv.material3.Border border, optional androidx.tv.material3.Glow glow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColor color, optional androidx.tv.material3.ClickableSurfaceColor contentColor, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceBorder border, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ToggleableSurfaceShape shape, optional androidx.tv.material3.ToggleableSurfaceColor color, optional androidx.tv.material3.ToggleableSurfaceColor contentColor, optional androidx.tv.material3.ToggleableSurfaceScale scale, optional androidx.tv.material3.ToggleableSurfaceBorder border, optional androidx.tv.material3.ToggleableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(optional androidx.compose.ui.Modifier modifier, optional float tonalElevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.tv.material3.NonInteractiveSurfaceColors colors, optional androidx.tv.material3.Border border, optional androidx.tv.material3.Glow glow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColors colors, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceBorder border, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ToggleableSurfaceShape shape, optional androidx.tv.material3.ToggleableSurfaceColors colors, optional androidx.tv.material3.ToggleableSurfaceScale scale, optional androidx.tv.material3.ToggleableSurfaceBorder border, optional androidx.tv.material3.ToggleableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
@@ -509,13 +508,12 @@
@androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceBorder {
}
- @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceColor {
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceColors {
}
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class ToggleableSurfaceDefaults {
method public androidx.tv.material3.ToggleableSurfaceBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border selectedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedSelectedBorder, optional androidx.tv.material3.Border focusedDisabledBorder, optional androidx.tv.material3.Border pressedSelectedBorder, optional androidx.tv.material3.Border selectedDisabledBorder, optional androidx.tv.material3.Border focusedSelectedDisabledBorder);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceColor color(optional long color, optional long focusedColor, optional long pressedColor, optional long selectedColor, optional long disabledColor, optional long focusedSelectedColor, optional long pressedSelectedColor);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long selectedColor, optional long disabledColor, optional long focusedSelectedColor, optional long pressedSelectedColor);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
method public androidx.tv.material3.ToggleableSurfaceGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow, optional androidx.tv.material3.Glow selectedGlow, optional androidx.tv.material3.Glow focusedSelectedGlow, optional androidx.tv.material3.Glow pressedSelectedGlow);
method public androidx.tv.material3.ToggleableSurfaceScale scale(optional float scale, optional float focusedScale, optional float pressedScale, optional float selectedScale, optional float disabledScale, optional float focusedSelectedScale, optional float focusedDisabledScale, optional float pressedSelectedScale, optional float selectedDisabledScale, optional float focusedSelectedDisabledScale);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ToggleableSurfaceShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape, optional androidx.compose.ui.graphics.Shape selectedShape, optional androidx.compose.ui.graphics.Shape disabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedShape, optional androidx.compose.ui.graphics.Shape focusedDisabledShape, optional androidx.compose.ui.graphics.Shape pressedSelectedShape, optional androidx.compose.ui.graphics.Shape selectedDisabledShape, optional androidx.compose.ui.graphics.Shape focusedSelectedDisabledShape);
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NonInteractiveSurfaceScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NonInteractiveSurfaceScreenshotTest.kt
index c23e30b..f317957 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NonInteractiveSurfaceScreenshotTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NonInteractiveSurfaceScreenshotTest.kt
@@ -96,7 +96,10 @@
fun nonInteractiveSurface_containerColor() {
rule.setMaterialContent(scheme.colorScheme) {
Box(containerModifier.testTag(wrapperTestTag)) {
- Surface(surfaceModifier(), color = Color.Green) {}
+ Surface(
+ surfaceModifier(),
+ colors = NonInteractiveSurfaceDefaults.colors(containerColor = Color.Green)
+ ) {}
}
}
assertAgainstGolden("non_interactive_surface_${scheme.name}_containerColor")
@@ -106,7 +109,10 @@
fun nonInteractiveSurface_contentColor() {
rule.setMaterialContent(scheme.colorScheme) {
Box(containerModifier.testTag(wrapperTestTag)) {
- Surface(surfaceModifier(), contentColor = Color.Red) {}
+ Surface(
+ surfaceModifier(),
+ colors = NonInteractiveSurfaceDefaults.colors(contentColor = Color.Red)
+ ) {}
}
}
assertAgainstGolden("non_interactive_surface_${scheme.name}_contentColor")
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
index 9f25017..9684230 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -112,8 +112,8 @@
shape = ClickableSurfaceDefaults.shape(
shape = RectangleShape
),
- color = ClickableSurfaceDefaults.color(
- color = Color.Yellow
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.Yellow
)
) {
Box(Modifier.fillMaxSize())
@@ -123,8 +123,8 @@
shape = ClickableSurfaceDefaults.shape(
shape = RectangleShape
),
- color = ClickableSurfaceDefaults.color(
- color = Color.Green
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.Green
)
) {
Box(Modifier.fillMaxSize())
@@ -180,7 +180,7 @@
},
onClick = {},
tonalElevation = 2.toDp(),
- contentColor = ClickableSurfaceDefaults.color(color = expectedColor)
+ colors = ClickableSurfaceDefaults.colors(contentColor = expectedColor)
) {}
}
}
@@ -379,9 +379,9 @@
.testTag("surface")
.size(100.toDp()),
onClick = {},
- color = ClickableSurfaceDefaults.color(
- color = Color.Transparent,
- focusedColor = Color.Transparent
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.Transparent,
+ focusedContainerColor = Color.Transparent
),
glow = ClickableSurfaceDefaults.glow(
glow = Glow(
@@ -447,9 +447,9 @@
border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
)
),
- color = ClickableSurfaceDefaults.color(
- color = Color.Transparent,
- focusedColor = Color.Transparent
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.Transparent,
+ focusedContainerColor = Color.Transparent
)
) {}
}
@@ -661,9 +661,9 @@
.testTag("surface")
.size(100.toDp()),
onCheckedChange = { isChecked = it },
- color = ToggleableSurfaceDefaults.color(
- color = Color.Transparent,
- selectedColor = Color.Transparent
+ colors = ToggleableSurfaceDefaults.colors(
+ containerColor = Color.Transparent,
+ selectedContainerColor = Color.Transparent
),
glow = ToggleableSurfaceDefaults.glow(
glow = Glow(
@@ -746,9 +746,9 @@
border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
)
),
- color = ToggleableSurfaceDefaults.color(
- color = Color.Transparent,
- selectedColor = Color.Transparent
+ colors = ToggleableSurfaceDefaults.colors(
+ containerColor = Color.Transparent,
+ selectedContainerColor = Color.Transparent
)
) {}
}
@@ -779,9 +779,9 @@
.testTag("surface"),
onClick = {},
enabled = surfaceEnabled,
- color = ClickableSurfaceDefaults.color(
- color = Color.Green,
- disabledColor = Color.Red
+ colors = ClickableSurfaceDefaults.colors(
+ containerColor = Color.Green,
+ disabledContainerColor = Color.Red
)
) {}
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
index b547d9d..6d4dee9 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
@@ -48,6 +48,10 @@
*
* The default text style for internal [Text] components will be set to [Typography.labelLarge].
*
+ * Samples:
+ * @sample androidx.tv.samples.ButtonSample
+ * @sample androidx.tv.samples.LikeButtonSample
+ *
* @param onClick called when this button is clicked
* @param modifier the [Modifier] to be applied to this button
* @param enabled controls the enabled state of this button. When `false`, this component will not
@@ -115,6 +119,9 @@
*
* The default text style for internal [Text] components will be set to [Typography.labelLarge].
*
+ * Samples:
+ * @sample androidx.tv.samples.OutlinedButtonSample
+ *
* @param onClick called when this button is clicked
* @param modifier the [Modifier] to be applied to this button
* @param enabled controls the enabled state of this button. When `false`, this component will not
@@ -190,8 +197,7 @@
scale = scale.toClickableSurfaceScale(),
glow = glow.toClickableSurfaceGlow(),
shape = shape.toClickableSurfaceShape(),
- color = colors.toClickableSurfaceContainerColor(),
- contentColor = colors.toClickableSurfaceContentColor(),
+ colors = colors.toClickableSurfaceColors(),
tonalElevation = tonalElevation,
border = border.toClickableSurfaceBorder(),
interactionSource = interactionSource
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
index 560ad85..3661d23 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
@@ -285,6 +285,8 @@
}
}
+private val WideButtonContainerColor = Color.Transparent
+
@OptIn(ExperimentalTvMaterial3Api::class)
internal fun ButtonShape.toClickableSurfaceShape(): ClickableSurfaceShape = ClickableSurfaceShape(
shape = shape,
@@ -295,30 +297,29 @@
)
@OptIn(ExperimentalTvMaterial3Api::class)
-internal fun ButtonColors.toClickableSurfaceContainerColor(): ClickableSurfaceColor =
- ClickableSurfaceColor(
- color = containerColor,
- focusedColor = focusedContainerColor,
- pressedColor = pressedContainerColor,
- disabledColor = disabledContainerColor,
+internal fun ButtonColors.toClickableSurfaceColors(): ClickableSurfaceColors =
+ ClickableSurfaceColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor
)
@OptIn(ExperimentalTvMaterial3Api::class)
-internal fun ButtonColors.toClickableSurfaceContentColor(): ClickableSurfaceColor =
- ClickableSurfaceColor(
- color = contentColor,
- focusedColor = focusedContentColor,
- pressedColor = pressedContentColor,
- disabledColor = disabledContentColor,
- )
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-internal fun WideButtonContentColor.toClickableSurfaceContentColor(): ClickableSurfaceColor =
- ClickableSurfaceColor(
- color = contentColor,
- focusedColor = focusedContentColor,
- pressedColor = pressedContentColor,
- disabledColor = disabledContentColor,
+internal fun WideButtonContentColor.toClickableSurfaceColors(): ClickableSurfaceColors =
+ ClickableSurfaceColors(
+ containerColor = WideButtonContainerColor,
+ contentColor = contentColor,
+ focusedContainerColor = WideButtonContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = WideButtonContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = WideButtonContainerColor,
+ disabledContentColor = disabledContentColor
)
@OptIn(ExperimentalTvMaterial3Api::class)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
index 1c1fc72..38550b6 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
@@ -46,6 +46,8 @@
*
* This Card handles click events, calling its [onClick] lambda.
*
+ * @sample androidx.tv.samples.CardSample
+ *
* @param onClick called when this card is clicked
* @param modifier the [Modifier] to be applied to this card
* @param shape [CardShape] defines the shape of this card's container in different interaction
@@ -80,8 +82,7 @@
onClick = onClick,
modifier = modifier,
shape = shape.toClickableSurfaceShape(),
- color = colors.toClickableSurfaceContainerColor(),
- contentColor = colors.toClickableSurfaceContentColor(),
+ colors = colors.toClickableSurfaceColors(),
scale = scale.toClickableSurfaceScale(),
border = border.toClickableSurfaceBorder(),
glow = glow.toClickableSurfaceGlow(),
@@ -100,6 +101,8 @@
*
* This Card handles click events, calling its [onClick] lambda.
*
+ * @sample androidx.tv.samples.ClassicCardSample
+ *
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
@@ -176,6 +179,8 @@
*
* This Card handles click events, calling its [onClick] lambda.
*
+ * @sample androidx.tv.samples.CompactCardSample
+ *
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
@@ -257,6 +262,8 @@
*
* This Card handles click events, calling its [onClick] lambda.
*
+ * @sample androidx.tv.samples.WideClassicCardSample
+ *
* @param onClick called when this card is clicked
* @param image defines the [Composable] image to be displayed on top of the Card.
* @param title defines the [Composable] title placed below the image in the Card.
@@ -526,21 +533,16 @@
private const val DescriptionAlpha = 0.8f
@OptIn(ExperimentalTvMaterial3Api::class)
-private fun CardColors.toClickableSurfaceContainerColor() =
- ClickableSurfaceColor(
- color = containerColor,
- focusedColor = focusedContainerColor,
- pressedColor = pressedContainerColor,
- disabledColor = containerColor
- )
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-private fun CardColors.toClickableSurfaceContentColor() =
- ClickableSurfaceColor(
- color = contentColor,
- focusedColor = focusedContentColor,
- pressedColor = pressedContentColor,
- disabledColor = contentColor
+private fun CardColors.toClickableSurfaceColors() =
+ ClickableSurfaceColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = containerColor,
+ disabledContentColor = contentColor
)
@OptIn(ExperimentalTvMaterial3Api::class)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
index 92aa581..df431a3 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
@@ -40,6 +40,8 @@
* It provides a vertical layout with an image card slot at the top. And below that, there are
* slots for the title, subtitle and description.
*
+ * @sample androidx.tv.samples.StandardCardLayoutSample
+ *
* @param imageCard defines the [Composable] to be used for the image card. See
* [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
* in the lambda function should be forwarded and used with the image card composable.
@@ -101,6 +103,8 @@
* It provides a horizontal layout with an image card slot at the start, followed by the title,
* subtitle and description at the end.
*
+ * @sample androidx.tv.samples.WideCardLayoutSample
+ *
* @param imageCard defines the [Composable] to be used for the image card. See
* [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
* in the lambda function should to be forwarded and used with the image card composable.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
index 960df60..d6900dd 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Checkbox.kt
@@ -64,6 +64,8 @@
*
* @see [TriStateCheckbox] if you require support for an indeterminate state.
*
+ * @sample androidx.tv.samples.CheckboxSample
+ *
* @param checked whether this checkbox is checked or unchecked
* @param onCheckedChange called when this checkbox is clicked. If `null`, then this checkbox will
* not be interactable, unless something else handles its input events and updates its state.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
index 1c66a17..408ab4f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
@@ -42,6 +42,8 @@
*
* The default text style for internal [Text] components will be set to [Typography.labelLarge].
*
+ * @sample androidx.tv.samples.IconButtonSample
+ *
* @param onClick called when this button is clicked
* @param modifier the [Modifier] to be applied to this button
* @param enabled controls the enabled state of this button. When `false`, this component will not
@@ -79,8 +81,7 @@
onClick = onClick,
enabled = enabled,
shape = shape.toClickableSurfaceShape(),
- color = colors.toClickableSurfaceContainerColor(),
- contentColor = colors.toClickableSurfaceContentColor(),
+ colors = colors.toClickableSurfaceColors(),
scale = scale.toClickableSurfaceScale(),
border = border.toClickableSurfaceBorder(),
glow = glow.toClickableSurfaceGlow(),
@@ -107,6 +108,8 @@
*
* The default text style for internal [Text] components will be set to [Typography.labelLarge].
*
+ * @sample androidx.tv.samples.OutlinedIconButtonSample
+ *
* @param onClick called when this button is clicked
* @param modifier the [Modifier] to be applied to this button
* @param enabled controls the enabled state of this button. When `false`, this component will not
@@ -144,8 +147,7 @@
onClick = onClick,
enabled = enabled,
shape = shape.toClickableSurfaceShape(),
- color = colors.toClickableSurfaceContainerColor(),
- contentColor = colors.toClickableSurfaceContentColor(),
+ colors = colors.toClickableSurfaceColors(),
scale = scale.toClickableSurfaceScale(),
border = border.toClickableSurfaceBorder(),
glow = glow.toClickableSurfaceGlow(),
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
index 33e5a85..bf6c3fc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/RadioButton.kt
@@ -47,6 +47,8 @@
*
* ![Radio button image](https://developer.android.com/images/reference/androidx/compose/material3/radio-button.png)
*
+ * @sample androidx.tv.samples.RadioButtonSample
+ *
* @param selected whether this radio button is selected or not
* @param onClick called when this radio button is clicked. If `null`, then this radio button will
* not be interactable, unless something else handles its input events and updates its state.
@@ -72,7 +74,8 @@
) {
val dotRadius = animateDpAsState(
targetValue = if (selected) RadioButtonDotSize / 2 else 0.dp,
- animationSpec = tween(durationMillis = RadioAnimationDuration)
+ animationSpec = tween(durationMillis = RadioAnimationDuration),
+ label = "radioButton_dotRadius"
)
val radioColor = colors.radioColor(enabled, selected)
val selectableModifier =
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index 16f3dcc..2b563e7 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -65,8 +65,8 @@
* @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
* in a darker color in light theme and lighter color in dark theme.
* @param shape Defines the surface's shape.
- * @param color Color to be used on background of the Surface
- * @param contentColor The preferred content color provided by this Surface to its children.
+ * @param colors Defines the background & content color to be used in this Surface.
+ * See [NonInteractiveSurfaceDefaults.colors].
* @param border Defines a border around the Surface.
* @param glow Diffused shadow to be shown behind the Surface.
* @param content defines the [Composable] content inside the surface
@@ -78,8 +78,7 @@
modifier: Modifier = Modifier,
tonalElevation: Dp = 0.dp,
shape: Shape = NonInteractiveSurfaceDefaults.shape,
- color: Color = NonInteractiveSurfaceDefaults.color,
- contentColor: Color = NonInteractiveSurfaceDefaults.contentColor,
+ colors: NonInteractiveSurfaceColors = NonInteractiveSurfaceDefaults.colors(),
border: Border = NonInteractiveSurfaceDefaults.border,
glow: Glow = NonInteractiveSurfaceDefaults.glow,
content: @Composable (BoxScope.() -> Unit)
@@ -90,8 +89,8 @@
enabled = true,
tonalElevation = tonalElevation,
shape = shape,
- color = color,
- contentColor = contentColor,
+ color = colors.containerColor,
+ contentColor = colors.contentColor,
scale = 1.0f,
border = border,
glow = glow,
@@ -114,8 +113,8 @@
* @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
* in a darker color in light theme and lighter color in dark theme.
* @param shape Defines the surface's shape.
- * @param color Color to be used on background of the Surface
- * @param contentColor The preferred content color provided by this Surface to its children.
+ * @param colors Defines the background & content colors to be used in this surface for different
+ * interaction states. See [ClickableSurfaceDefaults.colors].
* @param scale Defines size of the Surface relative to its original size.
* @param border Defines a border around the Surface.
* @param glow Diffused shadow to be shown behind the Surface.
@@ -133,8 +132,7 @@
enabled: Boolean = true,
tonalElevation: Dp = 0.dp,
shape: ClickableSurfaceShape = ClickableSurfaceDefaults.shape(),
- color: ClickableSurfaceColor = ClickableSurfaceDefaults.color(),
- contentColor: ClickableSurfaceColor = ClickableSurfaceDefaults.contentColor(),
+ colors: ClickableSurfaceColors = ClickableSurfaceDefaults.colors(),
scale: ClickableSurfaceScale = ClickableSurfaceDefaults.scale(),
border: ClickableSurfaceBorder = ClickableSurfaceDefaults.border(),
glow: ClickableSurfaceGlow = ClickableSurfaceDefaults.glow(),
@@ -158,17 +156,17 @@
pressed = pressed,
shape = shape
),
- color = ClickableSurfaceDefaults.color(
+ color = ClickableSurfaceDefaults.containerColor(
enabled = enabled,
focused = focused,
pressed = pressed,
- color = color
+ colors = colors
),
- contentColor = ClickableSurfaceDefaults.color(
+ contentColor = ClickableSurfaceDefaults.contentColor(
enabled = enabled,
focused = focused,
pressed = pressed,
- color = contentColor
+ colors = colors
),
scale = ClickableSurfaceDefaults.scale(
enabled = enabled,
@@ -215,8 +213,8 @@
* @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
* in a darker color in light theme and lighter color in dark theme.
* @param shape Defines the surface's shape.
- * @param color Color to be used on background of the Surface
- * @param contentColor The preferred content color provided by this Surface to its children.
+ * @param colors Defines the background & content colors to be used in this surface for different
+ * interaction states. See [ToggleableSurfaceDefaults.colors].
* @param scale Defines size of the Surface relative to its original size.
* @param border Defines a border around the Surface.
* @param glow Diffused shadow to be shown behind the Surface.
@@ -235,8 +233,7 @@
enabled: Boolean = true,
tonalElevation: Dp = Elevation.Level0,
shape: ToggleableSurfaceShape = ToggleableSurfaceDefaults.shape(),
- color: ToggleableSurfaceColor = ToggleableSurfaceDefaults.color(),
- contentColor: ToggleableSurfaceColor = ToggleableSurfaceDefaults.contentColor(),
+ colors: ToggleableSurfaceColors = ToggleableSurfaceDefaults.colors(),
scale: ToggleableSurfaceScale = ToggleableSurfaceDefaults.scale(),
border: ToggleableSurfaceBorder = ToggleableSurfaceDefaults.border(),
glow: ToggleableSurfaceGlow = ToggleableSurfaceDefaults.glow(),
@@ -263,19 +260,19 @@
selected = checked,
shape = shape
),
- color = ToggleableSurfaceDefaults.color(
+ color = ToggleableSurfaceDefaults.containerColor(
enabled = enabled,
focused = focused,
pressed = pressed,
selected = checked,
- color = color
+ colors = colors
),
- contentColor = ToggleableSurfaceDefaults.color(
+ contentColor = ToggleableSurfaceDefaults.contentColor(
enabled = enabled,
focused = focused,
pressed = pressed,
selected = checked,
- color = contentColor
+ colors = colors
),
scale = ToggleableSurfaceDefaults.scale(
enabled = enabled,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
index 8dc31ea..10cbe4c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
@@ -36,15 +36,21 @@
val shape: Shape @ReadOnlyComposable @Composable get() = MaterialTheme.shapes.medium
/**
- * Represents the default container color used by a non-interactive [Surface]
+ * Creates a [NonInteractiveSurfaceColors] that represents the default container & content
+ * colors used by a non-interactive [Surface].
+ *
+ * @param containerColor the container color of this Surface
+ * @param contentColor the content color of this Surface
*/
- val color: Color @ReadOnlyComposable @Composable get() = MaterialTheme.colorScheme.surface
-
- /**
- * Represents the default content color used by a non-interactive [Surface]
- */
- val contentColor: Color @ReadOnlyComposable @Composable get() =
- MaterialTheme.colorScheme.onSurface
+ @ReadOnlyComposable
+ @Composable
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(containerColor)
+ ) = NonInteractiveSurfaceColors(
+ containerColor = containerColor,
+ contentColor = contentColor
+ )
/**
* Represents the default border used by a non-interactive [Surface]
@@ -104,66 +110,68 @@
focusedDisabledShape = focusedDisabledShape
)
- internal fun color(
+ internal fun containerColor(
enabled: Boolean,
focused: Boolean,
pressed: Boolean,
- color: ClickableSurfaceColor
+ colors: ClickableSurfaceColors
): Color {
return when {
- pressed && enabled -> color.pressedColor
- focused && enabled -> color.focusedColor
- enabled -> color.color
- else -> color.disabledColor
+ pressed && enabled -> colors.pressedContainerColor
+ focused && enabled -> colors.focusedContainerColor
+ enabled -> colors.containerColor
+ else -> colors.disabledContainerColor
+ }
+ }
+
+ internal fun contentColor(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ colors: ClickableSurfaceColors
+ ): Color {
+ return when {
+ pressed && enabled -> colors.pressedContentColor
+ focused && enabled -> colors.focusedContentColor
+ enabled -> colors.contentColor
+ else -> colors.disabledContentColor
}
}
/**
- * Creates a [ClickableSurfaceColor] that represents the default container colors used in a
- * Surface.
+ * Creates a [ClickableSurfaceColors] that represents the default container & content colors
+ * used in a Surface.
*
- * @param color the container color of this Surface when enabled
- * @param focusedColor the container color of this Surface when enabled and focused
- * @param pressedColor the container color of this Surface when enabled and pressed
- * @param disabledColor the container color of this Surface when not enabled
+ * @param containerColor the container color of this Surface when enabled
+ * @param contentColor the content color of this Surface when enabled
+ * @param focusedContainerColor the container color of this Surface when enabled and focused
+ * @param focusedContentColor the content color of this Surface when enabled and focused
+ * @param pressedContainerColor the container color of this Surface when enabled and pressed
+ * @param pressedContentColor the content color of this Surface when enabled and pressed
+ * @param disabledContainerColor the container color of this Surface when not enabled
+ * @param disabledContentColor the content color of this Surface when not enabled
*/
@ReadOnlyComposable
@Composable
- fun color(
- color: Color = MaterialTheme.colorScheme.surface,
- focusedColor: Color = MaterialTheme.colorScheme.inverseSurface,
- pressedColor: Color = MaterialTheme.colorScheme.inverseSurface,
- disabledColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(
- alpha = DisabledBackgroundAlpha
- )
- ) = ClickableSurfaceColor(
- color = color,
- focusedColor = focusedColor,
- pressedColor = pressedColor,
- disabledColor = disabledColor
- )
-
- /**
- * Creates a [ClickableSurfaceColor] that represents the default content colors used in a
- * Surface.
- *
- * @param color the content color of this Surface when enabled
- * @param focusedColor the content color of this Surface when enabled and focused
- * @param pressedColor the content color of this Surface when enabled and pressed
- * @param disabledColor the content color of this Surface when not enabled
- */
- @ReadOnlyComposable
- @Composable
- fun contentColor(
- color: Color = MaterialTheme.colorScheme.onSurface,
- focusedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- pressedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- disabledColor: Color = MaterialTheme.colorScheme.onSurface
- ) = ClickableSurfaceColor(
- color = color,
- focusedColor = focusedColor,
- pressedColor = pressedColor,
- disabledColor = disabledColor
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(containerColor),
+ focusedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface,
+ focusedContentColor: Color = contentColorFor(focusedContainerColor),
+ pressedContainerColor: Color = focusedContainerColor,
+ pressedContentColor: Color = contentColorFor(pressedContainerColor),
+ disabledContainerColor: Color = MaterialTheme.colorScheme.surfaceVariant
+ .copy(alpha = DisabledContainerAlpha),
+ disabledContentColor: Color = MaterialTheme.colorScheme.onSurface
+ ) = ClickableSurfaceColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor
)
internal fun scale(
@@ -342,69 +350,71 @@
)
/**
- * Creates a [ToggleableSurfaceColor] that represents the default container colors used in a
- * toggleable Surface.
+ * Creates a [ToggleableSurfaceColors] that represents the default container & content colors
+ * used in a toggleable Surface.
*
- * @param color the color used when the Surface is enabled, and has no other [Interaction]s.
- * @param focusedColor the color used when the Surface is enabled and focused.
- * @param pressedColor the color used when the Surface is enabled and pressed.
- * @param selectedColor the color used when the Surface is enabled and selected.
- * @param disabledColor the color used when the Surface is not enabled.
- * @param focusedSelectedColor the color used when the Surface is enabled, focused and selected.
- * @param pressedSelectedColor the color used when the Surface is enabled, pressed and selected.
+ * @param containerColor the container color used when the Surface is enabled, and has no other
+ * [Interaction]s.
+ * @param contentColor the content color used when the Surface is enabled, and has no other
+ * [Interaction]s.
+ * @param focusedContainerColor the container color used when the Surface is enabled and
+ * focused.
+ * @param focusedContentColor the content color used when the Surface is enabled and
+ * focused.
+ * @param pressedContainerColor the container color used when the Surface is enabled and
+ * pressed.
+ * @param pressedContentColor the content color used when the Surface is enabled and
+ * pressed.
+ * @param selectedContainerColor the container color used when the Surface is enabled and
+ * selected.
+ * @param selectedContentColor the content color used when the Surface is enabled and
+ * selected.
+ * @param disabledContainerColor the container color used when the Surface is not enabled.
+ * @param disabledContentColor the content color used when the Surface is not enabled.
+ * @param focusedSelectedContainerColor the container color used when the Surface is enabled,
+ * focused and selected.
+ * @param focusedSelectedContentColor the content color used when the Surface is enabled,
+ * focused and selected.
+ * @param pressedSelectedContainerColor the container color used when the Surface is enabled,
+ * pressed and selected.
+ * @param pressedSelectedContentColor the content color used when the Surface is enabled,
+ * pressed and selected.
*/
@ReadOnlyComposable
@Composable
- fun color(
- color: Color = MaterialTheme.colorScheme.surface,
- focusedColor: Color = MaterialTheme.colorScheme.inverseSurface,
- pressedColor: Color = MaterialTheme.colorScheme.inverseSurface,
- selectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
- disabledColor: Color = MaterialTheme.colorScheme.surfaceVariant.copy(
- alpha = DisabledBackgroundAlpha
- ),
- focusedSelectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
- pressedSelectedColor: Color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f)
- ) = ToggleableSurfaceColor(
- color = color,
- focusedColor = focusedColor,
- pressedColor = pressedColor,
- selectedColor = selectedColor,
- disabledColor = disabledColor,
- focusedSelectedColor = focusedSelectedColor,
- pressedSelectedColor = pressedSelectedColor
- )
-
- /**
- * Creates a [ToggleableSurfaceColor] that represents the default content colors used in a
- * toggleable Surface.
- *
- * @param color the color used when the Surface is enabled, and has no other [Interaction]s.
- * @param focusedColor the color used when the Surface is enabled and focused.
- * @param pressedColor the color used when the Surface is enabled and pressed.
- * @param selectedColor the color used when the Surface is enabled and selected.
- * @param disabledColor the color used when the Surface is not enabled.
- * @param focusedSelectedColor the color used when the Surface is enabled, focused and selected.
- * @param pressedSelectedColor the color used when the Surface is enabled, pressed and selected.
- */
- @ReadOnlyComposable
- @Composable
- fun contentColor(
- color: Color = MaterialTheme.colorScheme.onSurface,
- focusedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- pressedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- selectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- disabledColor: Color = MaterialTheme.colorScheme.onSurface,
- focusedSelectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
- pressedSelectedColor: Color = MaterialTheme.colorScheme.inverseOnSurface
- ) = ToggleableSurfaceColor(
- color = color,
- focusedColor = focusedColor,
- pressedColor = pressedColor,
- selectedColor = selectedColor,
- disabledColor = disabledColor,
- focusedSelectedColor = focusedSelectedColor,
- pressedSelectedColor = pressedSelectedColor
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(containerColor),
+ focusedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface,
+ focusedContentColor: Color = contentColorFor(focusedContainerColor),
+ pressedContainerColor: Color = focusedContainerColor,
+ pressedContentColor: Color = contentColorFor(pressedContainerColor),
+ selectedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface
+ .copy(alpha = SelectedContainerAlpha),
+ selectedContentColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ disabledContainerColor: Color = MaterialTheme.colorScheme.surfaceVariant
+ .copy(alpha = DisabledContainerAlpha),
+ disabledContentColor: Color = MaterialTheme.colorScheme.onSurface,
+ focusedSelectedContainerColor: Color = MaterialTheme.colorScheme.inverseSurface
+ .copy(alpha = SelectedContainerAlpha),
+ focusedSelectedContentColor: Color = MaterialTheme.colorScheme.inverseOnSurface,
+ pressedSelectedContainerColor: Color = focusedSelectedContainerColor,
+ pressedSelectedContentColor: Color = focusedSelectedContentColor
+ ) = ToggleableSurfaceColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ focusedContainerColor = focusedContainerColor,
+ focusedContentColor = focusedContentColor,
+ pressedContainerColor = pressedContainerColor,
+ pressedContentColor = pressedContentColor,
+ selectedContainerColor = selectedContainerColor,
+ selectedContentColor = selectedContentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ focusedSelectedContainerColor = focusedSelectedContainerColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
+ pressedSelectedContainerColor = pressedSelectedContainerColor,
+ pressedSelectedContentColor = pressedSelectedContentColor
)
/**
@@ -546,21 +556,39 @@
}
}
- internal fun color(
+ internal fun containerColor(
enabled: Boolean,
focused: Boolean,
pressed: Boolean,
selected: Boolean,
- color: ToggleableSurfaceColor
+ colors: ToggleableSurfaceColors
): Color {
return when {
- enabled && selected && pressed -> color.pressedSelectedColor
- enabled && selected && focused -> color.focusedSelectedColor
- enabled && selected -> color.selectedColor
- enabled && pressed -> color.pressedColor
- enabled && focused -> color.focusedColor
- enabled -> color.color
- else -> color.disabledColor
+ enabled && selected && pressed -> colors.pressedSelectedContainerColor
+ enabled && selected && focused -> colors.focusedSelectedContainerColor
+ enabled && selected -> colors.selectedContainerColor
+ enabled && pressed -> colors.pressedContainerColor
+ enabled && focused -> colors.focusedContainerColor
+ enabled -> colors.containerColor
+ else -> colors.disabledContainerColor
+ }
+ }
+
+ internal fun contentColor(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ selected: Boolean,
+ colors: ToggleableSurfaceColors
+ ): Color {
+ return when {
+ enabled && selected && pressed -> colors.pressedSelectedContentColor
+ enabled && selected && focused -> colors.focusedSelectedContentColor
+ enabled && selected -> colors.selectedContentColor
+ enabled && pressed -> colors.pressedContentColor
+ enabled && focused -> colors.focusedContentColor
+ enabled -> colors.contentColor
+ else -> colors.disabledContentColor
}
}
@@ -625,4 +653,5 @@
}
}
-private const val DisabledBackgroundAlpha = 0.4f
+private const val DisabledContainerAlpha = 0.4f
+private const val SelectedContainerAlpha = 0.5f
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
index c8d929a..2da15cf 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
@@ -134,38 +134,56 @@
*/
@ExperimentalTvMaterial3Api
@Immutable
-class ClickableSurfaceColor internal constructor(
- internal val color: Color,
- internal val focusedColor: Color,
- internal val pressedColor: Color,
- internal val disabledColor: Color
+class ClickableSurfaceColors internal constructor(
+ internal val containerColor: Color,
+ internal val contentColor: Color,
+ internal val focusedContainerColor: Color,
+ internal val focusedContentColor: Color,
+ internal val pressedContainerColor: Color,
+ internal val pressedContentColor: Color,
+ internal val disabledContainerColor: Color,
+ internal val disabledContentColor: Color
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
- other as ClickableSurfaceColor
+ other as ClickableSurfaceColors
- if (color != other.color) return false
- if (focusedColor != other.focusedColor) return false
- if (pressedColor != other.pressedColor) return false
- if (disabledColor != other.disabledColor) return false
+ if (containerColor != other.containerColor) return false
+ if (contentColor != other.contentColor) return false
+ if (focusedContainerColor != other.focusedContainerColor) return false
+ if (focusedContentColor != other.focusedContentColor) return false
+ if (pressedContainerColor != other.pressedContainerColor) return false
+ if (pressedContentColor != other.pressedContentColor) return false
+ if (disabledContainerColor != other.disabledContainerColor) return false
+ if (disabledContentColor != other.disabledContentColor) return false
return true
}
override fun hashCode(): Int {
- var result = color.hashCode()
- result = 31 * result + focusedColor.hashCode()
- result = 31 * result + pressedColor.hashCode()
- result = 31 * result + disabledColor.hashCode()
-
+ var result = containerColor.hashCode()
+ result = 31 * result + contentColor.hashCode()
+ result = 31 * result + focusedContainerColor.hashCode()
+ result = 31 * result + focusedContentColor.hashCode()
+ result = 31 * result + pressedContainerColor.hashCode()
+ result = 31 * result + pressedContentColor.hashCode()
+ result = 31 * result + disabledContainerColor.hashCode()
+ result = 31 * result + disabledContentColor.hashCode()
return result
}
override fun toString(): String {
- return "ClickableSurfaceColor(color=$color, focusedColor=$focusedColor, " +
- "pressedColor=$pressedColor, disabledColor=$disabledColor)"
+ return "ClickableSurfaceColors(" +
+ "containerColor=$containerColor, " +
+ "contentColor=$contentColor, " +
+ "focusedContainerColor=$focusedContainerColor, " +
+ "focusedContentColor=$focusedContentColor, " +
+ "pressedContainerColor=$pressedContainerColor, " +
+ "pressedContentColor=$pressedContentColor, " +
+ "disabledContainerColor=$disabledContainerColor, " +
+ "disabledContentColor=$disabledContentColor)"
}
}
@@ -174,49 +192,80 @@
*/
@ExperimentalTvMaterial3Api
@Immutable
-class ToggleableSurfaceColor internal constructor(
- internal val color: Color,
- internal val focusedColor: Color,
- internal val pressedColor: Color,
- internal val selectedColor: Color,
- internal val disabledColor: Color,
- internal val focusedSelectedColor: Color,
- internal val pressedSelectedColor: Color
+class ToggleableSurfaceColors internal constructor(
+ internal val containerColor: Color,
+ internal val contentColor: Color,
+ internal val focusedContainerColor: Color,
+ internal val focusedContentColor: Color,
+ internal val pressedContainerColor: Color,
+ internal val pressedContentColor: Color,
+ internal val selectedContainerColor: Color,
+ internal val selectedContentColor: Color,
+ internal val disabledContainerColor: Color,
+ internal val disabledContentColor: Color,
+ internal val focusedSelectedContainerColor: Color,
+ internal val focusedSelectedContentColor: Color,
+ internal val pressedSelectedContainerColor: Color,
+ internal val pressedSelectedContentColor: Color
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
- other as ToggleableSurfaceColor
+ other as ToggleableSurfaceColors
- if (color != other.color) return false
- if (focusedColor != other.focusedColor) return false
- if (pressedColor != other.pressedColor) return false
- if (selectedColor != other.selectedColor) return false
- if (disabledColor != other.disabledColor) return false
- if (focusedSelectedColor != other.focusedSelectedColor) return false
- if (pressedSelectedColor != other.pressedSelectedColor) return false
+ if (containerColor != other.containerColor) return false
+ if (contentColor != other.contentColor) return false
+ if (focusedContainerColor != other.focusedContainerColor) return false
+ if (focusedContentColor != other.focusedContentColor) return false
+ if (pressedContainerColor != other.pressedContainerColor) return false
+ if (pressedContentColor != other.pressedContentColor) return false
+ if (selectedContainerColor != other.selectedContainerColor) return false
+ if (selectedContentColor != other.selectedContentColor) return false
+ if (disabledContainerColor != other.disabledContainerColor) return false
+ if (disabledContentColor != other.disabledContentColor) return false
+ if (focusedSelectedContainerColor != other.focusedSelectedContainerColor) return false
+ if (focusedSelectedContentColor != other.focusedSelectedContentColor) return false
+ if (pressedSelectedContainerColor != other.pressedSelectedContainerColor) return false
+ if (pressedSelectedContentColor != other.pressedSelectedContentColor) return false
return true
}
override fun hashCode(): Int {
- var result = color.hashCode()
- result = 31 * result + focusedColor.hashCode()
- result = 31 * result + pressedColor.hashCode()
- result = 31 * result + selectedColor.hashCode()
- result = 31 * result + disabledColor.hashCode()
- result = 31 * result + focusedSelectedColor.hashCode()
- result = 31 * result + pressedSelectedColor.hashCode()
-
+ var result = containerColor.hashCode()
+ result = 31 * result + contentColor.hashCode()
+ result = 31 * result + focusedContainerColor.hashCode()
+ result = 31 * result + focusedContentColor.hashCode()
+ result = 31 * result + pressedContainerColor.hashCode()
+ result = 31 * result + pressedContentColor.hashCode()
+ result = 31 * result + selectedContainerColor.hashCode()
+ result = 31 * result + selectedContentColor.hashCode()
+ result = 31 * result + disabledContainerColor.hashCode()
+ result = 31 * result + disabledContentColor.hashCode()
+ result = 31 * result + focusedSelectedContainerColor.hashCode()
+ result = 31 * result + focusedSelectedContentColor.hashCode()
+ result = 31 * result + pressedSelectedContainerColor.hashCode()
+ result = 31 * result + pressedSelectedContentColor.hashCode()
return result
}
override fun toString(): String {
- return "ToggleableSurfaceColor(color=$color, focusedColor=$focusedColor," +
- "pressedColor=$pressedColor, selectedColor=$selectedColor," +
- "disabledColor=$disabledColor, focusedSelectedColor=$focusedSelectedColor, " +
- "pressedSelectedColor=$pressedSelectedColor)"
+ return "ToggleableSurfaceColors(" +
+ "containerColor=$containerColor, " +
+ "contentColor=$contentColor, " +
+ "focusedContainerColor=$focusedContainerColor, " +
+ "focusedContentColor=$focusedContentColor, " +
+ "pressedContainerColor=$pressedContainerColor, " +
+ "pressedContentColor=$pressedContentColor, " +
+ "selectedContainerColor=$selectedContainerColor, " +
+ "selectedContentColor=$selectedContentColor, " +
+ "disabledContainerColor=$disabledContainerColor, " +
+ "disabledContentColor=$disabledContentColor, " +
+ "focusedSelectedContainerColor=$focusedSelectedContainerColor, " +
+ "focusedSelectedContentColor=$focusedSelectedContentColor, " +
+ "pressedSelectedContainerColor=$pressedSelectedContainerColor, " +
+ "pressedSelectedContentColor=$pressedSelectedContentColor)"
}
}
@@ -381,8 +430,10 @@
}
override fun toString(): String {
- return "${this.javaClass.simpleName}(border=$border, focusedBorder=$focusedBorder, " +
- "pressedBorder=$pressedBorder, disabledBorder=$disabledBorder, " +
+ return "ClickableSurfaceBorder(border=$border, " +
+ "focusedBorder=$focusedBorder, " +
+ "pressedBorder=$pressedBorder, " +
+ "disabledBorder=$disabledBorder, " +
"focusedDisabledBorder=$focusedDisabledBorder)"
}
}
@@ -533,3 +584,36 @@
"focusedSelectedGlow=$focusedSelectedGlow, pressedSelectedGlow=$pressedSelectedGlow)"
}
}
+
+/**
+ * Defines the container & content color [Color] for a non interactive surface.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class NonInteractiveSurfaceColors internal constructor(
+ internal val containerColor: Color,
+ internal val contentColor: Color
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as NonInteractiveSurfaceColors
+
+ if (containerColor != other.containerColor) return false
+ if (contentColor != other.contentColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = containerColor.hashCode()
+ result = 31 * result + contentColor.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "NonInteractiveSurfaceColors(containerColor=$containerColor, " +
+ "contentColor=$contentColor)"
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Switch.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Switch.kt
index 936056a..d71168a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Switch.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Switch.kt
@@ -66,6 +66,8 @@
*
* Switch can be used with a custom icon via [thumbContent] parameter
*
+ * @sample androidx.tv.samples.SwitchSample
+ *
* @param checked whether or not this switch is checked
* @param onCheckedChange called when this switch is clicked. If `null`, then this switch will not
* be interactable, unless something else handles its input events and updates its state.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
index 30f4d67..5e59fbc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
@@ -35,7 +35,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
@@ -48,6 +47,9 @@
/**
* Material Design wide button for TV.
*
+ * Samples:
+ * @sample androidx.tv.samples.WideButtonSample
+ *
* @param onClick called when this button is clicked
* @param modifier the [Modifier] to be applied to this button
* @param enabled controls the enabled state of this button. When `false`, this component will not
@@ -111,6 +113,11 @@
/**
* Material Design wide button for TV.
*
+ * Samples:
+ * @sample androidx.tv.samples.WideButtonWithIcon
+ * @sample androidx.tv.samples.WideButtonWithSubtitle
+ * @sample androidx.tv.samples.WideButtonWithIconAndSubtitle
+ *
* @param onClick called when this button is clicked
* @param title the title content of the button, typically a [Text]
* @param modifier the [Modifier] to be applied to this button
@@ -238,8 +245,7 @@
scale = scale.toClickableSurfaceScale(),
glow = glow.toClickableSurfaceGlow(),
shape = shape.toClickableSurfaceShape(),
- color = wideButtonContainerColor(),
- contentColor = contentColor.toClickableSurfaceContentColor(),
+ colors = contentColor.toClickableSurfaceColors(),
tonalElevation = tonalElevation,
border = border.toClickableSurfaceBorder(),
interactionSource = interactionSource
@@ -273,12 +279,3 @@
}
}
}
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-private fun wideButtonContainerColor() = ClickableSurfaceDefaults.color(
- color = Color.Transparent,
- focusedColor = Color.Transparent,
- pressedColor = Color.Transparent,
- disabledColor = Color.Transparent,
-)
diff --git a/wear/benchmark/integration-tests/macrobenchmark/build.gradle b/wear/benchmark/integration-tests/macrobenchmark/build.gradle
index 1708504..e7fc8f1 100644
--- a/wear/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -16,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id 'com.android.library'
+ id 'com.android.test'
id 'kotlin-android'
}
@@ -25,25 +25,22 @@
minSdkVersion 29
}
namespace "androidx.wear.benchmark.integration.macrobenchmark"
+ targetProjectPath = ":wear:benchmark:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":wear:benchmark:integration-tests:macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":wear:benchmark:integration-tests:macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
}
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/ScrollBenchmark.kt b/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/ScrollBenchmark.kt
similarity index 100%
rename from wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/ScrollBenchmark.kt
rename to wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/ScrollBenchmark.kt
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt b/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt
similarity index 100%
rename from wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt
rename to wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt b/wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
similarity index 100%
rename from wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
rename to wear/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/SwipeBenchmark.kt
diff --git a/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Card.kt b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Card.kt
new file mode 100644
index 0000000..7994fe3
--- /dev/null
+++ b/wear/compose/compose-material-core/src/commonMain/kotlin/androidx/wear/compose/materialcore/Card.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.materialcore
+
+import androidx.annotation.RestrictTo
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+
+/**
+ * Base level Wear Material [Card] that offers a single slot to take any content.
+ *
+ * Is used as the container for more opinionated [Card] components that take specific content such
+ * as icons, images, titles, subtitles and labels.
+ *
+ * Cards can be enabled or disabled. A disabled card will not respond to click events.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * Wear OS Material design guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param modifier Modifier to be applied to the card
+ * @param border A BorderStroke object which is used for the outline drawing.
+ * Can be null - then outline will not be drawn
+ * @param containerPainter A painter used to paint the background of the card. A card will
+ * normally have a gradient background. Use [CardDefaults.cardBackgroundPainter()] to obtain an
+ * appropriate painter
+ * @param containerPainter A Painter which is used for background drawing.
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param role The type of user interface element. Accessibility services might use this
+ * to describe the element or do customizations
+ * @param content A main slot for a content of this card
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+public fun Card(
+ onClick: () -> Unit,
+ modifier: Modifier,
+ border: BorderStroke?,
+ containerPainter: Painter,
+ enabled: Boolean,
+ contentPadding: PaddingValues,
+ shape: Shape,
+ interactionSource: MutableInteractionSource,
+ role: Role?,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min)
+ .clip(shape = shape)
+ .paint(
+ painter = containerPainter,
+ contentScale = ContentScale.Crop
+ )
+ .clickable(
+ enabled = enabled,
+ onClick = onClick,
+ role = role,
+ indication = rememberRipple(),
+ interactionSource = interactionSource,
+ )
+ .then(
+ border?.let { Modifier.border(border = border, shape = shape) } ?: Modifier
+ )
+ .padding(contentPadding),
+ content = content
+ )
+}
+
+/**
+ * Opinionated Wear Material [Card] that offers a specific 5 slot layout to show information about
+ * an application, e.g. a notification. AppCards are designed to show interactive elements from
+ * multiple applications. They will typically be used by the system UI, e.g. for showing a list of
+ * notifications from different applications. However it could also be adapted by individual
+ * application developers to show information about different parts of their application.
+ *
+ * The first row of the layout has three slots, 1) a small optional application [Image] or [Icon],
+ * 2) an application name, it is expected to be a short start-aligned [Text] composable,
+ * and 3) the optional time that the application activity has occurred which will be
+ * shown on the top row of the card, this is expected to be an end aligned [Text] composable
+ * showing a time relevant to the contents of the [Card].
+ *
+ * The second row shows a title, this is expected to be a single row of start aligned [Text].
+ *
+ * The rest of the [Card] contains the content which can be either [Text] or an [Image].
+ * If the content is text it can be single or multiple line and is expected to be Top and Start
+ * aligned.
+ *
+ * If more than one composable is provided in the content slot it is the responsibility of the
+ * caller to determine how to layout the contents, e.g. provide either a row or a column.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param border A BorderStroke object which is used for the outline drawing.
+ * Can be null - then outline will not be drawn
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param containerPainter A Painter which is used for background drawing.
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param appImage A slot for a small [Image] associated with the application.
+ * @param appName A slot for displaying the application name, expected to be a single line of start
+ * aligned text.
+ * @param time A slot for displaying the time relevant to the contents of the card, expected to be a
+ * short piece of end aligned text.
+ * @param title A slot for displaying the title of the card, expected to be one or two lines of
+ * start aligned text.
+ * @param content A main slot for a content of this card
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+public fun AppCard(
+ onClick: () -> Unit,
+ modifier: Modifier,
+ enabled: Boolean,
+ border: BorderStroke?,
+ contentPadding: PaddingValues,
+ containerPainter: Painter,
+ interactionSource: MutableInteractionSource,
+ shape: Shape,
+ appImage: @Composable (RowScope.() -> Unit)?,
+ appName: @Composable RowScope.() -> Unit,
+ time: @Composable (RowScope.() -> Unit)?,
+ title: @Composable RowScope.() -> Unit,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Card(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ containerPainter = containerPainter,
+ border = border,
+ contentPadding = contentPadding,
+ interactionSource = interactionSource,
+ role = null,
+ shape = shape
+ ) {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ appImage?.let {
+ appImage()
+ Spacer(Modifier.width(6.dp))
+ }
+ appName()
+ Spacer(modifier = Modifier.weight(1.0f))
+
+ time?.let {
+ time()
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ content = title
+ )
+ content()
+ }
+ }
+}
+
+/**
+ * Opinionated Wear Material [Card] that offers a specific 3 slot layout to show interactive
+ * information about an application, e.g. a message. TitleCards are designed for use within an
+ * application.
+ *
+ * The first row of the layout has two slots. 1. a start aligned title. The title text is
+ * expected to be a maximum of 2 lines of text.
+ * 2. An optional time that the application activity has occurred shown at the
+ * end of the row, expected to be an end aligned [Text] composable showing a time relevant to the
+ * contents of the [Card].
+ *
+ * The rest of the [Card] contains the content which is expected to be [Text] or a contained
+ * [Image].
+ *
+ * If the content is text it can be single or multiple line and is expected to be Top and Start
+ * aligned.
+ *
+ * Overall the [title] and [content] text should be no more than 5 rows of text combined.
+ *
+ * If more than one composable is provided in the content slot it is the responsibility of the
+ * caller to determine how to layout the contents, e.g. provide either a row or a column.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param border A BorderStroke object which is used for the outline drawing.
+ * Can be null - then outline will not be drawn
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param containerPainter A Painter which is used for background drawing.
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param title A slot for displaying the title of the card, expected to be one or two
+ * lines of text.
+ * @param time An optional slot for displaying the time relevant to the contents of the card,
+ * expected to be a short piece of end aligned text.
+ * @param content A main slot for a content of this card.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Composable
+public fun TitleCard(
+ onClick: () -> Unit,
+ modifier: Modifier,
+ enabled: Boolean,
+ border: BorderStroke?,
+ contentPadding: PaddingValues,
+ containerPainter: Painter,
+ interactionSource: MutableInteractionSource,
+ shape: Shape,
+ title: @Composable RowScope.() -> Unit,
+ time: @Composable (RowScope.() -> Unit)?,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Card(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ containerPainter = containerPainter,
+ border = border,
+ contentPadding = contentPadding,
+ interactionSource = interactionSource,
+ role = null,
+ shape = shape
+ ) {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ title()
+ time?.let {
+ Spacer(modifier = Modifier.weight(1.0f))
+ time()
+ }
+ }
+ content()
+ }
+ }
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index 596a2f2..2b09353 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -17,29 +17,18 @@
package androidx.wear.compose.material
import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.paint
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
@@ -53,7 +42,6 @@
import androidx.compose.ui.graphics.painter.BrushPainter
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
@@ -113,23 +101,16 @@
role: Role? = null,
content: @Composable ColumnScope.() -> Unit,
) {
- Column(
- modifier = modifier
- .fillMaxWidth()
- .height(IntrinsicSize.Min)
- .clip(shape = shape)
- .paint(
- painter = backgroundPainter,
- contentScale = ContentScale.Crop
- )
- .clickable(
- enabled = enabled,
- onClick = onClick,
- role = role,
- indication = rememberRipple(),
- interactionSource = interactionSource,
- )
- .padding(contentPadding)
+ androidx.wear.compose.materialcore.Card(
+ onClick = onClick,
+ modifier = modifier,
+ border = null,
+ containerPainter = backgroundPainter,
+ enabled = enabled,
+ contentPadding = contentPadding,
+ shape = shape,
+ interactionSource = interactionSource,
+ role = role,
) {
CompositionLocalProvider(
LocalContentColor provides contentColor,
@@ -208,53 +189,46 @@
titleColor: Color = MaterialTheme.colors.onSurface,
content: @Composable ColumnScope.() -> Unit,
) {
- Card(
+ androidx.wear.compose.materialcore.AppCard(
onClick = onClick,
modifier = modifier,
- backgroundPainter = backgroundPainter,
enabled = enabled,
- ) {
- Column {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- CompositionLocalProvider(
- LocalTextStyle provides MaterialTheme.typography.caption1
- ) {
- if (appImage != null) {
- appImage()
- Spacer(modifier = Modifier.width(6.dp))
- }
- CompositionLocalProvider(
- LocalContentColor provides appColor,
- ) {
- appName()
- }
- }
- Spacer(modifier = Modifier.weight(1.0f))
- CompositionLocalProvider(
- LocalContentColor provides timeColor,
- LocalTextStyle provides MaterialTheme.typography.caption1,
- ) {
- time()
- }
- }
- Spacer(modifier = Modifier.height(4.dp))
- Row {
- CompositionLocalProvider(
- LocalContentColor provides titleColor,
- LocalTextStyle provides MaterialTheme.typography.title3,
- ) {
- title()
- }
- }
+ border = null,
+ contentPadding = CardDefaults.ContentPadding,
+ containerPainter = backgroundPainter,
+ interactionSource = remember { MutableInteractionSource() },
+ shape = MaterialTheme.shapes.large,
+ appImage = appImage?.let { { appImage() } },
+ appName = {
CompositionLocalProvider(
- LocalContentColor provides contentColor,
- LocalTextStyle provides MaterialTheme.typography.body1,
+ LocalContentColor provides appColor,
+ LocalTextStyle provides MaterialTheme.typography.caption1
) {
- content()
+ appName()
}
+ },
+ time = {
+ CompositionLocalProvider(
+ LocalContentColor provides timeColor,
+ LocalTextStyle provides MaterialTheme.typography.caption1,
+ ) {
+ time()
+ }
+ },
+ title = {
+ CompositionLocalProvider(
+ LocalContentColor provides titleColor,
+ LocalTextStyle provides MaterialTheme.typography.title3
+ ) {
+ title()
+ }
+ }
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides MaterialTheme.typography.body1,
+ ) {
+ content()
}
}
}
@@ -321,33 +295,35 @@
timeColor: Color = contentColor,
content: @Composable ColumnScope.() -> Unit,
) {
- Card(
+ androidx.wear.compose.materialcore.TitleCard(
onClick = onClick,
modifier = modifier,
- backgroundPainter = backgroundPainter,
enabled = enabled,
- ) {
- Column {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
+ border = null,
+ contentPadding = CardDefaults.ContentPadding,
+ containerPainter = backgroundPainter,
+ interactionSource = remember { MutableInteractionSource() },
+ shape = MaterialTheme.shapes.large,
+ title = {
+ CompositionLocalProvider(
+ LocalContentColor provides titleColor,
+ LocalTextStyle provides MaterialTheme.typography.title3,
) {
+ title()
+ }
+ },
+ time = {
+ time?.let {
+ Spacer(modifier = Modifier.weight(1.0f))
CompositionLocalProvider(
- LocalContentColor provides titleColor,
- LocalTextStyle provides MaterialTheme.typography.title3,
+ LocalContentColor provides timeColor,
+ LocalTextStyle provides MaterialTheme.typography.caption1,
) {
- title()
- }
- if (time != null) {
- Spacer(modifier = Modifier.weight(1.0f))
- CompositionLocalProvider(
- LocalContentColor provides timeColor,
- LocalTextStyle provides MaterialTheme.typography.caption1,
- ) {
- time()
- }
+ time()
}
}
+ },
+ content = {
Spacer(modifier = Modifier.height(2.dp))
CompositionLocalProvider(
LocalContentColor provides contentColor,
@@ -356,7 +332,7 @@
content()
}
}
- }
+ )
}
/**
@@ -387,12 +363,14 @@
.compositeOver(MaterialTheme.colors.background),
gradientDirection: LayoutDirection = LocalLayoutDirection.current
): Painter {
- return BrushPainter(FortyFiveDegreeLinearGradient(
- colors = listOf(
- startBackgroundColor,
- endBackgroundColor
- ),
- ltr = gradientDirection == LayoutDirection.Ltr)
+ return BrushPainter(
+ FortyFiveDegreeLinearGradient(
+ colors = listOf(
+ startBackgroundColor,
+ endBackgroundColor
+ ),
+ ltr = gradientDirection == LayoutDirection.Ltr
+ )
)
}
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index cf3b196..ffc5824 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -2,6 +2,8 @@
package androidx.wear.compose.material3 {
@androidx.compose.runtime.Immutable public final class ButtonColors {
+ ctor public ButtonColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long secondaryContentColor, long iconColor, androidx.compose.ui.graphics.painter.Painter disabledContainerPainter, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
+ ctor public ButtonColors(long containerColor, long contentColor, long secondaryContentColor, long iconColor, long disabledContainerColor, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
}
public final class ButtonDefaults {
@@ -22,14 +24,38 @@
}
public final class ButtonKt {
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ }
+
+ @androidx.compose.runtime.Immutable public final class CardColors {
+ ctor public CardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long appNameColor, long timeColor, long titleColor);
+ }
+
+ public final class CardDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors cardColors(optional long containerColor, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method public float getAppImageSize();
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors imageCardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional long outlineColor, optional float borderWidth);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors outlinedCardColors(optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ property public final float AppImageSize;
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ field public static final androidx.wear.compose.material3.CardDefaults INSTANCE;
+ }
+
+ public final class CardKt {
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public final class ColorScheme {
@@ -116,6 +142,7 @@
}
@androidx.compose.runtime.Immutable public final class IconButtonColors {
+ ctor public IconButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
}
public final class IconButtonDefaults {
diff --git a/wear/compose/compose-material3/api/public_plus_experimental_current.txt b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
index a23926c..bbe675a 100644
--- a/wear/compose/compose-material3/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material3/api/public_plus_experimental_current.txt
@@ -2,6 +2,8 @@
package androidx.wear.compose.material3 {
@androidx.compose.runtime.Immutable public final class ButtonColors {
+ ctor public ButtonColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long secondaryContentColor, long iconColor, androidx.compose.ui.graphics.painter.Painter disabledContainerPainter, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
+ ctor public ButtonColors(long containerColor, long contentColor, long secondaryContentColor, long iconColor, long disabledContainerColor, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
}
public final class ButtonDefaults {
@@ -22,14 +24,38 @@
}
public final class ButtonKt {
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ }
+
+ @androidx.compose.runtime.Immutable public final class CardColors {
+ ctor public CardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long appNameColor, long timeColor, long titleColor);
+ }
+
+ public final class CardDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors cardColors(optional long containerColor, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method public float getAppImageSize();
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors imageCardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional long outlineColor, optional float borderWidth);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors outlinedCardColors(optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ property public final float AppImageSize;
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ field public static final androidx.wear.compose.material3.CardDefaults INSTANCE;
+ }
+
+ public final class CardKt {
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public final class ColorScheme {
@@ -119,6 +145,7 @@
}
@androidx.compose.runtime.Immutable public final class IconButtonColors {
+ ctor public IconButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
}
public final class IconButtonDefaults {
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index cf3b196..ffc5824 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -2,6 +2,8 @@
package androidx.wear.compose.material3 {
@androidx.compose.runtime.Immutable public final class ButtonColors {
+ ctor public ButtonColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long secondaryContentColor, long iconColor, androidx.compose.ui.graphics.painter.Painter disabledContainerPainter, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
+ ctor public ButtonColors(long containerColor, long contentColor, long secondaryContentColor, long iconColor, long disabledContainerColor, long disabledContentColor, long disabledSecondaryContentColor, long disabledIconColor);
}
public final class ButtonDefaults {
@@ -22,14 +24,38 @@
}
public final class ButtonKt {
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ChildButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void FilledTonalButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? secondaryLabel, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+ }
+
+ @androidx.compose.runtime.Immutable public final class CardColors {
+ ctor public CardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, long contentColor, long appNameColor, long timeColor, long titleColor);
+ }
+
+ public final class CardDefaults {
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors cardColors(optional long containerColor, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method public float getAppImageSize();
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors imageCardColors(androidx.compose.ui.graphics.painter.Painter containerPainter, optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional long outlineColor, optional float borderWidth);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.CardColors outlinedCardColors(optional long contentColor, optional long appNameColor, optional long timeColor, optional long titleColor);
+ property public final float AppImageSize;
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ field public static final androidx.wear.compose.material3.CardDefaults INSTANCE;
+ }
+
+ public final class CardKt {
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void OutlinedCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.wear.compose.material3.CardColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.compose.runtime.Stable public final class ColorScheme {
@@ -116,6 +142,7 @@
}
@androidx.compose.runtime.Immutable public final class IconButtonColors {
+ ctor public IconButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
}
public final class IconButtonDefaults {
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardScreenshotTest.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardScreenshotTest.kt
new file mode 100644
index 0000000..6684dec
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardScreenshotTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.test
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.material3.AppCard
+import androidx.wear.compose.material3.Card
+import androidx.wear.compose.material3.CardColors
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.OutlinedCard
+import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
+import androidx.wear.compose.material3.TEST_TAG
+import androidx.wear.compose.material3.TestIcon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.TitleCard
+import androidx.wear.compose.material3.setContentWithTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CardScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+ @get:Rule
+ val testName = TestName()
+
+ @Test
+ fun card_ltr() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleCard()
+ }
+
+ @Test
+ fun card_disabled() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleCard(enabled = false)
+ }
+
+ @Test
+ fun card_rtl() = verifyScreenshot(layoutDirection = LayoutDirection.Rtl) {
+ sampleCard()
+ }
+
+ @Test
+ fun card_image_background() = verifyScreenshot {
+ sampleCard(
+ colors = CardDefaults.imageCardColors(
+ containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
+ backgroundImagePainter = painterResource(id = R.drawable.backgroundimage1)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun outlined_card_ltr() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleOutlinedCard()
+ }
+
+ @Test
+ fun outlined_card_disabled() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleOutlinedCard(enabled = false)
+ }
+
+ @Test
+ fun outlined_card_rtl() = verifyScreenshot(layoutDirection = LayoutDirection.Rtl) {
+ sampleOutlinedCard()
+ }
+
+ @Test
+ fun app_card_ltr() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleAppCard()
+ }
+
+ @Test
+ fun app_card_disabled() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleAppCard(enabled = false)
+ }
+
+ @Test
+ fun app_card_rtl() = verifyScreenshot(layoutDirection = LayoutDirection.Rtl) {
+ sampleAppCard()
+ }
+
+ @Test
+ fun app_card_image_background() = verifyScreenshot {
+ sampleAppCard(
+ colors = CardDefaults.imageCardColors(
+ containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
+ backgroundImagePainter = painterResource(id = R.drawable.backgroundimage1)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun title_card_ltr() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleTitleCard()
+ }
+
+ @Test
+ fun title_card_disabled() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+ sampleTitleCard(enabled = false)
+ }
+
+ @Test
+ fun title_card_rtl() = verifyScreenshot(layoutDirection = LayoutDirection.Rtl) {
+ sampleTitleCard()
+ }
+
+ @Test
+ fun title_card_image_background() = verifyScreenshot {
+ sampleTitleCard(
+ colors = CardDefaults.imageCardColors(
+ containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
+ backgroundImagePainter = painterResource(id = R.drawable.backgroundimage1)
+ )
+ )
+ )
+ }
+
+ @Composable
+ private fun sampleCard(
+ enabled: Boolean = true,
+ colors: CardColors = CardDefaults.cardColors()
+ ) {
+ Card(
+ enabled = enabled,
+ onClick = {},
+ colors = colors,
+ modifier = Modifier.testTag(TEST_TAG),
+ ) {
+ Text("Card: Some body content")
+ }
+ }
+
+ @Composable
+ private fun sampleOutlinedCard(
+ enabled: Boolean = true,
+ ) {
+ OutlinedCard(
+ enabled = enabled,
+ onClick = {},
+ modifier = Modifier.testTag(TEST_TAG),
+ ) {
+ Text("Outlined Card: Some body content")
+ }
+ }
+
+ @Composable
+ private fun sampleAppCard(
+ enabled: Boolean = true,
+ colors: CardColors = CardDefaults.cardColors()
+ ) {
+ AppCard(
+ enabled = enabled,
+ onClick = {},
+ appName = { Text("AppName") },
+ appImage = { TestIcon() },
+ title = { Text("AppCard") },
+ colors = colors,
+ time = { Text("now") },
+ modifier = Modifier.testTag(TEST_TAG),
+ ) {
+ Text("Some body content")
+ Text("and some more body content")
+ }
+ }
+
+ @Composable
+ private fun sampleTitleCard(
+ enabled: Boolean = true,
+ colors: CardColors = CardDefaults.cardColors()
+ ) {
+ TitleCard(
+ enabled = enabled,
+ onClick = {},
+ title = { Text("TitleCard") },
+ time = { Text("now") },
+ colors = colors,
+ modifier = Modifier.testTag(TEST_TAG),
+ ) {
+ Text("Some body content")
+ Text("and some more body content")
+ }
+ }
+
+ private fun verifyScreenshot(
+ layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+ content: @Composable () -> Unit
+ ) {
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+ content()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, testName.methodName)
+ }
+}
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardTest.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
new file mode 100644
index 0000000..6b01051
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
@@ -0,0 +1,565 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertContainsColor
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+public class CardTest {
+ @get:Rule
+ public val rule: ComposeContentTestRule = createComposeRule()
+
+ @Test
+ public fun supports_test_tag() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertExists()
+ }
+
+ @Test
+ public fun has_clickaction_when_enabled() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertHasClickAction()
+ }
+
+ @Test
+ public fun has_clickaction_when_disabled() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ enabled = false,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertHasClickAction()
+ }
+
+ @Test
+ public fun is_correctly_enabled_when_enabled_equals_true() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertIsEnabled()
+ }
+
+ @Test
+ public fun is_correctly_disabled_when_enabled_equals_false() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ enabled = false,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).assertIsNotEnabled()
+ }
+
+ @Test
+ public fun responds_to_click_when_enabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ Card(
+ onClick = { clicked = true },
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(true, clicked)
+ }
+ }
+
+ @Test
+ public fun does_not_respond_to_click_when_disabled() {
+ var clicked = false
+
+ rule.setContentWithTheme {
+ Card(
+ onClick = { clicked = true },
+ enabled = false,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performClick()
+
+ rule.runOnIdle {
+ assertEquals(false, clicked)
+ }
+ }
+
+ @Test
+ public fun has_role_button_if_explicitly_set() {
+ rule.setContentWithTheme {
+ Card(
+ onClick = {},
+ enabled = false,
+ modifier = Modifier.testTag(TEST_TAG)
+ .semantics { role = Role.Button },
+ ) {
+ TestImage()
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assert(
+ SemanticsMatcher.expectValue(
+ SemanticsProperties.Role,
+ Role.Button
+ )
+ )
+ }
+
+ @Test
+ public fun gives_base_card_correct_default_max_height(): Unit =
+ verifyHeight(
+ expectedHeight = 100.dp +
+ CardDefaults.ContentPadding.calculateBottomPadding() +
+ CardDefaults.ContentPadding.calculateTopPadding(),
+ imageModifier = Modifier.requiredHeight(100.dp)
+ )
+
+ @Test
+ public fun gives_enabled_default_colors(): Unit =
+ verifyColors(
+ CardStatus.Enabled,
+ ) { MaterialTheme.colorScheme.onSurfaceVariant }
+
+ @Test
+ public fun gives_disabled_default_colors(): Unit =
+ verifyColors(
+ CardStatus.Disabled,
+ ) { MaterialTheme.colorScheme.onSurfaceVariant }
+
+ @Test
+ public fun app_card_gives_default_colors() {
+ var expectedAppColor = Color.Transparent
+ var expectedTimeColor = Color.Transparent
+ var expectedTitleColor = Color.Transparent
+ var expectedContentColor = Color.Transparent
+ var actualContentColor = Color.Transparent
+ var actualTitleColor = Color.Transparent
+ var actualTimeColor = Color.Transparent
+ var actualAppColor = Color.Transparent
+ val testBackground = Color.White
+
+ rule.setContentWithTheme {
+ expectedAppColor = MaterialTheme.colorScheme.onSurfaceVariant
+ expectedTimeColor = MaterialTheme.colorScheme.onSurfaceVariant
+ expectedTitleColor = MaterialTheme.colorScheme.onSurface
+ expectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ AppCard(
+ onClick = {},
+ appName = { actualAppColor = LocalContentColor.current },
+ time = { actualTimeColor = LocalContentColor.current },
+ title = { actualTitleColor = LocalContentColor.current },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ actualContentColor = LocalContentColor.current
+ }
+ }
+ }
+
+ assertEquals(expectedAppColor, actualAppColor)
+ assertEquals(expectedTimeColor, actualTimeColor)
+ assertEquals(expectedTitleColor, actualTitleColor)
+ assertEquals(expectedContentColor, actualContentColor)
+ }
+
+ @Test
+ public fun title_card_gives_default_colors() {
+ var expectedTimeColor = Color.Transparent
+ var expectedTitleColor = Color.Transparent
+ var expectedContentColor = Color.Transparent
+ var actualContentColor = Color.Transparent
+ var actualTitleColor = Color.Transparent
+ var actualTimeColor = Color.Transparent
+ val testBackground = Color.White
+
+ rule.setContentWithTheme {
+ expectedTimeColor = MaterialTheme.colorScheme.onSurfaceVariant
+ expectedTitleColor = MaterialTheme.colorScheme.onSurface
+ expectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ TitleCard(
+ onClick = {},
+ time = { actualTimeColor = LocalContentColor.current },
+ title = { actualTitleColor = LocalContentColor.current },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ actualContentColor = LocalContentColor.current
+ }
+ }
+ }
+
+ assertEquals(expectedTimeColor, actualTimeColor)
+ assertEquals(expectedTitleColor, actualTitleColor)
+ assertEquals(expectedContentColor, actualContentColor)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ public fun outlined_card_has_outlined_border_and_transparent() {
+ val outlineColor = Color.Red
+ val testBackground = Color.Green
+
+ rule.setContentWithTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ OutlinedCard(
+ onClick = {},
+ border = CardDefaults.outlinedCardBorder(outlineColor),
+ modifier = Modifier
+ .testTag(TEST_TAG)
+ .size(100.dp)
+ .align(Alignment.Center)
+ ) {
+ }
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(outlineColor)
+ // As the color of the OutlinedCard is transparent, we expect to see a
+ // testBackground color covering everything except border.
+ rule.onNodeWithTag(TEST_TAG).captureToImage()
+ .assertColorInPercentageRange(testBackground, 93f..97f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ public fun outlined_titlecard_has_outlined_border_and_transparent() {
+ val outlineColor = Color.Red
+ val testBackground = Color.Green
+
+ rule.setContentWithTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ TitleCard(
+ onClick = {},
+ title = {},
+ border = CardDefaults.outlinedCardBorder(outlineColor),
+ colors = CardDefaults.outlinedCardColors(),
+ modifier = Modifier
+ .testTag(TEST_TAG)
+ .size(100.dp)
+ .align(Alignment.Center)
+ ) {
+ }
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(outlineColor)
+ // As the color of the OutlinedCard is transparent, we expect to see a
+ // testBackground color covering everything except border.
+ rule.onNodeWithTag(TEST_TAG).captureToImage()
+ .assertColorInPercentageRange(testBackground, 93f..97f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ public fun outlined_appcard_has_outlined_border_and_transparent() {
+ val outlineColor = Color.Red
+ val testBackground = Color.Green
+
+ rule.setContentWithTheme {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ AppCard(
+ onClick = {},
+ appName = {},
+ title = {},
+ border = CardDefaults.outlinedCardBorder(outlineColor),
+ colors = CardDefaults.outlinedCardColors(),
+ modifier = Modifier
+ .testTag(TEST_TAG)
+ .size(100.dp)
+ .align(Alignment.Center)
+ ) {
+ }
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).captureToImage().assertContainsColor(outlineColor)
+ // As the color of the OutlinedCard is transparent, we expect to see a
+ // testBackground color covering everything except border.
+ rule.onNodeWithTag(TEST_TAG).captureToImage()
+ .assertColorInPercentageRange(testBackground, 93f..97f)
+ }
+
+ @Test
+ public fun gives_correct_text_style_base() {
+ var actualTextStyle = TextStyle.Default
+ var expectedTextStyle = TextStyle.Default
+ rule.setContentWithTheme {
+ expectedTextStyle = MaterialTheme.typography.bodyLarge
+ Card(
+ onClick = {},
+ content = {
+ actualTextStyle = LocalTextStyle.current
+ },
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ assertEquals(expectedTextStyle, actualTextStyle)
+ }
+
+ @Test
+ public fun app_card_gives_correct_text_style_base() {
+ var actualAppTextStyle = TextStyle.Default
+ var actualTimeTextStyle = TextStyle.Default
+ var actualTitleTextStyle = TextStyle.Default
+ var actuaContentTextStyle = TextStyle.Default
+ var expectedAppTextStyle = TextStyle.Default
+ var expectedTimeTextStyle = TextStyle.Default
+ var expectedTitleTextStyle = TextStyle.Default
+ var expectedContentTextStyle = TextStyle.Default
+
+ rule.setContentWithTheme {
+ expectedAppTextStyle = MaterialTheme.typography.captionLarge
+ expectedTimeTextStyle = MaterialTheme.typography.captionLarge
+ expectedTitleTextStyle = MaterialTheme.typography.titleSmall
+ expectedContentTextStyle = MaterialTheme.typography.bodyLarge
+
+ AppCard(
+ onClick = {},
+ appName = {
+ actualAppTextStyle = LocalTextStyle.current
+ },
+ time = {
+ actualTimeTextStyle = LocalTextStyle.current
+ },
+ title = {
+ actualTitleTextStyle = LocalTextStyle.current
+ },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ actuaContentTextStyle = LocalTextStyle.current
+ }
+ }
+ assertEquals(expectedAppTextStyle, actualAppTextStyle)
+ assertEquals(expectedTimeTextStyle, actualTimeTextStyle)
+ assertEquals(expectedTitleTextStyle, actualTitleTextStyle)
+ assertEquals(expectedContentTextStyle, actuaContentTextStyle)
+ }
+
+ @Test
+ public fun title_card_gives_correct_text_style_base() {
+ var actualTimeTextStyle = TextStyle.Default
+ var actualTitleTextStyle = TextStyle.Default
+ var actuaContentTextStyle = TextStyle.Default
+ var expectedTimeTextStyle = TextStyle.Default
+ var expectedTitleTextStyle = TextStyle.Default
+ var expectedContentTextStyle = TextStyle.Default
+
+ rule.setContentWithTheme {
+ expectedTimeTextStyle = MaterialTheme.typography.captionLarge
+ expectedTitleTextStyle = MaterialTheme.typography.titleSmall
+ expectedContentTextStyle = MaterialTheme.typography.bodyLarge
+
+ TitleCard(
+ onClick = {},
+ time = {
+ actualTimeTextStyle = LocalTextStyle.current
+ },
+ title = {
+ actualTitleTextStyle = LocalTextStyle.current
+ },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ actuaContentTextStyle = LocalTextStyle.current
+ }
+ }
+ assertEquals(expectedTimeTextStyle, actualTimeTextStyle)
+ assertEquals(expectedTitleTextStyle, actualTitleTextStyle)
+ assertEquals(expectedContentTextStyle, actuaContentTextStyle)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ public fun outlined_app_card_gives_correct_text_style_base() {
+ var actualAppTextStyle = TextStyle.Default
+ var actualTimeTextStyle = TextStyle.Default
+ var actualTitleTextStyle = TextStyle.Default
+ var actuaContentTextStyle = TextStyle.Default
+ var expectedAppTextStyle = TextStyle.Default
+ var expectedTimeTextStyle = TextStyle.Default
+ var expectedTitleTextStyle = TextStyle.Default
+ var expectedContentTextStyle = TextStyle.Default
+
+ rule.setContentWithTheme {
+ expectedAppTextStyle = MaterialTheme.typography.captionLarge
+ expectedTimeTextStyle = MaterialTheme.typography.captionLarge
+ expectedTitleTextStyle = MaterialTheme.typography.titleSmall
+ expectedContentTextStyle = MaterialTheme.typography.bodyLarge
+
+ AppCard(
+ onClick = {},
+ appName = {
+ actualAppTextStyle = LocalTextStyle.current
+ },
+ time = {
+ actualTimeTextStyle = LocalTextStyle.current
+ },
+ title = {
+ actualTitleTextStyle = LocalTextStyle.current
+ },
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ actuaContentTextStyle = LocalTextStyle.current
+ }
+ }
+ rule.onNodeWithTag(TEST_TAG).captureToImage()
+ assertEquals(expectedAppTextStyle, actualAppTextStyle)
+ assertEquals(expectedTimeTextStyle, actualTimeTextStyle)
+ assertEquals(expectedTitleTextStyle, actualTitleTextStyle)
+ assertEquals(expectedContentTextStyle, actuaContentTextStyle)
+ }
+
+ private fun verifyHeight(expectedHeight: Dp, imageModifier: Modifier = Modifier) {
+ rule.verifyHeight(expectedHeight) {
+ Card(
+ onClick = {},
+ ) {
+ TestIcon(modifier = imageModifier)
+ }
+ }
+ }
+
+ private fun verifyColors(
+ status: CardStatus,
+ contentColor: @Composable () -> Color
+ ) {
+ var expectedContent = Color.Transparent
+ var actualContent = Color.Transparent
+ val testBackground = Color.White
+
+ rule.setContentWithTheme {
+ expectedContent = contentColor()
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ Card(
+ onClick = {},
+ content = { actualContent = LocalContentColor.current },
+ enabled = status.enabled(),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ }
+
+ assertEquals(expectedContent, actualContent)
+ }
+}
+
+private fun ComposeContentTestRule.verifyHeight(expected: Dp, content: @Composable () -> Unit) {
+ setContentWithThemeForSizeAssertions {
+ content()
+ }.assertHeightIsEqualTo(expected, Dp(1.0f))
+}
+
+private enum class CardStatus {
+ Enabled,
+ Disabled;
+
+ fun enabled() = this == Enabled
+}
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index 750e25e..2c8d474 100644
--- a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -34,19 +34,31 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertTouchHeightIsEqualTo
import androidx.compose.ui.test.assertTouchWidthIsEqualTo
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.isUnspecified
+import androidx.compose.ui.unit.toSize
import org.junit.Assert
+import kotlin.math.abs
+/**
+ * Constant to emulate very big but finite constraints
+ */
val BigTestMaxWidth = 5000.dp
val BigTestMaxHeight = 5000.dp
@@ -95,20 +107,17 @@
): SemanticsNodeInteraction {
setContent {
MaterialTheme {
- Box {
- Box(
- Modifier
- .sizeIn(
- maxWidth = parentMaxWidth,
- maxHeight = parentMaxHeight
- )
- .testTag("containerForSizeAssertion")
- ) {
- content()
- }
+ Box(
+ Modifier.sizeIn(
+ maxWidth = parentMaxWidth,
+ maxHeight = parentMaxHeight
+ ).testTag("containerForSizeAssertion")
+ ) {
+ content()
}
}
}
+
return onNodeWithTag("containerForSizeAssertion", useUnmergedTree)
}
@@ -176,8 +185,115 @@
.assertContainsColor(finalExpectedContainerColor)
}
+/**
+ * Checks that [expectedColor] is in the percentage [range] of an [ImageBitmap] color histogram
+ */
+fun ImageBitmap.assertColorInPercentageRange(
+ expectedColor: Color,
+ range: ClosedFloatingPointRange<Float> = 50.0f..100.0f
+) {
+ val histogram = histogram()
+ if (!histogram.containsKey(expectedColor)) {
+ throw AssertionError("Expected color $expectedColor was not found in the bitmap.")
+ }
+
+ ((histogram[expectedColor]!! * 100f) / (width * height)).let { actualPercent ->
+ if (actualPercent !in range) {
+ throw AssertionError(
+ "Expected color $expectedColor found " +
+ "$actualPercent%, not in the percentage range $range"
+ )
+ }
+ }
+}
+
+/**
+ * Asserts that the layout of this node has height equal to [expectedHeight].
+ *
+ * @throws AssertionError if comparison fails.
+ */
+internal fun SemanticsNodeInteraction.assertHeightIsEqualTo(
+ expectedHeight: Dp,
+ tolerance: Dp = Dp(0.5f)
+): SemanticsNodeInteraction {
+ return withUnclippedBoundsInRoot {
+ it.height.assertIsEqualTo(expectedHeight, "height", tolerance)
+ }
+}
+
+private fun SemanticsNodeInteraction.withUnclippedBoundsInRoot(
+ assertion: (DpRect) -> Unit
+): SemanticsNodeInteraction {
+ val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
+ val bounds = with(node.root!!.density) {
+ node.unclippedBoundsInRoot.let {
+ DpRect(it.left.toDp(), it.top.toDp(), it.right.toDp(), it.bottom.toDp())
+ }
+ }
+ assertion.invoke(bounds)
+ return this
+}
+
+private val SemanticsNode.unclippedBoundsInRoot: Rect
+ get() {
+ return if (layoutInfo.isPlaced) {
+ Rect(positionInRoot, size.toSize())
+ } else {
+ Dp.Unspecified.value.let { Rect(it, it, it, it) }
+ }
+ }
+
+/**
+ * Returns if this value is equal to the [reference], within a given [tolerance]. If the
+ * reference value is [Float.NaN], [Float.POSITIVE_INFINITY] or [Float.NEGATIVE_INFINITY], this
+ * only returns true if this value is exactly the same (tolerance is disregarded).
+ */
+private fun Dp.isWithinTolerance(reference: Dp, tolerance: Dp): Boolean {
+ return when {
+ reference.isUnspecified -> this.isUnspecified
+ reference.value.isInfinite() -> this.value == reference.value
+ else -> abs(this.value - reference.value) <= tolerance.value
+ }
+}
+
+/**
+ * Asserts that this value is equal to the given [expected] value.
+ *
+ * Performs the comparison with the given [tolerance] or the default one if none is provided. It is
+ * recommended to use tolerance when comparing positions and size coming from the framework as there
+ * can be rounding operation performed by individual layouts so the values can be slightly off from
+ * the expected ones.
+ *
+ * @param expected The expected value to which this one should be equal to.
+ * @param subject Used in the error message to identify which item this assertion failed on.
+ * @param tolerance The tolerance within which the values should be treated as equal.
+ *
+ * @throws AssertionError if comparison fails.
+ */
+internal fun Dp.assertIsEqualTo(expected: Dp, subject: String, tolerance: Dp = Dp(.5f)) {
+ if (!isWithinTolerance(expected, tolerance)) {
+ // Comparison failed, report the error in DPs
+ throw AssertionError(
+ "Actual $subject is $this, expected $expected (tolerance: $tolerance)"
+ )
+ }
+}
+
+private fun ImageBitmap.histogram(): MutableMap<Color, Long> {
+ val pixels = this.toPixelMap()
+ val histogram = mutableMapOf<Color, Long>()
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val color = pixels[x, y]
+ histogram[color] = histogram.getOrDefault(color, 0) + 1
+ }
+ }
+ return histogram
+}
+
internal enum class Status {
Enabled,
Disabled;
+
fun enabled() = this == Enabled
}
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
index fa6786f..7503da4 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
@@ -77,8 +77,6 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
*/
@Composable
fun Button(
@@ -90,7 +88,6 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
content: @Composable RowScope.() -> Unit,
) {
androidx.wear.compose.materialcore.Chip(
@@ -102,7 +99,7 @@
contentPadding = contentPadding,
shape = shape,
interactionSource = interactionSource,
- role = role,
+ role = Role.Button,
content = provideScopeContent(
colors.contentColor(enabled = enabled),
MaterialTheme.typography.buttonMedium,
@@ -151,8 +148,6 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
*/
@Composable
fun FilledTonalButton(
@@ -164,11 +159,9 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
content: @Composable RowScope.() -> Unit,
) = Button(
- onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, role,
- content
+ onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, content
)
/**
@@ -210,8 +203,6 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
*/
@Composable
fun OutlinedButton(
@@ -223,11 +214,9 @@
border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
content: @Composable RowScope.() -> Unit,
) = Button(
- onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, role,
- content
+ onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, content
)
/**
@@ -269,8 +258,6 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
*/
@Composable
fun ChildButton(
@@ -282,11 +269,9 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
content: @Composable RowScope.() -> Unit,
) = Button(
- onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, role,
- content
+ onClick, modifier, enabled, shape, colors, border, contentPadding, interactionSource, content
)
/**
@@ -316,8 +301,6 @@
*
* TODO(b/261838497) Add Material3 samples and UX guidance links
*
- * @param label A slot for providing the button's main label. The contents are expected to be text
- * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param secondaryLabel A slot for providing the button's secondary label. The contents are
@@ -343,12 +326,11 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
+ * @param label A slot for providing the button's main label. The contents are expected to be text
+ * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
*/
@Composable
fun Button(
- label: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
secondaryLabel: (@Composable RowScope.() -> Unit)? = null,
@@ -359,7 +341,7 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
+ label: @Composable RowScope.() -> Unit,
) {
androidx.wear.compose.materialcore.Chip(
modifier = modifier.height(ButtonDefaults.Height),
@@ -384,7 +366,7 @@
provideScopeContent(colors.iconColor(enabled), icon)
},
defaultIconSpacing = ButtonDefaults.IconSpacing,
- role = role
+ role = Role.Button
)
}
@@ -418,8 +400,6 @@
*
* TODO(b/261838497) Add Material3 samples and UX guidance links
*
- * @param label A slot for providing the button's main label. The contents are expected to be text
- * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param secondaryLabel A slot for providing the button's secondary label. The contents are
@@ -444,12 +424,11 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
+ * @param label A slot for providing the button's main label. The contents are expected to be text
+ * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
*/
@Composable
fun FilledTonalButton(
- label: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
secondaryLabel: (@Composable RowScope.() -> Unit)? = null,
@@ -460,9 +439,8 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
+ label: @Composable RowScope.() -> Unit,
) = Button(
- label,
onClick,
modifier,
secondaryLabel,
@@ -473,7 +451,7 @@
border,
contentPadding,
interactionSource,
- role
+ label
)
/**
@@ -503,8 +481,6 @@
*
* TODO(b/261838497) Add Material3 samples and UX guidance links
*
- * @param label A slot for providing the button's main label. The contents are expected to be text
- * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param secondaryLabel A slot for providing the button's secondary label. The contents are
@@ -529,12 +505,11 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
+ * @param label A slot for providing the button's main label. The contents are expected to be text
+ * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
*/
@Composable
fun OutlinedButton(
- label: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
secondaryLabel: (@Composable RowScope.() -> Unit)? = null,
@@ -545,9 +520,8 @@
border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
+ label: @Composable RowScope.() -> Unit,
) = Button(
- label,
onClick,
modifier,
secondaryLabel,
@@ -558,7 +532,7 @@
border,
contentPadding,
interactionSource,
- role
+ label,
)
/**
@@ -587,8 +561,6 @@
*
* TODO(b/261838497) Add Material3 samples and UX guidance links
*
- * @param label A slot for providing the button's main label. The contents are expected to be text
- * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
* @param onClick Will be called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param secondaryLabel A slot for providing the button's secondary label. The contents are
@@ -613,12 +585,11 @@
* [Interaction]s for this Button. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this Button in different [Interaction]s.
- * @param role Role semantics that accessibility services can use to provide more
- * context to users.
+ * @param label A slot for providing the button's main label. The contents are expected to be text
+ * which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
*/
@Composable
fun ChildButton(
- label: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
secondaryLabel: (@Composable RowScope.() -> Unit)? = null,
@@ -629,9 +600,8 @@
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- role: Role? = Role.Button,
+ label: @Composable RowScope.() -> Unit,
) = Button(
- label,
onClick,
modifier,
secondaryLabel,
@@ -642,7 +612,7 @@
border,
contentPadding,
interactionSource,
- role
+ label,
)
/**
@@ -924,7 +894,7 @@
* in different states.
*/
@Immutable
-class ButtonColors internal constructor(
+class ButtonColors constructor(
private val containerPainter: Painter,
private val contentColor: Color,
private val secondaryContentColor: Color,
@@ -946,7 +916,7 @@
* @param disabledSecondaryContentColor The content color of this [Button] when not enabled
* @param disabledIconColor The content color of this [Button] when not enabled
*/
- internal constructor(
+ constructor(
containerColor: Color,
contentColor: Color,
secondaryContentColor: Color,
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Card.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Card.kt
new file mode 100644
index 0000000..5454a04
--- /dev/null
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Card.kt
@@ -0,0 +1,552 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.materialcore.ImageWithScrimPainter
+import androidx.wear.compose.materialcore.Text
+
+/**
+ * Base level Wear Material 3 [Card] that offers a single slot to take any content.
+ *
+ * Is used as the container for more opinionated [Card] components that take specific content such
+ * as icons, images, titles, subtitles and labels.
+ *
+ * The [Card] is Rectangle shaped rounded corners by default.
+ *
+ * Cards can be enabled or disabled. A disabled card will not respond to click events.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * Wear OS Material design guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param colors [CardColors] that will be used to resolve the colors used for this card in
+ * different states. See [CardDefaults.cardColors].
+ * @param border A BorderStroke object which is used for drawing outlines.
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param content The main slot for a content of this card
+ */
+@Composable
+fun Card(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.large,
+ colors: CardColors = CardDefaults.cardColors(),
+ border: BorderStroke? = null,
+ contentPadding: PaddingValues = CardDefaults.ContentPadding,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ androidx.wear.compose.materialcore.Card(
+ onClick = onClick,
+ modifier = modifier,
+ border = border,
+ containerPainter = colors.containerPainter,
+ enabled = enabled,
+ contentPadding = contentPadding,
+ interactionSource = interactionSource,
+ role = null,
+ shape = shape,
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides MaterialTheme.typography.bodyLarge,
+ ) {
+ content()
+ }
+ }
+}
+
+/**
+ * Opinionated Wear Material 3 [Card] that offers a specific 5 slot layout to show information about
+ * an application, e.g. a notification. AppCards are designed to show interactive elements from
+ * multiple applications. They will typically be used by the system UI, e.g. for showing a list of
+ * notifications from different applications. However it could also be adapted by individual
+ * application developers to show information about different parts of their application.
+ *
+ * The first row of the layout has three slots, 1) a small optional application [Image] or [Icon] of
+ * size [CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] dp, 2) an application name
+ * (emphasised with the [CardColors.appColor()] color), it is expected to be a short start aligned
+ * [Text] composable, and 3) the time that the application activity has occurred which will be
+ * shown on the top row of the card, this is expected to be an end aligned [Text] composable
+ * showing a time relevant to the contents of the [Card].
+ *
+ * The second row shows a title, this is expected to be a single row of start aligned [Text].
+ *
+ * The rest of the [Card] contains the content which can be either [Text] or an [Image].
+ * If the content is text it can be single or multiple line and is expected to be Top and Start
+ * aligned.
+ *
+ * If more than one composable is provided in the content slot it is the responsibility of the
+ * caller to determine how to layout the contents, e.g. provide either a row or a column.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param appName A slot for displaying the application name, expected to be a single line of start
+ * aligned text of [Typography.captionLarge]
+ * @param title A slot for displaying the title of the card, expected to be one or two lines of
+ * start aligned text of [Typography.titleSmall]
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param colors [CardColors] that will be used to resolve the colors used for this card in
+ * different states. See [CardDefaults.cardColors].
+ * @param border A BorderStroke object which is used for drawing outlines.
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param appImage A slot for a small ([CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] )
+ * [Image] associated with the application.
+ * @param time A slot for displaying the time relevant to the contents of the card, expected to be a
+ * short piece of end aligned text of [Typography.captionLarge].
+ * @param content The main slot for a content of this card
+ */
+@Composable
+fun AppCard(
+ onClick: () -> Unit,
+ appName: @Composable RowScope.() -> Unit,
+ title: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.large,
+ colors: CardColors = CardDefaults.cardColors(),
+ border: BorderStroke? = null,
+ contentPadding: PaddingValues = CardDefaults.ContentPadding,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ appImage: @Composable (RowScope.() -> Unit)? = null,
+ time: @Composable (RowScope.() -> Unit)? = null,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ androidx.wear.compose.materialcore.AppCard(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ shape = shape,
+ containerPainter = colors.containerPainter,
+ border = border,
+ contentPadding = contentPadding,
+ appImage = appImage?.let { { appImage() } },
+ interactionSource = interactionSource,
+ appName = {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.appNameColor,
+ LocalTextStyle provides MaterialTheme.typography.captionLarge,
+ ) {
+ appName()
+ }
+ },
+ time = time?.let {
+ {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.timeColor,
+ LocalTextStyle provides MaterialTheme.typography.captionLarge,
+ ) {
+ time()
+ }
+ }
+ },
+ title = {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleColor,
+ LocalTextStyle provides MaterialTheme.typography.titleSmall,
+ ) {
+ title()
+ }
+ },
+ content = {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides MaterialTheme.typography.bodyLarge,
+ ) {
+ content()
+ }
+ }
+ )
+}
+
+/**
+ * Opinionated Wear Material 3 [Card] that offers a specific 3 slot layout to show interactive
+ * information about an application, e.g. a message. TitleCards are designed for use within an
+ * application.
+ *
+ * The first row of the layout has two slots. 1. a start aligned title. The title text is
+ * expected to be a maximum of 2 lines of text.
+ * 2. An optional time that the application activity has occurred shown at the
+ * end of the row, expected to be an end aligned [Text] composable showing a time relevant to the
+ * contents of the [Card].
+ *
+ * The rest of the [Card] contains the content which is expected to be [Text] or a contained
+ * [Image].
+ *
+ * If the content is text it can be single or multiple line and is expected to be Top and Start
+ * aligned and of type of [Typography.bodyMedium].
+ *
+ * Overall the [title] and [content] text should be no more than 5 rows of text combined.
+ *
+ * If more than one composable is provided in the content slot it is the responsibility of the
+ * caller to determine how to layout the contents, e.g. provide either a row or a column.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param title A slot for displaying the title of the card, expected to be one or two lines of text
+ * of [Typography.buttonMedium]
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param colors [CardColors] that will be used to resolve the colors used for this card in
+ * different states. See [CardDefaults.cardColors].
+ * @param border A BorderStroke object which is used for drawing outlines.
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param time An optional slot for displaying the time relevant to the contents of the card,
+ * expected to be a short piece of end aligned text.
+ * @param content The main slot for a content of this card
+ */
+@Composable
+fun TitleCard(
+ onClick: () -> Unit,
+ title: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.large,
+ colors: CardColors = CardDefaults.cardColors(),
+ border: BorderStroke? = null,
+ contentPadding: PaddingValues = CardDefaults.ContentPadding,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ time: @Composable (RowScope.() -> Unit)? = null,
+ content: @Composable () -> Unit,
+) {
+ androidx.wear.compose.materialcore.TitleCard(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ shape = shape,
+ border = border,
+ contentPadding = contentPadding,
+ containerPainter = colors.containerPainter,
+ interactionSource = interactionSource,
+ title = {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleColor,
+ LocalTextStyle provides MaterialTheme.typography.titleSmall,
+ ) {
+ title()
+ }
+ },
+ time = {
+ time?.let {
+ Spacer(modifier = Modifier.weight(1.0f))
+ CompositionLocalProvider(
+ LocalContentColor provides colors.timeColor,
+ LocalTextStyle provides MaterialTheme.typography.captionLarge,
+ ) {
+ time()
+ }
+ }
+ },
+ content = {
+ CompositionLocalProvider(
+ values = arrayOf(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides MaterialTheme.typography.bodyLarge
+ ),
+ content = content
+ )
+ }
+ )
+}
+
+/**
+ * Outlined Wear Material 3 [Card] that offers a single slot to take any content.
+ *
+ * Outlined [Card] components that take specific content such
+ * as icons, images, titles, subtitles and labels. Outlined Cards have a
+ * visual boundary around the container. This can emphasise the content of this card.
+ *
+ * The [Card] is Rectangle shaped with rounded corners by default.
+ *
+ * Cards can be enabled or disabled. A disabled card will not respond to click events.
+ *
+ * For more information, see the
+ * [Cards](https://developer.android.com/training/wearables/components/cards)
+ * Wear OS Material design guide.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When false, this card will not
+ * be clickable and there will be no ripple effect on click. Wear cards do not have any specific
+ * elevation or alpha differences when not enabled - they are simply not clickable.
+ * @param shape Defines the card's shape. It is strongly recommended to use the default as this
+ * shape is a key characteristic of the Wear Material Theme
+ * @param colors [CardColors] that will be used to resolve the colors used for this card in
+ * different states. See [CardDefaults.cardColors].
+ * @param border A BorderStroke object which is used for the outline drawing.
+ * @param contentPadding The spacing values to apply internally between the container and the
+ * content
+ * @param interactionSource The [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this card. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this card in different [Interaction]s.
+ * @param content The main slot for a content of this card
+ */
+@Composable
+fun OutlinedCard(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.large,
+ colors: CardColors = CardDefaults.outlinedCardColors(),
+ border: BorderStroke = CardDefaults.outlinedCardBorder(),
+ contentPadding: PaddingValues = CardDefaults.ContentPadding,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Card(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ border = border,
+ interactionSource = interactionSource,
+ contentPadding = contentPadding,
+ shape = shape,
+ content = content
+ )
+}
+
+/**
+ * Contains the default values used by [Card]
+ */
+public object CardDefaults {
+
+ /**
+ * Creates a [CardColors] that represents the default container and content colors used in a
+ * [Card], [AppCard] or [TitleCard].
+ *
+ * @param containerColor the container color of this [Card].
+ * @param contentColor the content color of this [Card].
+ * @param appNameColor the color used for appName, only applies to [AppCard].
+ * @param timeColor the color used for time, applies to [AppCard] and [TitleCard].
+ * @param titleColor the color used for title, applies to [AppCard] and [TitleCard].
+ */
+ @Composable
+ public fun cardColors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ appNameColor: Color = contentColor,
+ timeColor: Color = contentColor,
+ titleColor: Color = MaterialTheme.colorScheme.onSurface,
+ ): CardColors = CardColors(
+ containerPainter = remember(containerColor) { ColorPainter(containerColor) },
+ contentColor = contentColor,
+ appNameColor = appNameColor,
+ timeColor = timeColor,
+ titleColor = titleColor
+ )
+
+ /**
+ * Creates a [CardColors] that represents the default container and content colors used in an
+ * [OutlinedCard], outlined [AppCard] or [TitleCard].
+ *
+ * @param contentColor the content color of this [OutlinedCard].
+ * @param appNameColor the color used for appName, only applies to [AppCard].
+ * @param timeColor the color used for time, applies to [AppCard] and [TitleCard].
+ * @param titleColor the color used for title, applies to [AppCard] and [TitleCard].
+ */
+ @Composable
+ public fun outlinedCardColors(
+ contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ appNameColor: Color = contentColor,
+ timeColor: Color = contentColor,
+ titleColor: Color = MaterialTheme.colorScheme.onSurface,
+ ): CardColors = CardColors(
+ containerPainter = remember { ColorPainter(Color.Transparent) },
+ contentColor = contentColor,
+ appNameColor = appNameColor,
+ timeColor = timeColor,
+ titleColor = titleColor
+ )
+
+ /**
+ * Creates a [CardColors] that represents the default container and content colors
+ * used in a [Card], [AppCard] or [TitleCard] with Image set as a background.
+ *
+ * @param containerPainter a Painter which is used for background drawing.
+ * @param contentColor the content color of this [Card].
+ * @param appNameColor the color used for appName, only applies to [AppCard].
+ * @param timeColor the color used for time, applies to [AppCard] and [TitleCard].
+ * @param titleColor the color used for title, applies to [AppCard] and [TitleCard].
+ */
+ @Composable
+ public fun imageCardColors(
+ containerPainter: Painter,
+ contentColor: Color = MaterialTheme.colorScheme.onBackground,
+ appNameColor: Color = contentColor,
+ timeColor: Color = contentColor,
+ titleColor: Color = contentColor,
+ ): CardColors = CardColors(
+ containerPainter = containerPainter,
+ contentColor = contentColor,
+ appNameColor = appNameColor,
+ timeColor = timeColor,
+ titleColor = titleColor
+ )
+
+ /**
+ * Creates a [Painter] for the background of a [Card] that displays an Image with a scrim over
+ * the image to make sure that any content above the background will be legible.
+ *
+ * An Image background is a means to reinforce the meaning of information in a Card, e.g. To
+ * help to contextualize the information in a TitleCard
+ *
+ * Cards should have a content color that contrasts with the background image and scrim
+ *
+ * @param backgroundImagePainter The [Painter] to use to draw the background of the [Card]
+ * @param backgroundImageScrimBrush The [Brush] to use to paint a scrim over the background
+ * image to ensure that any text drawn over the image is legible
+ */
+ @Composable
+ public fun imageWithScrimBackgroundPainter(
+ backgroundImagePainter: Painter,
+ backgroundImageScrimBrush: Brush = SolidColor(OverlayScrimColor)
+ ): Painter {
+ return ImageWithScrimPainter(
+ imagePainter = backgroundImagePainter,
+ brush = backgroundImageScrimBrush
+ )
+ }
+
+ /**
+ * Creates a [BorderStroke] that represents the default border used in Outlined Cards.
+ * @param outlineColor The color to be used for drawing an outline.
+ * @param borderWidth width of the border in [Dp].
+ */
+ @Composable
+ public fun outlinedCardBorder(
+ outlineColor: Color = MaterialTheme.colorScheme.outline,
+ borderWidth: Dp = 1.dp
+ ): BorderStroke =
+ BorderStroke(borderWidth, outlineColor)
+
+ private val CardHorizontalPadding = 10.dp
+ private val CardVerticalPadding = 10.dp
+
+ private val OverlayScrimColor: Color = Color(0x99202124)
+
+ /**
+ * The default content padding used by [Card]
+ */
+ public val ContentPadding: PaddingValues = PaddingValues(
+ start = CardHorizontalPadding,
+ top = CardVerticalPadding,
+ end = CardHorizontalPadding,
+ bottom = CardVerticalPadding
+ )
+
+ /**
+ * The default size of the app icon/image when used inside a [AppCard].
+ */
+ public val AppImageSize: Dp = 16.dp
+}
+
+/**
+ * Represents Colors used in [Card].
+ * Unlike other Material 3 components, Cards do not change their color appearance when
+ * they are disabled. All colors remain the same in enabled and disabled states
+ */
+@Immutable
+public class CardColors(
+ internal val containerPainter: Painter,
+ internal val contentColor: Color,
+ internal val appNameColor: Color,
+ internal val timeColor: Color,
+ internal val titleColor: Color,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is CardColors) return false
+
+ if (containerPainter != other.containerPainter) return false
+ if (contentColor != other.contentColor) return false
+ if (appNameColor != other.appNameColor) return false
+ if (timeColor != other.timeColor) return false
+ if (titleColor != other.titleColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = containerPainter.hashCode()
+ result = 31 * result + contentColor.hashCode()
+ result = 31 * result + appNameColor.hashCode()
+ result = 31 * result + timeColor.hashCode()
+ result = 31 * result + titleColor.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
index 087dd93..d4b2a43 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
@@ -423,7 +423,7 @@
* [OutlinedIconButton].
*/
@Immutable
-class IconButtonColors internal constructor(
+class IconButtonColors constructor(
private val containerColor: Color,
private val contentColor: Color,
private val disabledContainerColor: Color,
diff --git a/wear/compose/integration-tests/macrobenchmark/build.gradle b/wear/compose/integration-tests/macrobenchmark/build.gradle
index 1e07985..414dd2b 100644
--- a/wear/compose/integration-tests/macrobenchmark/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark/build.gradle
@@ -1,5 +1,3 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -18,7 +16,7 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("com.android.test")
id("kotlin-android")
}
@@ -27,25 +25,22 @@
minSdkVersion 29
}
namespace "androidx.wear.compose.integration.macrobenchmark"
+ targetProjectPath = ":wear:compose:integration-tests:macrobenchmark-target"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
}
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
dependencies {
- androidTestImplementation(project(":benchmark:benchmark-junit4"))
- androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
- androidTestImplementation(project(":internal-testutils-macrobenchmark"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testUiautomator)
-}
-
-// Define a task dependency so the app is installed before we run macro benchmarks.
-afterEvaluate {
- tasks.getByPath(":wear:compose:integration-tests:macrobenchmark:connectedDebugAndroidTest")
- .dependsOn(
- tasks.getByPath(
- ":wear:compose:integration-tests:macrobenchmark-target:installRelease"
- )
- )
+ implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-macro-junit4"))
+ implementation(project(":internal-testutils-macrobenchmark"))
+ implementation(libs.testRules)
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testUiautomator)
}
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/wear/compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e0788d6
--- /dev/null
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?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 />
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
similarity index 100%
rename from wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
rename to wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
similarity index 100%
rename from wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
rename to wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/Common.kt
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
similarity index 100%
rename from wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
rename to wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/ScrollBenchmark.kt
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
similarity index 100%
rename from wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
rename to wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
similarity index 100%
rename from wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
rename to wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index db32f5c..e2d7a5c 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -48,7 +48,7 @@
FONT_VARIANT_BODY = 2;
// Placeholder for a custom font variant in the renderer.
- FONT_VARIANT_EXTENSION_1 = 3;
+ FONT_VARIANT_CUSTOM_1 = 3;
}
// An extensible FontVariant property.
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
index 4b4a012..010004ed 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/modifiers.proto
@@ -183,6 +183,9 @@
//
// Defaults to false (i.e. not hidden).
BoolProp hidden = 8;
+
+ // The optional identifier for the layout element.
+ string id = 9;
}
// The content transition of an element. Any update to the element or its
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityEmbeddingRule.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityEmbeddingRule.kt
index cd9fe23..70f2ba7 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityEmbeddingRule.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityEmbeddingRule.kt
@@ -31,6 +31,9 @@
* A [TestRule] that will stub out the behavior of [ActivityEmbeddingController] and
* [RuleController] with a more simple one that will support testing independent of the current
* platform.
+ *
+ * By default [ActivityEmbeddingRule] has values as if the developer has not opted in to the
+ * ActivityEmbedding feature. There are no set rules, and each [Activity] is not embedded.
*/
class ActivityEmbeddingRule : TestRule {
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
index 3bbca06..ef38d1a 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
@@ -21,17 +21,15 @@
import androidx.window.embedding.ActivityStack
/**
- * Creates an [ActivityStack] instance for testing, which defaults to an [ActivityStack] with
- * cross-process activities.
- *
- * The [activitiesInProcess] can be passed from the activity obtained from
- * [androidx.test.core.app.ActivityScenario] or even mock Activities.
+ * Creates an [ActivityStack] instance for testing. The default values are an empty list for
+ * [activitiesInProcess] but a false value for [isEmpty]. This is the same as being embedded with
+ * an [Activity] from another process.
*
* @param activitiesInProcess The [Activity] list with the same process of the host task with
- * empty list as the default value
+ * empty list as the default value.
* @param isEmpty Indicates whether this `ActivityStack` contains any [Activity] regardless of the
- * process with `false` as the default value
- * @return An [ActivityStack] instance for testing
+ * process with `false` as the default value.
+ * @return An [ActivityStack] instance for testing.
*/
@Suppress("FunctionName")
@JvmName("createTestActivityStack")
diff --git a/window/window-testing/src/main/java/androidx/window/testing/layout/DisplayFeatureTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/layout/DisplayFeatureTesting.kt
index 2a065cb..ee66c4f 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/layout/DisplayFeatureTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/layout/DisplayFeatureTesting.kt
@@ -42,10 +42,10 @@
* always cover the window in one dimension and that determines the other coordinates.
*
* @param activity that will house the [FoldingFeature].
- * @param center the center of the fold complementary to the orientation. For a [HORIZONTAL] fold,
- * this is the y-axis and for a [VERTICAL] fold this is the x-axis.
- * @param size the smaller dimension of the fold. The larger dimension always covers the entire
- * window.
+ * @param center the center of the fold complementary to the orientation in px. For a
+ * [HORIZONTAL] fold, this is the y-axis and for a [VERTICAL] fold this is the x-axis.
+ * @param size the smaller dimension of the fold in px. The larger dimension always covers the
+ * entire window.
* @param state [State] of the fold. The default value is [HALF_OPENED]
* @param orientation [Orientation] of the fold. The default value is [HORIZONTAL]
* @return [FoldingFeature] that is splitting if the width is not 0 and runs parallel to the
@@ -84,10 +84,10 @@
* always cover the window in one dimension and that determines the other coordinates.
*
* @param windowBounds that will contain the [FoldingFeature].
- * @param center the center of the fold complementary to the orientation. For a [HORIZONTAL] fold,
- * this is the y-axis and for a [VERTICAL] fold this is the x-axis.
- * @param size the smaller dimension of the fold. The larger dimension always covers the entire
- * window.
+ * @param center the center of the fold complementary to the orientation in px. For a
+ * [HORIZONTAL] fold, this is the y-axis and for a [VERTICAL] fold this is the x-axis.
+ * @param size the smaller dimension of the fold in px. The larger dimension always covers the
+ * entire window.
* @param state [State] of the fold. The default value is [HALF_OPENED]
* @param orientation [Orientation] of the fold. The default value is [HORIZONTAL]
* @return [FoldingFeature] that is splitting if the width is not 0 and runs parallel to the
diff --git a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
index 2e44ffd..ca861ee 100644
--- a/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
+++ b/work/work-testing/src/androidTest/java/androidx/work/testing/TestSchedulerTest.java
@@ -44,6 +44,7 @@
import androidx.work.testing.workers.TestWorker;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -77,6 +78,7 @@
CountingTestWorker.COUNT.set(0);
}
+ @Ignore // b/281720148
@Test
public void testWorker_shouldSucceedSynchronously()
throws InterruptedException, ExecutionException {