Merge "Add helpers to CallStats to map CallTypes to AppSearchManagerService APIs" into androidx-platform-dev
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 a58ae85..b1fcd9c 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
@@ -38,47 +38,7 @@
private const val CAPABILITY_NAME: String = "actions.intent.CREATE_CALL"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(CreateCall.Arguments::class.java, CreateCall.Arguments::Builder)
- .setOutput(CreateCall.Output::class.java)
- .bindOptionalParameter(
- "call.callFormat",
- { properties ->
- Optional.ofNullable(
- properties[CreateCall.PropertyMapStrings.CALL_FORMAT.key]
- as Property<Call.CanonicalValue.CallFormat>
- )
- },
- CreateCall.Arguments.Builder::setCallFormat,
- TypeConverters.CALL_FORMAT_PARAM_VALUE_CONVERTER,
- TypeConverters.CALL_FORMAT_ENTITY_CONVERTER
- )
- .bindRepeatedParameter(
- "call.participant",
- { properties ->
- Optional.ofNullable(
- properties[CreateCall.PropertyMapStrings.PARTICIPANT.key]
- as Property<Participant>
- )
- },
- CreateCall.Arguments.Builder::setParticipantList,
- ParticipantValue.PARAM_VALUE_CONVERTER,
- EntityConverter.of(PARTICIPANT_TYPE_SPEC)
- )
- .bindOptionalOutput(
- "call",
- { output -> Optional.ofNullable(output.call) },
- ParamValueConverter.of(CALL_TYPE_SPEC)::toParamValue
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- CreateCall.ExecutionStatus::toParamValue
- )
- .build()
-
+/** A capability corresponding to actions.intent.CREATE_CALL */
@CapabilityFactory(name = CAPABILITY_NAME)
class CreateCall private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -216,4 +176,47 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "call.callFormat",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.CALL_FORMAT.key]
+ as Property<Call.CanonicalValue.CallFormat>
+ )
+ },
+ Arguments.Builder::setCallFormat,
+ TypeConverters.CALL_FORMAT_PARAM_VALUE_CONVERTER,
+ TypeConverters.CALL_FORMAT_ENTITY_CONVERTER
+ )
+ .bindRepeatedParameter(
+ "call.participant",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.PARTICIPANT.key]
+ as Property<Participant>
+ )
+ },
+ Arguments.Builder::setParticipantList,
+ ParticipantValue.PARAM_VALUE_CONVERTER,
+ EntityConverter.of(PARTICIPANT_TYPE_SPEC)
+ )
+ .bindOptionalOutput(
+ "call",
+ { output -> Optional.ofNullable(output.call) },
+ ParamValueConverter.of(CALL_TYPE_SPEC)::toParamValue
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
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 806e5ac..61ceb00 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
@@ -39,47 +39,7 @@
private const val CAPABILITY_NAME: String = "actions.intent.CREATE_MESSAGE"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(CreateMessage.Arguments::class.java, CreateMessage.Arguments::Builder)
- .setOutput(CreateMessage.Output::class.java)
- .bindRepeatedParameter(
- "message.recipient",
- { properties ->
- Optional.ofNullable(
- properties[CreateMessage.PropertyMapStrings.RECIPIENT.key]
- as Property<Recipient>
- )
- },
- CreateMessage.Arguments.Builder::setRecipientList,
- RecipientValue.PARAM_VALUE_CONVERTER,
- EntityConverter.of(RECIPIENT_TYPE_SPEC)
- )
- .bindOptionalParameter(
- "message.text",
- { properties ->
- Optional.ofNullable(
- properties[CreateMessage.PropertyMapStrings.MESSAGE_TEXT.key]
- as Property<StringValue>
- )
- },
- CreateMessage.Arguments.Builder::setMessageText,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "message",
- { output -> Optional.ofNullable(output.message) },
- ParamValueConverter.of(MESSAGE_TYPE_SPEC)::toParamValue
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- CreateMessage.ExecutionStatus::toParamValue
- )
- .build()
-
+/** A capability corresponding to actions.intent.CREATE_MESSAGE */
@CapabilityFactory(name = CAPABILITY_NAME)
class CreateMessage private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -218,4 +178,47 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindRepeatedParameter(
+ "message.recipient",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.RECIPIENT.key]
+ as Property<Recipient>
+ )
+ },
+ Arguments.Builder::setRecipientList,
+ RecipientValue.PARAM_VALUE_CONVERTER,
+ EntityConverter.of(RECIPIENT_TYPE_SPEC)
+ )
+ .bindOptionalParameter(
+ "message.text",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.MESSAGE_TEXT.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setMessageText,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "message",
+ { output -> Optional.ofNullable(output.message) },
+ ParamValueConverter.of(MESSAGE_TYPE_SPEC)::toParamValue
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
index 3b6f3d9..d983859 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
@@ -31,7 +31,8 @@
SYNC_REQUEST_FAILURE(6),
CONFIRMATION_REQUEST_FAILURE(7),
TOUCH_EVENT_REQUEST_FAILURE(8),
- EXTERNAL_EXCEPTION(9);
+ EXTERNAL_EXCEPTION(9),
+ SESSION_ALREADY_DESTROYED(10);
private final int mCode;
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 56cf334..1f577f56 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
@@ -51,7 +51,6 @@
// single-turn capability does not have status
override val isActive: Boolean
get() = when (sessionOrchestrator.status) {
- TaskOrchestrator.Status.COMPLETED,
TaskOrchestrator.Status.DESTROYED -> false
else -> true
}
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 8b71429..790a750 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
@@ -50,7 +50,7 @@
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
- GROUND_IF_NO_IDENTIFIER,
+ GROUND_NEVER,
GenericResolverInternal.fromInventoryListener(listener),
converter,
null,
@@ -66,7 +66,7 @@
mutableTaskParamMap[paramName] =
TaskParamBinding(
paramName,
- GROUND_IF_NO_IDENTIFIER,
+ GROUND_NEVER,
GenericResolverInternal.fromInventoryListListener(listener),
converter,
null,
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 48e73b6..5b51ac3 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
@@ -68,7 +68,6 @@
internal enum class Status {
UNINITIATED,
IN_PROGRESS,
- COMPLETED,
DESTROYED,
}
/**
@@ -147,7 +146,14 @@
inProgress = true
}
try {
- if (updateRequest.assistantRequest != null) {
+ if (status == Status.DESTROYED) {
+ if (updateRequest.assistantRequest != null) {
+ FulfillmentResult(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+ .applyToCallback(updateRequest.assistantRequest.callbackInternal)
+ } else if (updateRequest.touchEventRequest != null && touchEventCallback != null) {
+ touchEventCallback!!.onError(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+ }
+ } else if (updateRequest.assistantRequest != null) {
processAssistantUpdateRequest(updateRequest.assistantRequest)
} else if (updateRequest.touchEventRequest != null) {
processTouchEventUpdateRequest(updateRequest.touchEventRequest)
@@ -187,12 +193,12 @@
FulfillmentRequest.Fulfillment.Type.SYNC -> handleSync(argumentsWrapper)
FulfillmentRequest.Fulfillment.Type.CONFIRM -> handleConfirm()
- FulfillmentRequest.Fulfillment.Type.CANCEL,
- FulfillmentRequest.Fulfillment.Type.TERMINATE,
+ FulfillmentRequest.Fulfillment.Type.CANCEL
-> {
terminate()
FulfillmentResult(FulfillmentResponse.getDefaultInstance())
}
+ else -> FulfillmentResult(ErrorStatusInternal.INVALID_REQUEST)
}
}
fulfillmentResult.applyToCallback(callback)
@@ -254,7 +260,6 @@
}
}
- // TODO: add cleanup logic if any
internal fun terminate() {
externalSession.onDestroy()
status = Status.DESTROYED
@@ -471,7 +476,7 @@
val result = invokeExternalSuspendBlock("onExecute") {
externalSession.onExecute(actionSpec.buildArguments(finalArguments))
}
- status = Status.COMPLETED
+ terminate()
val fulfillmentResponse =
FulfillmentResponse.newBuilder().setStartDictation(result.shouldStartDictation)
convertToExecutionOutput(result)?.let { fulfillmentResponse.executionOutput = it }
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
index ba5b322..18c1cab 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
@@ -19,6 +19,7 @@
import androidx.appactions.builtintypes.experimental.types.ListItem
import androidx.appactions.interaction.capabilities.core.AppEntityListener
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput
import androidx.appactions.interaction.capabilities.core.EntitySearchResult
import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.capabilities.core.HostProperties
@@ -58,6 +59,7 @@
import androidx.appactions.interaction.proto.DisambiguationData
import androidx.appactions.interaction.proto.Entity
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.CANCEL
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.SYNC
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.UNKNOWN_TYPE
import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput
@@ -431,7 +433,6 @@
buildRequestArgs(CANCEL),
callback3
)
- assertThat(callback3.receiveResponse().fulfillmentResponse).isNotNull()
assertThat(session.isActive).isFalse()
}
@@ -959,6 +960,158 @@
assertThat(callback.receiveResponse().fulfillmentResponse!!.startDictation).isTrue()
}
+ @Test
+ @kotlin.Throws(Exception::class)
+ fun fulfillmentType_finalSync_stateCleared() {
+ val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+ { _ ->
+ object : ExecutionSession {
+ override suspend fun onExecute(arguments: Arguments) =
+ ExecutionResult.Builder<Output>().build()
+ }
+ }
+ val property = mapOf(
+ "required" to Property.Builder<StringValue>().setRequired(true).build()
+ )
+ val capability: Capability =
+ createCapability(
+ property,
+ sessionFactory = sessionFactory,
+ sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionUpdaterSupplier = ::EmptyTaskUpdater,
+ )
+ val session = capability.createSession(fakeSessionId, hostProperties)
+
+ // TURN 1. Not providing all the required slots in the SYNC Request
+ val callback = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(SYNC),
+ callback,
+ )
+ assertThat(callback.receiveResponse()).isNotNull()
+ assertThat(getCurrentValues("required", session.state!!)).isEmpty()
+ assertThat(session.isActive).isEqualTo(true)
+
+ // TURN 2. Providing the required slots so that the task completes and the state gets cleared
+ val callback2 = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(SYNC,
+ "required",
+ ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+ ),
+ callback2,
+ )
+ assertThat(callback2.receiveResponse().fulfillmentResponse).isNotNull()
+ assertThat(session.isActive).isEqualTo(false)
+ }
+
+ @Test
+ @kotlin.Throws(Exception::class)
+ @Suppress("DEPRECATION") // TODO(b/279830425) implement tryExecute (INTENT_CONFIRMED can be used instead)
+ fun fulfillmentType_syncWithConfirmation_stateClearedAfterConfirmation() {
+ val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+ { _ ->
+ object : ExecutionSession {
+ override suspend fun onExecute(arguments: Arguments) =
+ ExecutionResult.Builder<Output>().build()
+ }
+ }
+ var onReadyToConfirm =
+ object : OnReadyToConfirmListenerInternal<Confirmation> {
+ override suspend fun onReadyToConfirm(args: Map<String, List<ParamValue>>):
+ ConfirmationOutput<Confirmation> {
+ return ConfirmationOutput.Builder<Confirmation>()
+ .setConfirmation(Confirmation.builder().setOptionalStringField("bar")
+ .build())
+ .build()
+ }
+ }
+ val property = mapOf(
+ "required" to Property.Builder<StringValue>().setRequired(true).build()
+ )
+ val capability: Capability =
+ createCapability(
+ property,
+ sessionFactory = sessionFactory,
+ sessionBridge = SessionBridge {
+ TaskHandler.Builder<Confirmation>()
+ .setOnReadyToConfirmListenerInternal(onReadyToConfirm)
+ .build() },
+ sessionUpdaterSupplier = ::EmptyTaskUpdater,
+ )
+ val session = capability.createSession(fakeSessionId, hostProperties)
+
+ // TURN 1. Providing all the required slots in the SYNC Request
+ val callback = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(SYNC,
+ "required",
+ ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+ ),
+ callback,
+ )
+ assertThat(callback.receiveResponse()).isNotNull()
+ assertThat(session.isActive).isEqualTo(true)
+
+ // Sending the confirmation request. After the confirm request, the session should not be
+ // active
+ val callback2 = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(Type.CONFIRM),
+ callback2
+ )
+
+ assertThat(callback2.receiveResponse().fulfillmentResponse).isNotNull()
+ assertThat(session.isActive).isEqualTo(false)
+ }
+
+ @Test
+ fun fulfillmentRequest_whenStatusDestroyed_errorReported() {
+ val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+ { _ ->
+ object : ExecutionSession {
+ override suspend fun onExecute(arguments: Arguments) =
+ ExecutionResult.Builder<Output>().build()
+ }
+ }
+ val property = mapOf(
+ "required" to Property.Builder<StringValue>().setRequired(true).build()
+ )
+ val capability: Capability =
+ createCapability(
+ property,
+ sessionFactory = sessionFactory,
+ sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+ sessionUpdaterSupplier = ::EmptyTaskUpdater,
+ )
+ val session = capability.createSession(fakeSessionId, hostProperties)
+
+ // TURN 1. Providing the required slots so that the task completes and the state gets cleared
+ val callback = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(SYNC,
+ "required",
+ ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+ ),
+ callback,
+ )
+ assertThat(callback.receiveResponse().fulfillmentResponse).isNotNull()
+ assertThat(session.isActive).isEqualTo(false)
+
+ // TURN 2. Trying to sync after the session is destroyed
+ val callback2 = FakeCallbackInternal()
+ session.execute(
+ buildRequestArgs(SYNC,
+ "required",
+ ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+ ),
+ callback2,
+ )
+ assertThat(session.isActive).isEqualTo(false)
+ assertThat(callback2.receiveResponse().errorStatus)
+ .isEqualTo(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+ }
+
/**
* an implementation of Capability.Builder using Argument. Output, etc. defined under
* testing/spec
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 d530c21..eeae92b 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
@@ -26,44 +26,9 @@
import java.time.LocalTime
import java.util.Optional
-/** GetExerciseObservation.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.GET_EXERCISE_OBSERVATION"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(
- GetExerciseObservation.Arguments::class.java,
- GetExerciseObservation.Arguments::Builder
- )
- .setOutput(GetExerciseObservation.Output::class.java)
- .bindOptionalParameter(
- "exerciseObservation.startTime",
- { properties ->
- Optional.ofNullable(
- properties[GetExerciseObservation.PropertyMapStrings.START_TIME.key]
- as Property<LocalTime>
- )
- },
- GetExerciseObservation.Arguments.Builder::setStartTime,
- TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
- TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
- )
- .bindOptionalParameter(
- "exerciseObservation.endTime",
- { properties ->
- Optional.ofNullable(
- properties[GetExerciseObservation.PropertyMapStrings.END_TIME.key]
- as Property<LocalTime>
- )
- },
- GetExerciseObservation.Arguments.Builder::setEndTime,
- TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
- TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.GET_EXERCISE_OBSERVATION */
@CapabilityFactory(name = CAPABILITY_NAME)
class GetExerciseObservation private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -134,4 +99,41 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(
+ Arguments::class.java,
+ Arguments::Builder
+ )
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "exerciseObservation.startTime",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.START_TIME.key]
+ as Property<LocalTime>
+ )
+ },
+ Arguments.Builder::setStartTime,
+ TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+ TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+ )
+ .bindOptionalParameter(
+ "exerciseObservation.endTime",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.END_TIME.key]
+ as Property<LocalTime>
+ )
+ },
+ Arguments.Builder::setEndTime,
+ TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+ TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 c7482e1..cfacf6e 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
@@ -26,46 +26,12 @@
import java.time.LocalTime
import java.util.Optional
-/** GetHealthObservation.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.GET_HEALTH_OBSERVATION"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(
- GetHealthObservation.Arguments::class.java,
- GetHealthObservation.Arguments::Builder
- )
- .setOutput(GetHealthObservation.Output::class.java)
- .bindOptionalParameter(
- "healthObservation.startTime",
- { properties ->
- Optional.ofNullable(
- properties[GetHealthObservation.PropertyMapStrings.START_TIME.key]
- as Property<LocalTime>
- )
- },
- GetHealthObservation.Arguments.Builder::setStartTime,
- TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
- TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
- )
- .bindOptionalParameter(
- "healthObservation.endTime",
- { properties ->
- Optional.ofNullable(
- properties[GetHealthObservation.PropertyMapStrings.END_TIME.key]
- as Property<LocalTime>
- )
- },
- GetHealthObservation.Arguments.Builder::setEndTime,
- TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
- TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.GET_HEALTH_OBSERVATION */
@CapabilityFactory(name = CAPABILITY_NAME)
class GetHealthObservation private constructor() {
+
internal enum class PropertyMapStrings(val key: String) {
START_TIME("healthObservation.startTime"),
END_TIME("healthObservation.endTime"),
@@ -138,4 +104,41 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(
+ Arguments::class.java,
+ Arguments::Builder
+ )
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "healthObservation.startTime",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.START_TIME.key]
+ as Property<LocalTime>
+ )
+ },
+ Arguments.Builder::setStartTime,
+ TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+ TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+ )
+ .bindOptionalParameter(
+ "healthObservation.endTime",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.END_TIME.key]
+ as Property<LocalTime>
+ )
+ },
+ Arguments.Builder::setEndTime,
+ TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+ TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 f04ceee..f8b68c1 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
@@ -26,29 +26,9 @@
import androidx.appactions.interaction.capabilities.core.properties.StringValue
import java.util.Optional
-/** PauseExercise.kt in interaction-capabilities-fitness */
private const val CAPABILITY_NAME = "actions.intent.PAUSE_EXERCISE"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(PauseExercise.Arguments::class.java, PauseExercise.Arguments::Builder)
- .setOutput(PauseExercise.Output::class.java)
- .bindOptionalParameter(
- "exercise.name",
- { properties ->
- Optional.ofNullable(
- properties[PauseExercise.PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
- },
- PauseExercise.Arguments.Builder::setName,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.PAUSE_EXERCISE */
@CapabilityFactory(name = CAPABILITY_NAME)
class PauseExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -110,4 +90,26 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "exercise.name",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.NAME.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setName,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 1881a7c..a154fc2 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
@@ -26,29 +26,9 @@
import androidx.appactions.interaction.capabilities.core.properties.StringValue
import java.util.Optional
-/** ResumeExercise.kt in interaction-capabilities-fitness */
private const val CAPABILITY_NAME = "actions.intent.RESUME_EXERCISE"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(ResumeExercise.Arguments::class.java, ResumeExercise.Arguments::Builder)
- .setOutput(ResumeExercise.Output::class.java)
- .bindOptionalParameter(
- "exercise.name",
- { properties ->
- Optional.ofNullable(
- properties[ResumeExercise.PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
- },
- ResumeExercise.Arguments.Builder::setName,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.RESUME_EXERCISE */
@CapabilityFactory(name = CAPABILITY_NAME)
class ResumeExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -110,4 +90,26 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "exercise.name",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.NAME.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setName,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 0847d33..adb2749 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
@@ -27,41 +27,9 @@
import java.time.Duration
import java.util.Optional
-/** StartExercise.kt in interaction-capabilities-fitness */
private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StartExercise.Arguments::class.java, StartExercise.Arguments::Builder)
- .setOutput(StartExercise.Output::class.java)
- .bindOptionalParameter(
- "exercise.duration",
- { properties ->
- Optional.ofNullable(
- properties[StartExercise.PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
- },
- StartExercise.Arguments.Builder::setDuration,
- TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
- TypeConverters.DURATION_ENTITY_CONVERTER
- )
- .bindOptionalParameter(
- "exercise.name",
- { properties ->
- Optional.ofNullable(
- properties[StartExercise.PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
- },
- StartExercise.Arguments.Builder::setName,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.START_EXERCISE */
@CapabilityFactory(name = CAPABILITY_NAME)
class StartExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -136,4 +104,38 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(StartExercise.Arguments::class.java, StartExercise.Arguments::Builder)
+ .setOutput(StartExercise.Output::class.java)
+ .bindOptionalParameter(
+ "exercise.duration",
+ { properties ->
+ Optional.ofNullable(
+ properties[StartExercise.PropertyMapStrings.DURATION.key]
+ as Property<Duration>
+ )
+ },
+ StartExercise.Arguments.Builder::setDuration,
+ TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+ TypeConverters.DURATION_ENTITY_CONVERTER
+ )
+ .bindOptionalParameter(
+ "exercise.name",
+ { properties ->
+ Optional.ofNullable(
+ properties[StartExercise.PropertyMapStrings.NAME.key]
+ as Property<StringValue>
+ )
+ },
+ StartExercise.Arguments.Builder::setName,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 18efde0..5bbc5eb7 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
@@ -26,29 +26,9 @@
import androidx.appactions.interaction.capabilities.core.properties.StringValue
import java.util.Optional
-/** StopExercise.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.PAUSE_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.STOP_EXERCISE"
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StopExercise.Arguments::class.java, StopExercise.Arguments::Builder)
- .setOutput(StopExercise.Output::class.java)
- .bindOptionalParameter(
- "exercise.name",
- { properties ->
- Optional.ofNullable(
- properties[StopExercise.PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
- },
- StopExercise.Arguments.Builder::setName,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER
- )
- .build()
-
+/** A capability corresponding to actions.intent.STOP_EXERCISE */
@CapabilityFactory(name = CAPABILITY_NAME)
class StopExercise private constructor() {
internal enum class PropertyMapStrings(val key: String) {
@@ -111,4 +91,26 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ // TODO(b/273602015): Update to use Name property from builtintype library.
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "exercise.name",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.NAME.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setName,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+ )
+ .build()
+ }
}
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 528a497..59ed86a 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
@@ -20,6 +20,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** PauseTimer.kt in interaction-capabilities-productivity */
private const val CAPABILITY_NAME = "actions.intent.PAUSE_TIMER"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(PauseTimer.Arguments::class.java, PauseTimer.Arguments::Builder)
- .setOutput(PauseTimer.Output::class.java)
- .bindRepeatedParameter(
- "timer",
- { properties ->
- Optional.ofNullable(
- properties[PauseTimer.PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
- },
- PauseTimer.Arguments.Builder::setTimerList,
- TimerValue.PARAM_VALUE_CONVERTER,
- TimerValue.ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- PauseTimer.ExecutionStatus::toParamValue,
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.PAUSE_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
class PauseTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
TIMER_LIST("timer.timerList"),
@@ -178,4 +155,30 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindRepeatedParameter(
+ "timer",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.TIMER_LIST.key]
+ as Property<TimerValue>
+ )
+ },
+ Arguments.Builder::setTimerList,
+ TimerValue.PARAM_VALUE_CONVERTER,
+ TimerValue.ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ 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 94d203b..e174668 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
@@ -20,6 +20,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** ResetTimer.kt in interaction-capabilities-productivity */
private const val CAPABILITY_NAME = "actions.intent.RESET_TIMER"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(ResetTimer.Arguments::class.java, ResetTimer.Arguments::Builder)
- .setOutput(ResetTimer.Output::class.java)
- .bindRepeatedParameter(
- "timer",
- { properties ->
- Optional.ofNullable(
- properties[ResetTimer.PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
- },
- ResetTimer.Arguments.Builder::setTimerList,
- TimerValue.PARAM_VALUE_CONVERTER,
- TimerValue.ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- ResetTimer.ExecutionStatus::toParamValue
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.RESET_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
class ResetTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindRepeatedParameter(
+ "timer",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.TIMER_LIST.key]
+ as Property<TimerValue>
+ )
+ },
+ Arguments.Builder::setTimerList,
+ TimerValue.PARAM_VALUE_CONVERTER,
+ TimerValue.ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
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 d967e45..a190cc8 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
@@ -20,6 +20,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** ResumeTimer.kt in interaction-capabilities-productivity */
private const val CAPABILITY_NAME = "actions.intent.RESUME_TIMER"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(ResumeTimer.Arguments::class.java, ResumeTimer.Arguments::Builder)
- .setOutput(ResumeTimer.Output::class.java)
- .bindRepeatedParameter(
- "timer",
- { properties ->
- Optional.ofNullable(
- properties[ResumeTimer.PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
- },
- ResumeTimer.Arguments.Builder::setTimerList,
- TimerValue.PARAM_VALUE_CONVERTER,
- TimerValue.ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- ResumeTimer.ExecutionStatus::toParamValue
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.RESUME_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
class ResumeTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindRepeatedParameter(
+ "timer",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.TIMER_LIST.key]
+ as Property<TimerValue>
+ )
+ },
+ Arguments.Builder::setTimerList,
+ TimerValue.PARAM_VALUE_CONVERTER,
+ TimerValue.ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
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 fcd211c..7d71c22 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
@@ -20,6 +20,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.HostProperties
import androidx.appactions.interaction.capabilities.core.ValueListener
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
@@ -35,78 +36,10 @@
import java.time.Duration
import java.util.Optional
-/** StartTimer.kt in interaction-capabilities-productivity */
private const val CAPABILITY_NAME = "actions.intent.START_TIMER"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StartTimer.Arguments::class.java, StartTimer.Arguments::Builder)
- .setOutput(StartTimer.Output::class.java)
- .bindOptionalParameter(
- "timer.identifier",
- { properties ->
- Optional.ofNullable(
- properties[StartTimer.PropertyMapStrings.IDENTIFIER.key]
- as Property<StringValue>
- )
- },
- StartTimer.Arguments.Builder::setIdentifier,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
- )
- .bindOptionalParameter(
- "timer.name",
- { properties ->
- Optional.ofNullable(
- properties[StartTimer.PropertyMapStrings.NAME.key]
- as Property<StringValue>
- )
- },
- StartTimer.Arguments.Builder::setName,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
- )
- .bindOptionalParameter(
- "timer.duration",
- { properties ->
- Optional.ofNullable(
- properties[StartTimer.PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
- },
- StartTimer.Arguments.Builder::setDuration,
- TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
- TypeConverters.DURATION_ENTITY_CONVERTER,
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StartTimer.ExecutionStatus::toParamValue,
- )
- .build()
-
-private val SESSION_BRIDGE = SessionBridge<StartTimer.ExecutionSession, StartTimer.Confirmation> {
- session ->
- val taskHandlerBuilder = TaskHandler.Builder<StartTimer.Confirmation>()
- session.nameListener?.let {
- taskHandlerBuilder.registerValueTaskParam(
- "timer.name",
- it,
- TypeConverters.STRING_PARAM_VALUE_CONVERTER,
- )
- }
- session.durationListener?.let {
- taskHandlerBuilder.registerValueTaskParam(
- "timer.duration",
- it,
- TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
- )
- }
- taskHandlerBuilder.build()
-}
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StartTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
TIMER_LIST("timer.timerList"),
@@ -265,4 +198,74 @@
}
class Confirmation internal constructor()
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "timer.identifier",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.IDENTIFIER.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setIdentifier,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+ )
+ .bindOptionalParameter(
+ "timer.name",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.NAME.key]
+ as Property<StringValue>
+ )
+ },
+ Arguments.Builder::setName,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+ )
+ .bindOptionalParameter(
+ "timer.duration",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.DURATION.key]
+ as Property<Duration>
+ )
+ },
+ Arguments.Builder::setDuration,
+ TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+ TypeConverters.DURATION_ENTITY_CONVERTER,
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue,
+ )
+ .build()
+
+ private val SESSION_BRIDGE = SessionBridge<ExecutionSession, Confirmation> {
+ session ->
+ val taskHandlerBuilder = TaskHandler.Builder<Confirmation>()
+ session.nameListener?.let {
+ taskHandlerBuilder.registerValueTaskParam(
+ "timer.name",
+ it,
+ TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+ )
+ }
+ session.durationListener?.let {
+ taskHandlerBuilder.registerValueTaskParam(
+ "timer.duration",
+ it,
+ 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 93f5b75..394935b 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
@@ -20,6 +20,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** StopTimer.kt in interaction-capabilities-productivity */
private const val CAPABILITY_NAME = "actions.intent.STOP_TIMER"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StopTimer.Arguments::class.java, StopTimer.Arguments::Builder)
- .setOutput(StopTimer.Output::class.java)
- .bindRepeatedParameter(
- "timer",
- { properties ->
- Optional.ofNullable(
- properties[StopTimer.PropertyMapStrings.TIMER_LIST.key]
- as Property<TimerValue>
- )
- },
- StopTimer.Arguments.Builder::setTimerList,
- TimerValue.PARAM_VALUE_CONVERTER,
- TimerValue.ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StopTimer.ExecutionStatus::toParamValue
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StopTimer private constructor() {
internal enum class PropertyMapStrings(val key: String) {
TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindRepeatedParameter(
+ "timer",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.TIMER_LIST.key]
+ as Property<TimerValue>
+ )
+ },
+ Arguments.Builder::setTimerList,
+ TimerValue.PARAM_VALUE_CONVERTER,
+ TimerValue.ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
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 11e0682..9152d36 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
@@ -21,6 +21,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,24 +34,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** StartEmergencySharing.kt in interaction-capabilities-safety */
private const val CAPABILITY_NAME = "actions.intent.START_EMERGENCY_SHARING"
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(
- StartEmergencySharing.Arguments::class.java,
- StartEmergencySharing.Arguments::Builder
- )
- .setOutput(StartEmergencySharing.Output::class.java)
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StartEmergencySharing.ExecutionStatus::toParamValue,
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_EMERGENCY_SHARING */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StartEmergencySharing private constructor() {
// TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
@@ -167,4 +154,20 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(
+ Arguments::class.java,
+ Arguments::Builder
+ )
+ .setOutput(Output::class.java)
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue,
+ )
+ .build()
+ }
}
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 9bb6f01..133dfde 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
@@ -23,6 +23,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
@@ -39,51 +40,10 @@
import java.time.ZonedDateTime
import java.util.Optional
-/** StartSafetyCheck.kt in interaction-capabilities-safety */
private const val CAPABILITY_NAME = "actions.intent.START_SAFETY_CHECK"
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StartSafetyCheck.Arguments::class.java, StartSafetyCheck.Arguments::Builder)
- .setOutput(StartSafetyCheck.Output::class.java)
- .bindOptionalParameter(
- "safetyCheck.duration",
- { properties ->
- Optional.ofNullable(
- properties[StartSafetyCheck.PropertyMapStrings.DURATION.key]
- as Property<Duration>
- )
- },
- StartSafetyCheck.Arguments.Builder::setDuration,
- TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
- TypeConverters.DURATION_ENTITY_CONVERTER
- )
- .bindOptionalParameter(
- "safetyCheck.checkInTime",
- { property ->
- Optional.ofNullable(
- property[StartSafetyCheck.PropertyMapStrings.CHECK_IN_TIME.key]
- as Property<ZonedDateTime>
- )
- },
- StartSafetyCheck.Arguments.Builder::setCheckInTime,
- TypeConverters.ZONED_DATETIME_PARAM_VALUE_CONVERTER,
- TypeConverters.ZONED_DATETIME_ENTITY_CONVERTER
- )
- .bindOptionalOutput(
- "safetyCheck",
- { output -> Optional.ofNullable(output.safetyCheck) },
- ParamValueConverter.of(SAFETY_CHECK_TYPE_SPEC)::toParamValue
- )
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StartSafetyCheck.ExecutionStatus::toParamValue
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_SAFETY_CHECK */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StartSafetyCheck private constructor() {
internal enum class PropertyMapStrings(val key: String) {
DURATION("safetycheck.duration"),
@@ -265,4 +225,47 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalParameter(
+ "safetyCheck.duration",
+ { properties ->
+ Optional.ofNullable(
+ properties[PropertyMapStrings.DURATION.key]
+ as Property<Duration>
+ )
+ },
+ Arguments.Builder::setDuration,
+ TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+ TypeConverters.DURATION_ENTITY_CONVERTER
+ )
+ .bindOptionalParameter(
+ "safetyCheck.checkInTime",
+ { property ->
+ Optional.ofNullable(
+ property[PropertyMapStrings.CHECK_IN_TIME.key]
+ as Property<ZonedDateTime>
+ )
+ },
+ Arguments.Builder::setCheckInTime,
+ TypeConverters.ZONED_DATETIME_PARAM_VALUE_CONVERTER,
+ TypeConverters.ZONED_DATETIME_ENTITY_CONVERTER
+ )
+ .bindOptionalOutput(
+ "safetyCheck",
+ { output -> Optional.ofNullable(output.safetyCheck) },
+ ParamValueConverter.of(SAFETY_CHECK_TYPE_SPEC)::toParamValue
+ )
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
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 731aff5..88fe5f2 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
@@ -22,6 +22,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,24 +34,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** StopEmergencySharing.kt in interaction-capabilities-safety */
private const val CAPABILITY_NAME = "actions.intent.STOP_EMERGENCY_SHARING"
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(
- StopEmergencySharing.Arguments::class.java,
- StopEmergencySharing.Arguments::Builder
- )
- .setOutput(StopEmergencySharing.Output::class.java)
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StopEmergencySharing.ExecutionStatus::toParamValue,
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_EMERGENCY_SHARING */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StopEmergencySharing private constructor() {
// TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
@@ -168,4 +155,20 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(
+ Arguments::class.java,
+ Arguments::Builder
+ )
+ .setOutput(Output::class.java)
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue,
+ )
+ .build()
+ }
}
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 3d3ffed..c1d9aef 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
@@ -22,6 +22,7 @@
import androidx.appactions.builtintypes.experimental.types.SuccessStatus
import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,21 +34,10 @@
import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
-/** StopSafetyCheck.kt in interaction-capabilities-safety */
private const val CAPABILITY_NAME = "actions.intent.STOP_SAFETY_CHECK"
-private val ACTION_SPEC =
- ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
- .setArguments(StopSafetyCheck.Arguments::class.java, StopSafetyCheck.Arguments::Builder)
- .setOutput(StopSafetyCheck.Output::class.java)
- .bindOptionalOutput(
- "executionStatus",
- { output -> Optional.ofNullable(output.executionStatus) },
- StopSafetyCheck.ExecutionStatus::toParamValue
- )
- .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_SAFETY_CHECK */
+@CapabilityFactory(name = CAPABILITY_NAME)
class StopSafetyCheck private constructor() {
// TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
@@ -164,4 +154,17 @@
class Confirmation internal constructor()
sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+ companion object {
+ private val ACTION_SPEC =
+ ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setArguments(Arguments::class.java, Arguments::Builder)
+ .setOutput(Output::class.java)
+ .bindOptionalOutput(
+ "executionStatus",
+ { output -> Optional.ofNullable(output.executionStatus) },
+ ExecutionStatus::toParamValue
+ )
+ .build()
+ }
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 32f63ba..f60a494 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -161,6 +161,7 @@
error.add("ModifierNodeInspectableProperties")
error.add("ModifierParameter")
error.add("MutableCollectionMutableState")
+ error.add("OpaqueUnitKey")
error.add("UnnecessaryComposedModifier")
error.add("FrequentlyChangedStateReadInComposition")
error.add("ReturnFromAwaitPointerEventScope")
diff --git a/buildSrc/repos.gradle b/buildSrc/repos.gradle
index 9b5077d..4ae93fa 100644
--- a/buildSrc/repos.gradle
+++ b/buildSrc/repos.gradle
@@ -65,6 +65,13 @@
url("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
handler.mavenLocal()
+ // TODO(b/280646217): Remove after official release to gmaven.
+ handler.maven {
+ url("https://storage.googleapis.com/r8-releases/raw")
+ content {
+ includeModule("com.android.tools", "r8")
+ }
+ }
}
// Ordering appears to be important: b/229733266
def androidPluginRepoOverride = System.getenv("GRADLE_PLUGIN_REPO")
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index 288929c..5a0fb78 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -180,6 +180,10 @@
cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
}
+
+ if (::basicExtenderSessionProcessor.isInitialized) {
+ basicExtenderSessionProcessor.deInitSession()
+ }
}
@Test
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 6bbe1fe..8366234 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -847,29 +847,6 @@
}
@Test
- fun mute_defaultToNotMuted() {
- // Arrange.
- val recorder = createRecorder()
- val recording = createRecordingProcess(recorder = recorder)
- val recording2 = createRecordingProcess(recorder = recorder)
-
- // Act.
- recording.startAndVerify()
- recording.mute(true)
- recording.stopAndVerify()
-
- recording2.startAndVerify()
- recording2.verifyStatus(5) { statusList ->
- // Assert.
- statusList.forEach {
- assertThat(it.recordingStats.audioStats.audioState)
- .isEqualTo(AudioStats.AUDIO_STATE_ACTIVE)
- }
- }
- recording2.stopAndVerify()
- }
-
- @Test
fun optionsOverridesDefaults() {
val qualitySelector = QualitySelector.from(Quality.HIGHEST)
val recorder = createRecorder(qualitySelector = qualitySelector)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 2a79dbd..7e62cf4 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -846,6 +846,73 @@
file2.delete()
}
+ @Test
+ fun mute_defaultToNotMuted() {
+ assumeTrue("Audio stream is not available", audioStreamAvailable)
+
+ // Arrange.
+ val recorder = Recorder.Builder().build()
+ val videoCaptureLocal = VideoCapture.withOutput(recorder)
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ videoCaptureLocal
+ )
+ }
+ val file1 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+
+ recorder.prepareRecording(context, FileOutputOptions.Builder(file1).build())
+ .withAudioEnabled()
+ .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).use {
+ mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+ // Keep the first recording muted.
+ it.mute(true)
+ }
+
+ mockVideoRecordEventConsumer.verifyAcceptCall(
+ VideoRecordEvent.Finalize::class.java,
+ false,
+ GENERAL_TIMEOUT
+ )
+ file1.delete()
+
+ mockVideoRecordEventConsumer.clearAcceptCalls()
+
+ val file2 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+
+ // Act.
+ recorder.prepareRecording(context, FileOutputOptions.Builder(file2).build())
+ .withAudioEnabled()
+ .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).use {
+ mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+ val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+ VideoRecordEvent::class.java.isInstance(
+ argument
+ )
+ }
+ mockVideoRecordEventConsumer.verifyAcceptCall(
+ VideoRecordEvent::class.java,
+ false,
+ CallTimesAtLeast(1),
+ captor
+ )
+ assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
+ val status = captor.value as VideoRecordEvent.Status
+ // Assert: The second recording should not be muted.
+ assertThat(status.recordingStats.audioStats.audioState)
+ .isEqualTo(AudioStats.AUDIO_STATE_ACTIVE)
+ }
+
+ mockVideoRecordEventConsumer.verifyAcceptCall(
+ VideoRecordEvent.Finalize::class.java,
+ false,
+ GENERAL_TIMEOUT
+ )
+ file2.delete()
+ }
+
private fun performRecording(
videoCapture: VideoCapture<Recorder>,
file: File,
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
index af3ed21..58e7d60 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.video.internal
+import android.os.Build
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -35,6 +36,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import org.junit.Assume.assumeFalse
import org.junit.Test
import org.junit.runner.RunWith
@@ -289,6 +291,11 @@
@Test
@LargeTest
fun finalizeClosesUnclosedInstances() = runBlocking {
+ assumeFalse(
+ "Ignore devices that get flaky result. See b/278842333",
+ isModel("moto c") || isModel("rne-l23")
+ )
+
val buf = ByteBuffer.allocate(0)
val closeActionDeferred = CompletableDeferred<Unit>()
val origBuf = SharedByteBuffer.newSharedInstance(buf, CameraXExecutors.directExecutor()) {
@@ -327,4 +334,6 @@
phantomReferences.forEach { it.clear() }
}
}
+
+ private fun isModel(model: String) = model.equals(Build.MODEL, true)
}
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
index 26d932d..2fb1a60 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
@@ -19,6 +19,7 @@
import android.Manifest
import android.content.Context
import android.content.Intent
+import android.os.Build
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -38,7 +39,7 @@
import java.util.concurrent.TimeUnit
import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.After
-import org.junit.Assume
+import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -94,7 +95,12 @@
@Before
fun setUp() {
- Assume.assumeTrue(CameraUtil.deviceHasCamera())
+ assumeTrue(CameraUtil.deviceHasCamera())
+ assumeFalse(
+ "See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+ " the test failure.",
+ Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30
+ )
CoreAppTestUtil.assumeCompatibleDevice()
// Use the natural orientation throughout these tests to ensure the activity isn't
// recreated unexpectedly. This will also freeze the sensors until
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index 056a07e..8aceeb4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -44,7 +44,6 @@
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.hamcrest.CoreMatchers.equalTo
@@ -140,11 +139,6 @@
cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
}
-
- if (selectorName == "front" && implName == CameraPipeConfig::class.simpleName) {
- // TODO(b/264332446): Replace this delay with some API like closeAll() once available
- delay(5000)
- }
}
@Test
@@ -381,8 +375,8 @@
)
cameraCharacteristics?.run {
(if (flags.hasFlag(FLAG_AF)) (get(CONTROL_MAX_REGIONS_AF)!! > 0) else false) ||
- (if (flags.hasFlag(FLAG_AE)) (get(CONTROL_MAX_REGIONS_AE)!! > 0) else false) ||
- (if (flags.hasFlag(FLAG_AWB)) (get(CONTROL_MAX_REGIONS_AWB)!! > 0) else false)
+ (if (flags.hasFlag(FLAG_AE)) (get(CONTROL_MAX_REGIONS_AE)!! > 0) else false) ||
+ (if (flags.hasFlag(FLAG_AWB)) (get(CONTROL_MAX_REGIONS_AWB)!! > 0) else false)
} ?: false
} catch (e: Exception) {
false
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
index 1b89972..7ed59c5 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
@@ -56,7 +56,6 @@
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
@@ -134,11 +133,6 @@
cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
}
-
- if (selectorName == "front" && implName == CameraPipeConfig::class.simpleName) {
- // TODO(b/264332446): Replace this delay with some API like closeAll() once available
- delay(5000)
- }
}
@Test
@@ -186,7 +180,8 @@
*/
try {
cameraControl.setZoomRatio(maxZoomRatio + 1.0f)[5, TimeUnit.SECONDS]
- } catch (_: ExecutionException) {}
+ } catch (_: ExecutionException) {
+ }
assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
}
@@ -213,7 +208,8 @@
*/
try {
cameraControl.setZoomRatio(minZoomRatio - 1.0f)[5, TimeUnit.SECONDS]
- } catch (_: ExecutionException) {}
+ } catch (_: ExecutionException) {
+ }
assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
}
@@ -425,7 +421,8 @@
*/
try {
cameraControl.setLinearZoom(1.1f)[5, TimeUnit.SECONDS]
- } catch (_: ExecutionException) {}
+ } catch (_: ExecutionException) {
+ }
assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
}
@@ -448,7 +445,8 @@
*/
try {
cameraControl.setLinearZoom(-0.1f)[5, TimeUnit.SECONDS]
- } catch (_: ExecutionException) {}
+ } catch (_: ExecutionException) {
+ }
assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
}
@@ -761,8 +759,10 @@
private val failureException =
TimeoutException("Test doesn't complete after waiting for $captureCount frames.")
- @Volatile private var startReceiving = false
- @Volatile private var _verifyBlock: (
+ @Volatile
+ private var startReceiving = false
+ @Volatile
+ private var _verifyBlock: (
captureRequest: CaptureRequest,
captureResult: TotalCaptureResult
) -> Boolean = { _, _ -> false }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
index 7165a2e..b8d8429 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
@@ -16,9 +16,11 @@
package androidx.camera.integration.uiwidgets.rotations
+import android.os.Build
import androidx.test.core.app.ActivityScenario
import androidx.test.filters.LargeTest
import org.junit.After
+import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -54,6 +56,11 @@
@Before
fun before() {
+ Assume.assumeFalse(
+ "See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+ " the test failure.",
+ Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30
+ )
setUp(lensFacing)
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
index a0204db..aaedb05 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.integration.uiwidgets.rotations
import android.app.Instrumentation
+import android.os.Build
import androidx.camera.core.CameraSelector
import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.IMAGE_CAPTURE_MODE_FILE
import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.IMAGE_CAPTURE_MODE_IN_MEMORY
@@ -25,6 +26,7 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
+import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -87,6 +89,9 @@
@Before
fun before() {
+ assumeFalse("See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+ " the test failure.",
+ Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30)
setUp(lensFacing)
}
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 2482074..4777d20 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -15,9 +15,8 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,65 +24,15 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
- if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.1.0")
-
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation("androidx.compose.ui:ui:1.2.1")
- implementation("androidx.compose.ui:ui-unit:1.2.1")
- implementation("androidx.compose.ui:ui-util:1.2.1")
- implementation(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinCoroutinesCore)
-
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(project(":compose:animation:animation"))
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
- androidTestImplementation(project(":compose:test-utils"))
-
- lintPublish project(":compose:animation:animation-core-lint")
-
- samples(project(":compose:animation:animation-core:animation-core-samples"))
- }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
-
- jvmMain {
- dependencies {
- implementation(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
- }
- }
-
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-unit"))
@@ -91,45 +40,86 @@
implementation(libs.kotlinStdlibCommon)
api(libs.kotlinCoroutinesCore)
}
+ }
- androidMain {
- dependencies {
- api("androidx.annotation:annotation:1.1.0")
- implementation(libs.kotlinStdlib)
- }
+ commonTest {
+ dependencies {
}
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesCore)
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+ if (desktopEnabled) {
desktopMain {
+ dependsOn(jvmMain)
dependencies {
implementation(libs.kotlinStdlib)
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-util"))
}
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.testCore)
+ implementation(libs.junit)
+ implementation(project(":compose:animation:animation"))
+ implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
+ implementation(project(":compose:test-utils"))
+ }
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
implementation(libs.truth)
implementation(libs.kotlinCoroutinesCore)
}
+ }
- androidAndroidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.testCore)
- implementation(libs.junit)
- implementation(project(":compose:animation:animation"))
- implementation(project(":compose:ui:ui-test-junit4"))
- implementation(project(":compose:test-utils"))
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
}
}
}
- dependencies {
- samples(project(":compose:animation:animation-core:animation-core-samples"))
- }
+}
+
+dependencies {
+ lintPublish project(":compose:animation:animation-core-lint")
}
androidx {
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index c6597a7..bb4c4c1 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -15,7 +15,7 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -25,55 +25,15 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.1.0")
- api(project(":compose:animation:animation"))
- api("androidx.compose.foundation:foundation-layout:1.2.1")
- api("androidx.compose.runtime:runtime:1.2.1")
- api("androidx.compose.ui:ui:1.2.1")
- api("androidx.compose.ui:ui-geometry:1.2.1")
-
- implementation("androidx.compose.ui:ui-util:1.2.1")
- implementation(libs.kotlinStdlibCommon)
- implementation("androidx.core:core-ktx:1.5.0")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
-
- androidTestImplementation("androidx.compose.foundation:foundation:1.2.1")
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
-
- samples(project(":compose:animation:animation-graphics:animation-graphics-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:animation:animation"))
@@ -84,39 +44,84 @@
implementation(project(":compose:ui:ui-util"))
}
+ }
+ androidMain.dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.core:core-ktx:1.5.0")
+ }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
- implementation("androidx.core:core-ktx:1.5.0")
+ commonTest {
+ dependencies {
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ api(project(":compose:foundation:foundation-layout"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-geometry"))
+ implementation(project(":compose:ui:ui-util"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
implementation(libs.truth)
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:ui:ui-test-junit4"))
+ implementation("androidx.compose.foundation:foundation:1.2.1")
+ implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
implementation(project(":compose:test-utils"))
}
}
- }
- dependencies {
- samples(project(":compose:animation:animation-graphics:animation-graphics-samples"))
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 8d6b39f..f2b4f811 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,56 +23,15 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.1.0")
- api(project(":compose:animation:animation-core"))
- api("androidx.compose.foundation:foundation-layout:1.2.1")
- api("androidx.compose.runtime:runtime:1.2.1")
- api("androidx.compose.ui:ui:1.2.1")
- api("androidx.compose.ui:ui-geometry:1.2.1")
-
- implementation("androidx.compose.ui:ui-util:1.2.1")
- implementation(libs.kotlinStdlibCommon)
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
-
- androidTestImplementation("androidx.compose.foundation:foundation:1.2.1")
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
-
- lintPublish project(":compose:animation:animation-lint")
-
- samples(project(":compose:animation:animation:animation-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:animation:animation-core"))
@@ -85,39 +42,84 @@
implementation(project(":compose:ui:ui-util"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+
+ api(project(":compose:foundation:foundation-layout"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-geometry"))
+
+ implementation(project(":compose:ui:ui-util"))
+ }
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
+ jvmTest {
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
implementation(libs.truth)
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:ui:ui-test-junit4"))
+ implementation("androidx.compose.foundation:foundation:1.2.1")
+ implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
implementation(project(":compose:test-utils"))
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
- dependencies {
- samples(project(":compose:animation:animation:animation-samples"))
- }
+}
+
+dependencies {
+ lintPublish project(":compose:animation:animation-lint")
}
androidx {
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 4339957..c8e0c04 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -111,12 +111,6 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
- }
-
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
- }
-
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 3e7c323..3d72dbe 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -114,7 +114,7 @@
@kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
+ @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
}
public final class FlowLayoutKt {
@@ -122,7 +122,7 @@
method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable public static inline void FlowRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional int maxItemsInEachRow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.FlowRowScope,kotlin.Unit> content);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
+ @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
}
public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 2b75f19..03cdfb3 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -114,17 +114,11 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
- }
-
public final class FlowLayoutKt {
method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy columnMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, int maxItemsInMainAxis);
method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rowMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, int maxItemsInMainAxis);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
- }
-
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index a754486..872eef6 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,94 +23,66 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
-
- api("androidx.annotation:annotation:1.1.0")
- api(project(":compose:ui:ui"))
- api("androidx.compose.ui:ui-unit:1.2.1")
-
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation("androidx.compose.ui:ui-util:1.2.1")
- implementation("androidx.core:core:1.7.0")
- implementation("androidx.compose.animation:animation-core:1.2.1")
- implementation(libs.kotlinStdlibCommon)
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
- // old version of common-java8 conflicts with newer version, because both have
- // DefaultLifecycleEventObserver.
- // Outside of androidx this is resolved via constraint added to lifecycle-common,
- // but it doesn't work in androidx.
- // See aosp/1804059
- androidTestImplementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
-
- samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-util"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core:1.7.0")
implementation("androidx.compose.animation:animation-core:1.2.1")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-util"))
+ }
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ jvmTest {
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:ui:ui-test-junit4"))
+ implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
implementation(project(":compose:test-utils"))
implementation("androidx.activity:activity-compose:1.3.1")
@@ -121,9 +92,26 @@
implementation(libs.truth)
}
}
- }
- dependencies {
- samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
index 5d58064..cdec1d5 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
@@ -126,7 +126,7 @@
*/
@LayoutScopeMarker
@Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
interface FlowRowScope : RowScope
/**
@@ -134,11 +134,13 @@
*/
@LayoutScopeMarker
@Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
interface FlowColumnScope : ColumnScope
+@OptIn(ExperimentalLayoutApi::class)
internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope
+@OptIn(ExperimentalLayoutApi::class)
internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope
private fun getVerticalArrangement(verticalArrangement: Arrangement.Vertical):
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index 78210e2..b0a80c7 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -38,7 +38,6 @@
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
/**
@@ -768,12 +767,12 @@
private val Density.targetConstraints: Constraints
get() {
val maxWidth = if (maxWidth != Dp.Unspecified) {
- maxWidth.coerceAtLeast(0.dp).roundToPx()
+ maxWidth.roundToPx().coerceAtLeast(0)
} else {
Constraints.Infinity
}
val maxHeight = if (maxHeight != Dp.Unspecified) {
- maxHeight.coerceAtLeast(0.dp).roundToPx()
+ maxHeight.roundToPx().coerceAtLeast(0)
} else {
Constraints.Infinity
}
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index e367382..a57dfef 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -15,9 +15,8 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -26,80 +25,15 @@
id("AndroidXPaparazziPlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- api("androidx.annotation:annotation:1.1.0")
- api("androidx.compose.animation:animation:1.2.1")
- api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
-
- implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(':emoji2:emoji2'))
- implementation(project(':core:core'))
- implementation("androidx.compose.ui:ui-graphics:1.2.1")
- implementation("androidx.compose.ui:ui-text:1.2.1")
- implementation("androidx.compose.ui:ui-util:1.2.1")
-
- testImplementation(project(":compose:test-utils"))
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinCoroutinesTest)
- testImplementation(libs.kotlinTest)
- testImplementation(libs.mockitoCore4)
- testImplementation(libs.kotlinReflect)
- testImplementation(libs.mockitoKotlin4)
-
- testImplementation(project(":constraintlayout:constraintlayout-compose"))
-
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":internal-testutils-fonts"))
- androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(project(":internal-testutils-runtime"))
- androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testMonitor)
- androidTestImplementation "androidx.activity:activity-compose:1.3.1"
- androidTestImplementation "androidx.lifecycle:lifecycle-runtime:2.6.1"
- androidTestImplementation "androidx.savedstate:savedstate:1.2.1"
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.mockitoKotlin)
-
- lintChecks(project(":compose:foundation:foundation-lint"))
- lintPublish(project(":compose:foundation:foundation-lint"))
-
- samples(project(":compose:foundation:foundation:foundation-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(':compose:animation:animation'))
api(project(':compose:runtime:runtime'))
@@ -108,36 +42,57 @@
implementation(project(":compose:ui:ui-util"))
implementation(project(':compose:foundation:foundation-layout'))
}
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
- implementation(project(':emoji2:emoji2'))
- implementation(project(":core:core"))
- }
+ }
+ androidMain.dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation(project(':emoji2:emoji2'))
+ implementation(project(":core:core"))
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
-
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.mockitoCore4)
- implementation(libs.truth)
- implementation(libs.kotlinReflect)
- implementation(libs.mockitoKotlin4)
- }
-
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(libs.kotlinTest)
implementation(libs.kotlinCoroutinesTest)
}
+ }
- androidAndroidTest.dependencies {
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation(project(':emoji2:emoji2'))
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+
+ implementation(project(":compose:ui:ui-util"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(project(":compose:ui:ui-test"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:test-utils"))
implementation(project(":internal-testutils-fonts"))
implementation(project(":test:screenshot:screenshot"))
@@ -157,20 +112,43 @@
implementation(libs.mockitoCore)
implementation(libs.mockitoKotlin)
}
+ }
- desktopTest.dependencies {
- implementation(project(":compose:ui:ui-test-junit4"))
- implementation(libs.truth)
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
implementation(libs.junit)
- implementation(libs.skikoCurrentOs)
- implementation(libs.mockitoCore)
- implementation(libs.mockitoKotlin)
+ implementation(libs.truth)
+ implementation(libs.kotlinReflect)
+ implementation(project(":constraintlayout:constraintlayout-compose"))
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.skikoCurrentOs)
+ implementation(libs.mockitoCore)
+ implementation(libs.mockitoKotlin)
+ }
}
}
}
- dependencies {
- samples(project(":compose:foundation:foundation:foundation-samples"))
- }
+}
+
+dependencies {
+ lintChecks(project(":compose:foundation:foundation-lint"))
+ lintPublish(project(":compose:foundation:foundation-lint"))
}
// Screenshot tests related setup
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index d0fc90e..603ac21 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -94,7 +94,10 @@
@Test
fun measureAndPlaceTwoItems() {
val itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
LazyLayout(itemProvider) {
@@ -118,8 +121,14 @@
@Test
fun measureAndPlaceMultipleLayoutsInOneItem() {
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("${index}x0"))
- Box(Modifier.fillMaxSize().testTag("${index}x1"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x0"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x1"))
}
rule.setContent {
@@ -143,7 +152,10 @@
@Test
fun updatingitemProvider() {
var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
})
rule.setContent {
@@ -166,7 +178,10 @@
rule.runOnIdle {
itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
}
@@ -178,7 +193,10 @@
fun stateBaseditemProvider() {
var itemCount by mutableStateOf(1)
val itemProvider = itemProvider({ itemCount }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
@@ -228,7 +246,11 @@
}
}
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index")
+ .then(modifier))
}
var needToCompose by mutableStateOf(false)
val prefetchState = LazyLayoutPrefetchState()
@@ -335,13 +357,15 @@
fun nodeIsReusedWithoutExtraRemeasure() {
var indexToCompose by mutableStateOf<Int?>(0)
var remeasuresCount = 0
- val modifier = Modifier.layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- remeasuresCount++
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
+ val modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ remeasuresCount++
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
}
- }.fillMaxSize()
+ .fillMaxSize()
val itemProvider = itemProvider({ 2 }) {
Box(modifier)
}
@@ -376,6 +400,52 @@
}
@Test
+ fun nodeIsReusedWhenRemovedFirst() {
+ var itemCount by mutableStateOf(1)
+ var remeasuresCount = 0
+ val modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ remeasuresCount++
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ .fillMaxSize()
+ val itemProvider = itemProvider({ itemCount }) {
+ Box(modifier)
+ }
+
+ rule.setContent {
+ LazyLayout(itemProvider) { constraints ->
+ val node = if (itemCount == 1) {
+ measure(0, constraints).first()
+ } else {
+ null
+ }
+ layout(10, 10) {
+ node?.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasuresCount).isEqualTo(1)
+ // node will be kept for reuse
+ itemCount = 0
+ }
+
+ rule.runOnIdle {
+ // node should be now reused
+ itemCount = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasuresCount).isEqualTo(1)
+ }
+ }
+
+ @Test
fun regularCompositionIsUsedInPrefetchTimeCalculation() {
val itemProvider = itemProvider({ 1 }) {
Box(Modifier.fillMaxSize())
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
index e5d86d8..077a75f 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
@@ -20,8 +20,10 @@
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
import androidx.compose.runtime.Composable
+// TODO (b/269341173) remove inline once these composables are non-trivial
+
@Composable
-internal actual fun ContextMenuArea(
+internal actual inline fun ContextMenuArea(
manager: TextFieldSelectionManager,
content: @Composable () -> Unit
) {
@@ -29,7 +31,7 @@
}
@Composable
-internal actual fun ContextMenuArea(
+internal actual inline fun ContextMenuArea(
manager: SelectionManager,
content: @Composable () -> Unit
) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 910e79f..748d6b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -89,7 +89,7 @@
)
private class BackgroundElement(
- private val color: Color? = null,
+ private val color: Color = Color.Unspecified,
private val brush: Brush? = null,
private val alpha: Float,
private val shape: Shape,
@@ -116,7 +116,7 @@
}
override fun hashCode(): Int {
- var result = color?.hashCode() ?: 0
+ var result = color.hashCode()
result = 31 * result + (brush?.hashCode() ?: 0)
result = 31 * result + alpha.hashCode()
result = 31 * result + shape.hashCode()
@@ -133,7 +133,7 @@
}
private class BackgroundNode(
- var color: Color?,
+ var color: Color,
var brush: Brush?,
var alpha: Float,
var shape: Shape,
@@ -155,7 +155,7 @@
}
private fun ContentDrawScope.drawRect() {
- color?.let { drawRect(color = it) }
+ if (color != Color.Unspecified) drawRect(color = color)
brush?.let { drawRect(brush = it, alpha = alpha) }
}
@@ -166,7 +166,7 @@
} else {
shape.createOutline(size, layoutDirection, this)
}
- color?.let { drawOutline(outline, color = it) }
+ if (color != Color.Unspecified) drawOutline(outline, color = color)
brush?.let { drawOutline(outline, brush = it, alpha = alpha) }
lastOutline = outline
lastSize = size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 5ef5aa7..86fda0b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -46,7 +46,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
@@ -645,7 +645,7 @@
private var onClickLabel: String?,
private var role: Role?,
private var onClick: () -> Unit
-) : DelegatingNode(), SemanticsModifierNode, PointerInputModifierNode, KeyInputModifierNode {
+) : DelegatingNode(), PointerInputModifierNode, KeyInputModifierNode {
abstract val clickablePointerInputNode: AbstractClickablePointerInputNode
abstract val clickableSemanticsNode: ClickableSemanticsNode
@@ -695,9 +695,6 @@
interactionData.currentKeyPressInteractions.clear()
}
- override val semanticsConfiguration: SemanticsConfiguration
- get() = clickableSemanticsNode.semanticsConfiguration
-
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
@@ -812,26 +809,26 @@
this.onLongClick = onLongClick
}
- override val semanticsConfiguration
- get() = SemanticsConfiguration().apply {
- isMergingSemanticsOfDescendants = true
- if (this@ClickableSemanticsNode.role != null) {
- role = this@ClickableSemanticsNode.role!!
- }
- onClick(
- action = { onClick(); true },
- label = onClickLabel
- )
- if (onLongClick != null) {
- onLongClick(
- action = { onLongClick?.invoke(); true },
- label = onLongClickLabel
- )
- }
- if (!enabled) {
- disabled()
- }
+ override val shouldMergeDescendantSemantics: Boolean
+ get() = true
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ if (this@ClickableSemanticsNode.role != null) {
+ role = this@ClickableSemanticsNode.role!!
}
+ onClick(
+ action = { onClick(); true },
+ label = onClickLabel
+ )
+ if (onLongClick != null) {
+ onLongClick(
+ action = { onLongClick?.invoke(); true },
+ label = onLongClickLabel
+ )
+ }
+ if (!enabled) {
+ disabled()
+ }
+ }
}
private sealed class AbstractClickablePointerInputNode(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 55ea39a..de22bba 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.requestFocus
import kotlinx.coroutines.launch
@@ -246,9 +247,9 @@
}
// TODO(levima) Remove this once delegation can propagate this events on its own
- override val semanticsConfiguration: SemanticsConfiguration
- get() = focusableSemanticsNode.semanticsConfiguration
-
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ with(focusableSemanticsNode) { applySemantics() }
+ }
// TODO(levima) Remove this once delegation can propagate this events on its own
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
focusedBoundsNode.onGloballyPositioned(coordinates)
@@ -362,11 +363,10 @@
this.isFocused = focused
}
- override val semanticsConfiguration: SemanticsConfiguration
- get() = semanticsConfigurationCache.apply {
- focused = isFocused
- requestFocus {
- this@FocusableSemanticsNode.requestFocus()
- }
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ focused = isFocused
+ requestFocus {
+ this@FocusableSemanticsNode.requestFocus()
}
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 4023a81..9b61189 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ReusableContentHost
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -95,13 +96,11 @@
val index = itemProvider.findIndexByKey(key, lastKnownIndex).also {
lastKnownIndex = it
}
-
- if (index < itemProvider.itemCount) {
- val key = itemProvider.getKey(index)
- if (key == this.key) {
- StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
- itemProvider.Item(index)
- }
+ val indexIsUpToDate =
+ index < itemProvider.itemCount && itemProvider.getKey(index) == key
+ ReusableContentHost(active = indexIsUpToDate) {
+ StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
+ itemProvider.Item(index)
}
}
DisposableEffect(key) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index cb376a7..23c078e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -181,7 +181,8 @@
*/
@Deprecated(
"Please use the overload without pageCount. pageCount should be provided " +
- "through PagerState.", ReplaceWith(
+ "through PagerState.",
+ ReplaceWith(
"""HorizontalPager(
modifier = modifier,
state = state,
@@ -202,8 +203,9 @@
"androidx.compose.foundation.layout.PaddingValues",
"androidx.compose.foundation.pager.PageSize",
"androidx.compose.foundation.pager.PagerDefaults"
- )
- )
+ ),
+ ),
+ level = DeprecationLevel.ERROR
)
@Composable
@ExperimentalFoundationApi
@@ -387,7 +389,8 @@
"androidx.compose.foundation.pager.PageSize",
"androidx.compose.foundation.pager.PagerDefaults"
)
- )
+ ),
+ level = DeprecationLevel.ERROR
)
@Composable
@ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 23e45b8..1e9132b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -103,7 +103,7 @@
){
// provide pageCount
}"""
- )
+ ), level = DeprecationLevel.ERROR
)
@ExperimentalFoundationApi
@Composable
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index f8831fe..3616033 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -30,9 +30,7 @@
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.GlobalPositionAwareModifierNode
import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.node.invalidateMeasurement
-import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.TextLayoutResult
@@ -59,8 +57,7 @@
onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
private val selectionController: SelectionController? = null,
overrideColor: ColorProducer? = null
-) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode,
- SemanticsModifierNode {
+) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
private val delegate = delegate(
TextAnnotatedStringNode(
@@ -96,9 +93,6 @@
constraints: Constraints
): MeasureResult = delegate.measureNonExtension(this, measurable, constraints)
- override val semanticsConfiguration: SemanticsConfiguration
- get() = delegate.semanticsConfiguration
-
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
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 93b37e5..5068c23 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
@@ -43,7 +43,7 @@
import androidx.compose.ui.node.invalidateLayer
import androidx.compose.ui.node.invalidateMeasurement
import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.text
import androidx.compose.ui.text.AnnotatedString
@@ -207,7 +207,6 @@
callbacksChanged: Boolean
) {
if (textChanged) {
- _semanticsConfiguration = null
invalidateSemantics()
}
@@ -230,11 +229,9 @@
}
}
- private var _semanticsConfiguration: SemanticsConfiguration? = null
-
private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
- private fun generateSemantics(text: AnnotatedString): SemanticsConfiguration {
+ override fun SemanticsPropertyReceiver.applySemantics() {
var localSemanticsTextLayoutResult = semanticsTextLayoutResult
if (localSemanticsTextLayoutResult == null) {
localSemanticsTextLayoutResult = { textLayoutResult ->
@@ -245,24 +242,10 @@
}
semanticsTextLayoutResult = localSemanticsTextLayoutResult
}
- return SemanticsConfiguration().also {
- it.isMergingSemanticsOfDescendants = false
- it.isClearingSemantics = false
- it.text = text
- it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
- }
+ text = this@TextAnnotatedStringNode.text
+ getTextLayoutResult(action = localSemanticsTextLayoutResult)
}
- override val semanticsConfiguration: SemanticsConfiguration
- get() {
- var localSemantics = _semanticsConfiguration
- if (localSemantics == null) {
- localSemantics = generateSemantics(text)
- _semanticsConfiguration = localSemantics
- }
- return localSemantics
- }
-
fun measureNonExtension(
measureScope: MeasureScope,
measurable: Measurable,
@@ -401,7 +384,7 @@
decoration = textDecoration
)
} else {
- val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
+ val overrideColorVal = overrideColor?.produce() ?: 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 09d2c75..e046446 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
@@ -43,7 +43,7 @@
import androidx.compose.ui.node.invalidateLayer
import androidx.compose.ui.node.invalidateMeasurement
import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.text
import androidx.compose.ui.text.AnnotatedString
@@ -168,7 +168,6 @@
layoutChanged: Boolean
) {
if (textChanged) {
- _semanticsConfiguration = null
invalidateSemantics()
}
@@ -190,11 +189,9 @@
}
}
- private var _semanticsConfiguration: SemanticsConfiguration? = null
-
private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
- private fun generateSemantics(text: String): SemanticsConfiguration {
+ override fun SemanticsPropertyReceiver.applySemantics() {
var localSemanticsTextLayoutResult = semanticsTextLayoutResult
if (localSemanticsTextLayoutResult == null) {
localSemanticsTextLayoutResult = { textLayoutResult ->
@@ -206,24 +203,10 @@
}
semanticsTextLayoutResult = localSemanticsTextLayoutResult
}
- return SemanticsConfiguration().also {
- it.isMergingSemanticsOfDescendants = false
- it.isClearingSemantics = false
- it.text = AnnotatedString(text)
- it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
- }
+ this.text = AnnotatedString(this@TextStringSimpleNode.text)
+ getTextLayoutResult(action = localSemanticsTextLayoutResult)
}
- override val semanticsConfiguration: SemanticsConfiguration
- get() {
- var localSemantics = _semanticsConfiguration
- if (localSemantics == null) {
- localSemantics = generateSemantics(text)
- _semanticsConfiguration = localSemantics
- }
- return localSemantics
- }
-
/**
* Text layout happens here
*/
@@ -317,7 +300,7 @@
textDecoration = textDecoration
)
} else {
- val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
+ val overrideColorVal = overrideColor?.produce() ?: Color.Unspecified
val color = if (overrideColorVal.isSpecified) {
overrideColorVal
} else if (style.color.isSpecified) {
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
index b285b1c..fff404c 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
@@ -36,6 +36,7 @@
* WindowDraggableArea is a component that allows you to drag the window using the mouse.
*
* @param modifier The modifier to be applied to the layout.
+ * @param content The content lambda.
*/
@Composable
fun WindowScope.WindowDraggableArea(
diff --git a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
index dd46067..5c61695 100644
--- a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
+++ b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
@@ -219,6 +219,69 @@
"""
)
+ val Composables: TestFile = bytecodeStub(
+ filename = "Composables.kt",
+ filepath = "androidx/compose/runtime",
+ checksum = 0x92d0959f,
+ source = """
+ package androidx.compose.runtime
+
+ @Composable
+ inline fun <T> key(
+ @Suppress("UNUSED_PARAMETER")
+ vararg keys: Any?,
+ block: @Composable () -> T
+ ) = block()
+
+ @Composable
+ inline fun ReusableContent(
+ key: Any?,
+ content: @Composable () -> Unit
+ ) {
+ content()
+ }
+
+ @Composable
+ inline fun ReusableContentHost(
+ active: Boolean,
+ crossinline content: @Composable () -> Unit
+ ) {
+ if (active) { content() }
+ }
+ """,
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuWSSMxLKcrPTKnQS87PLcgvTtUr
+ Ks0rycxNFeJ1BgskJuWkFnuXCHEFpeam5ialFnmXcPFxsZSkFpcIsYUASe8S
+ JQYtBgDjkUhNXwAAAA==
+ """,
+ """
+ androidx/compose/runtime/ComposablesKt.class:
+ H4sIAAAAAAAA/51VzXLbVBT+rvwnK04sy05xXEhT103tGCrHpfzUaaBNKfHg
+ BiY2gWlW17IaFNtSR5I9ZZdhwzN0yxPArrBgMmHHg/AUDMORLCdO7GkK49H5
+ P+d+55wr+c9/fvsdwPv4jGGVmx3bMjovVM3qP7ccXbUHpmv0dXXL13m7pztf
+ uDEwBvmQD7na4+aB+mX7UNfIGmIIdfXvGT4v7jcuumuNruX2DFM9HPbVZwNT
+ cw3LdNTHgVSplaZTGFobrXvT9s3/UX+j3GrVNmslogw3G2/QKcXdaFj2gXqo
+ u22bG1SNm6bl8lHlHcvdGfR6FBWmph0REsPyBAbDdHXb5D21bro2JRuaE0OC
+ YVH7Tte6QfZX3OZ9nQIZbhVn9HRmaXpFDmqlvQQWkJQwD5kh0u5ZWleEcv7o
+ Ge3HkGGIGubQ6uoMmeKMaSdwBW/NYRFZBrFgFJ4V/GWyOkN6xrwZVi5bKUNy
+ Vx/4w9yyaBymy3B3VpuXXY09hkf/PW9j7P/aNFxv91Sm8LqN0kKCxce0EV4R
+ eZqWP4ypTpRZM0lfCNu2HAotFJ++SY93Lg2b1VJ2FrzRuVFOaUNaOHvKkBrn
+ PtFd3uEuJ7hCfxiit595RPQIKLbrCQI5XxieVCGps87w1/HRmnR8JAmyMGJE
+ PJ4VRo9IjzwX8JTHcyWKyQkVVhVlIRfOskqompLDuXklrJC1Ejn5KSqI0e2T
+ H8U/XjEKLckxPzwqi8Tj1bQYfl34Q6oqbEv5sHh8JEvVK/JcLqGIIlP8oyqJ
+ /PjI+VGNbWmqhqfKCyc/CDEpIp68rFaY122VeYNQxgObvNGsxbAw8Tm83aU5
+ h7esDk052TBMfWfQb+t2y3N6JSyN9/a4bXh6YIw3jQOTuwOb5Ku7o+9O3Rwa
+ jkHuB2cXku7NRe/p9+Jc2HzT5Vr3CX8eHCA1rYGt6Y8NT1kKauxN1cc6BIS9
+ pSOEJUQQJW2DtCbZvc0vrilzr5AqK2miofvlX7HE8LN3OXCfaJRmtIA4Nkle
+ oYQFxJDDVfJSKiS87ZdehIJ3KPITPy+GT4NMkfgDeuYFUuL+zfPoEpZxLcCx
+ G+CQy8qNCQTf/HIKQSIuQkYSqVMYIv0KAQyZurrpw5CRn4CxMhvG9QkYq7gV
+ wGgHMDJjGLmXkCaghPBwVOxvpNkErCzSVOcMVgKlAFYGayj7sDLnYBWnYMWF
+ U0gCtnxawyPi35L1XeruvX2E6rhdh1pHBet1VHGnTn/nd/fBHHyAD/eRdLDs
+ 4CMHEZ/mHXzsQHSw6mDNt9xzIPmC4iD6Lx9Lm/oRCAAA
+ """
+ )
+
val Modifier: TestFile = bytecodeStub(
filename = "Modifier.kt",
filepath = "androidx/compose/ui",
@@ -382,7 +445,7 @@
val Remember: TestFile = bytecodeStub(
filename = "Remember.kt",
filepath = "androidx/compose/runtime",
- checksum = 0xc78323f1,
+ checksum = 0x736631c7,
source = """
package androidx.compose.runtime
@@ -392,72 +455,71 @@
inline fun <T> remember(calculation: () -> T): T = calculation()
@Composable
- inline fun <T, V1> remember(
- v1: V1,
- calculation: () -> T
+ inline fun <T> remember(
+ key1: Any?,
+ crossinline calculation: () -> T
): T = calculation()
@Composable
- inline fun <T, V1, V2> remember(
- v1: V1,
- v2: V2,
- calculation: () -> T
+ inline fun <T> remember(
+ key1: Any?,
+ key2: Any?,
+ crossinline calculation: () -> T
): T = calculation()
@Composable
- inline fun <T, V1, V2, V3> remember(
- v1: V1,
- v2: V2,
- v3: V3,
- calculation: () -> T
+ inline fun <T> remember(
+ key1: Any?,
+ key2: Any?,
+ key3: Any?,
+ crossinline calculation: () -> T
): T = calculation()
@Composable
inline fun <V> remember(
vararg inputs: Any?,
- calculation: () -> V
+ crossinline calculation: () -> V
): V = calculation()
""",
-"""
- META-INF/main.kotlin_module:
- H4sIAAAAAAAAAGNgYGBmYGBgBGJWKM3ApcYlkZiXUpSfmVKhl5yfW5BfnKpX
- VJpXkpmbKsQVlJqbmpuUWuRdwqXJJYyhrjRTSMgZwk7xzU/JTMsEK+XjYilJ
- LS4RYgsBkt4lSgxaDACMRj6sewAAAA==
- """,
"""
- androidx/compose/runtime/RememberKt.class:
- H4sIAAAAAAAAAK1WXVPbRhQ9K38J29jCfJSYJiFgyndkG5q2MZBSWhpPCekE
- j9opT8JWqMCWMlrZk0emL33pH+hrf0Ef0z50GPrWH9XpXVlgg4UJTTzW3qvr
- u+ecu3el9T///vkXgFV8xTCtWzXHNmuv1ardeGVzQ3Walms2DPWF0TAaB4bz
- jRsDY1CO9Jau1nXrUH1+cGRUKRpikB0/i2F1bufYduumpR61GurLplV1Tdvi
- 6rbv5UvzO1cxSgyba5XHvfGNm8DWFiuV0kZpnkaGmZ1rq9jy7vWDukF50zu2
- c6geGe6Bo5uEpluW7ept5F3b3W3W65SVqOr1arPuxWXEGe51STEt13Asva6W
- LdchDLPKY0gyjFZ/NKrHPsi3uqM3DFesyuxcb3FdkT0Bclia15JIIR3HIJTL
- fAGlx5BhiJpWyz42GEbmApY1iRGMJjCMMYbBnJl7mev0iZUZJm9qFcN2kPD/
- 0+AfAhusFQK7XtEKN7Fc6rzUKjBkgmi/77/w71IRf/uKtOK1ZVa04i1LLTIc
- vV1V76fOX96xTm2lb/EVbeWWC7DC8PXc/nuqrrKmBcq7PT6p1DyVWsl7MF81
- XS5jlmE4AIth6BzumeHqNd3VRW2NVoheyUwMEXpGj4UjUfy1Kbw8ebUCY+7p
- yVhckkNxaVwim4xLyhBdpyfx05PsDNms9JRNheXTE4UVk4qUlTPhDIXyoadn
- P8t/v2GnJ2e/RSUlnF25nEyGKZFiVIlSMNJvaiy7GTSVRkmRLwCiygBZuR9Q
- PPv8eiAaQ0qiBy6qJMkm+sEOZtfbsKk2bKo4pqSzyYwss0x4nOWH8sqUZ7tA
- UldBMmc/SbF4RD77tZhnYu3p0WMV8abxW9f9ppS0ghiKYqAdyjRGjUTi/Px8
- eOwyhLfsGr2s0zumZew2RbgiziSBaNNxo+mOKe794MCeeWjpbtMhf+JF+yQr
- Wy2Tm/TzZufQYshd/fXi6LmUFt+zm07V2DYF+h1/jtaDhwIkhCE+EdyhK0p3
- 63S3RXGJbHohk3iDodDawh/4gOF3sUmxQWOUKpYRwxPyxygm/HGCYGIS4siS
- /dzLjmHzIh/4gq4YrRkGyBGMEz7jM0oVOz+92GZcXwxkHPQYJyn1nNGTibu4
- 55XR5mY+94c93AMivcN+32f/zl+H9FKbfWMpkH3EY1+g1HP20BX2B+R11kDy
- dUz26EiEvAkdJVO+kgNKj5BVlttKHoWXA6QMUGlPvL90EfLbUoR+5UKKciFF
- QY48yfOEqJAvarpH1GB7K3TLmvFl7fntGV3IzJGsfk1KUS3nTUp1NWkUs5j3
- 4EcvNemjXh2Sr6A9SrQdxbiGL8nWKLpAyhb3ESpjqYxlGvGwDBX5Mm3o4j4Y
- xwpW9zHMEeH4mCPO8YgjyvEJx12OCY5POR5w3Of4jCPHMcXxmGOWo+R9Z/4D
- oSNh5zILAAA=
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgUuOSSMxLKcrPTKnQS87PLcgvTtUr
+ Ks0rycxNFeIKSs1NzU1KLfIu4eLjYilJLS4RYgsBkt4lSgxaDAC9VMzjUAAA
+ AA==
+ """,
+ """
+ androidx/compose/runtime/RememberKt.class:
+ H4sIAAAAAAAA/61WWXPbVBT+rrzJjhfFWUgcCCFL62yV44YCdeoSAqEe3MDU
+ HsNMnmRbTeVF6kiyKW8ZXvgNvPILeCw8MJnwxo9iOFeWHW9JXdIHXZ17dO53
+ vnO/u+iff//8C8A+vmRYU/SqaWjVV3LFaL40LFU2W7qtNVX5mdpUm2XV/MYO
+ gDFINaWtyA1FP5O/LdfUCnk9DKLpRjHsJ/N1w25oulxrN+XnLb1ia4Zuyceu
+ lcps5ocxMgyHB8WHo/7sm8AOtovFTDazSS3DRv7aKo6cvlJuqBS3ljfMM7mm
+ 2mVT0QhN0XXDVjrIJ4Z90mo0KGqqojQqrYbjFxFiWO6joum2aupKQ87ptkkY
+ WsUKIMwwV3mhVuouyHeKqTRVm8/K3eRocX2eAgc5y2yWwogiFkIE0mC+MaUH
+ EGfwa3rbqKsMs8kx0xrGLOamMIN5hsi6tv58/UonlmNYeZNUDMfjiP8fgQvX
+ Cfy28AOSr98kJUngKu6tqz/tMcTHEfvhZmluU7M6ec23mgVeXpqhNhnwu6nt
+ x9vUdutq7zN8nTx9R6UUD0pjS3l7fOJZcniWMs7efNmyLRFJhpkxWAzTXbin
+ qq1UFVshn9Bse+hUZrwReQPaq3VuCPTxlcatFFnVPcZeXJwnQoLoCQkLAr3p
+ kS7OyQiQMR2ijxvUTwhP2KpXvDiXWDosCQkx7o2TK+V5cvmL+PdrdnF++Ztf
+ kLyJvaFgUfIlvAss5U9HRe/1AwOJzLUDRUmcBCKY+GoCCFEKTQI2lXjcAQt3
+ wMLpeSmSCMdFkcWd4ano6ihMeBgmdvmzEAj5xMtf0ynG55s2GCvyM8TVrP+U
+ 5BqV6MroXpb36jat0yOjSidzLK/p6kmLu4v8OOIQBt0tJcXUeN91Bgvama7Y
+ LZPspWedayuntzVLo8+HV8canXnDX3v3zEBYqGC0zIp6rHH0RXdMaQQPexDg
+ 5esMPizS46feAfWOyC/QO7YVn3qNac/B1h94j+F3vhLxiFo/VS0igCzZ8+Tj
+ 9gJBMD4IISTo/diJDuDzXjxwSE+AJgxBMnjGJTfjUwrlyzu23cn4aHtsxoiT
+ cYVCuxkdmljGh04ZndzMzf3+SO6IQJ0PnI3VZbDiMvjenYvYTodBdmcsg1mH
+ wRaFdhl4hhisknU1D4LL5aMRLjOeHpdBRmsuozIN89Fb2u0weuDdHUMpSGVm
+ nX85H9kdSrwOqUdJ6lGSsEGW4FicnMcltz5CbtE7RG6Q4h2XYsGVbW4rvkkU
+ bxIvSiy64kX7xJtDkqYTjtUv3t3x4gV7PAR84bQZWq7AKXm3idnOKTw57OZw
+ LwcZqRyt8XQO97FPARY+xoNTSBZ8Fj6xELLwqQW/hWULn1lYsrBqYcXChoU1
+ Cw8tJB3/nf8A7cabHS4LAAA=
"""
)
val SnapshotState: TestFile = bytecodeStub(
filename = "SnapshotState.kt",
filepath = "androidx/compose/runtime",
- checksum = 0x9907976f,
+ checksum = 0x3a5656cc,
source = """
package androidx.compose.runtime
@@ -500,6 +562,29 @@
}
}
+ @Composable
+ fun <T> produceState(
+ initialValue: T,
+ key1: Any?,
+ key2: Any?,
+ producer: suspend ProduceStateScope<T>.() -> Unit
+ ): State<T> {
+ return object : State<T> {
+ override val value = initialValue
+ }
+ }
+
+ @Composable
+ fun <T> produceState(
+ initialValue: T,
+ vararg keys: Any?,
+ producer: suspend ProduceStateScope<T>.() -> Unit
+ ): State<T> {
+ return object : State<T> {
+ override val value = initialValue
+ }
+ }
+
interface ProduceStateScope<T> : MutableState<T> {
suspend fun awaitDispose(onDispose: () -> Unit): Nothing
}
@@ -516,181 +601,487 @@
""",
"""
META-INF/main.kotlin_module:
- H4sIAAAAAAAAAGNgYGBmYGBgBGJ2KM3Apc8ln5iXUpSfmVKhl5yfW5BfnKqX
- mJeZm1iSmZ8HFClKFeJxBPMTk3JSvUu4zLkkMDQUleaVZOamCnEFpeam5ial
- FnmXCPEH5yUWFGfklwSXJJaANCpg0ViaqVeal1kixOJS4F2ixKDFAAA4Rqdc
- pAAAAA==
+ H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMueSSMxLKcrPTKnQS87PLcgvTtUr
+ Ks0rycxNFeIKSs1NzU1KLfIuEeIPzkssKM7ILwkuSSxJ9S7h4uNiKUktLhFi
+ CwGS3iVKDFoMAJF5eAthAAAA
""",
"""
androidx/compose/runtime/DerivedState.class:
- H4sIAAAAAAAAAIVRTW/TQBB9Yzuxk4bghhbSAKVCQk044FJxQKSqhPgQkVIh
- NVGElNM2XtJtHLvybqIe81s48CM4IKtHfhRinKIKERUuM/Nm37zdffPj57fv
- AF5gh/BExGGaqPAiGCXT80TLIJ3FRk1l8Famai7DnhFGuiDC0UH/VfdMzEUQ
- iXgcfDw5kyPTPlxtdW/UXIod9PvtwzbB/3vQhUPY/vewiyLBG0szENFMEjaa
- rdUHEArNFt/CTH3N3GyuElsDQrHJzLxY704SE6k4OJJGhMIInremc5utojy4
- BJpw60LlaI+r8DnhXbaolq26Vc4Wy2R5Be9zPVs8dbxs4dO+V3Nq1gfas47r
- vt2wXmaLT5dfq5dfipWG4zl+4bHjFX03F9sn7N5s3Z/r4IdRn7DzH6NzH+ZX
- n/d7sTjXp4lZHjybGEKpp8axMLOUj8u9ZJaO5HsVMdg6vhIZKK1OIvk6jhMe
- Ukms2XoLBRBcNsDiZXkoMXqYI5QZr6FyjW/B/l3Z2F7mB3jE+Q0zqqxyewi7
- A7+DdY6o5eFOBxvYHII07uLekLeHusaWRkPjvs5hSWNNo/ILV3pR08ICAAA=
+ H4sIAAAAAAAA/4VRTW/TQBB9Yzuxk4bghhbSAG1BQiQccKk4IFJVQnyISKmQ
+ mihCymkbL2Ebx668m6jH/BYO/AgOyOqRH4UYp6hCRIXLzLzZN29Hb378/PYd
+ wHM8IDwScZgmKjwPRsn0LNEySGexUVMZvJGpmsuwZ4SRLohwdNB/2T0VcxFE
+ Ih4HH05O5ci0D1db3Ws1l2IH/X77sE3w/x504RC2/z3sokjwxtIMRDSThI1m
+ a3UBQqHZ4l+Yqa+Ym81VYmtAKDaZmRfr3UliIhUHR9KIUBjB89Z0brNVlAcv
+ DyDQhPvnKkd7XIXPCG+zRbVs1a1ytlgmyyt4n+rZ4onjZQuf9r2aU7Pe0551
+ XPfthvUiW3y8+Fq9+FKsNBzP8QsPHa/ou7nYPuHx9f79eRPejvqE3f+4nZsx
+ v3TA78XiTH9OzPLh6cQQSj01joWZpfxc7iWzdCTfqYjB1vGlyEBpdRLJV3Gc
+ 8JBKYs3+WyiwDy4bYPHFPJQYbecIZcZrqFzhG7B/VzZ2lvk+djm/ZkaVVW4O
+ YXfgd7DeQQ23uMRGB5u4PQRp3EF9yCfElkZD467GPZ3DksaaRuUXZ2oGC8cC
+ AAA=
""",
"""
androidx/compose/runtime/DerivedStateImpl.class:
- H4sIAAAAAAAAAI1R227TQBA9u3Yc16Spm6ZXyq1QmqQUl4oH1EZBXFQ1UgCp
- iSJEn7aJ1W6T2JXXifqYr+AD+AKQQEg8oKiPfBRi7EQVECTy4Lkcn5kzs/Pj
- 57fvAB5jiyEvvGbgy+aF0/A7575ynaDrhbLjOi/dQPbcZjUUoVvunLeTYAy1
- Ym23ciZ6wmkL78R5c3zmNsK90jhUmahvsVbbK+0x2H/XJ6EzrE/UIwmDwShK
- T4Ylhvnc+Cz5OhFyJBUFWi5fT8HENQsJpBgSPdHuugyZ8boU0piZAofNoIen
- UjFsTrZX9F60lnnihvVh+2wuPy5A6rk8zUVMdcVMFineekq7aPGv2UrLD9vS
- c165oWiKUBDGOz2NLsgik2RgLYIuZJRtU9R8xPBu0E9bfIlbg37suGmYfGnQ
- L+jmoG+zHTOjZ/gB2+bPpzOGra3wJ4P+5QeD2/rh6ih9e/k+TZBtcdtc0c2E
- bazpZtLWI4UdEq0xbEz2HNGFq544V6d+GAMPWyHDVFWeeCLsBrS0/sJvkpup
- SM993e0cu0FNHLfjs/gN0a6LQEb5CLSqfjdouPsySpYPh4p1qST9feZ5PklI
- 31PYptslQE9KH4+OSX6DHoljGRrFJqLr5ggpkefkrcJXTBc2v2D2U8zLkzWI
- CaoukF0YspDBHBBHv3e1KMpiftTToSyqTBQ+Y/bjP9ulhoRRu2GTBcIWrwbb
- HQ1m/Hco42ooA0t/DKWNIg2bsb+PB+T3ibFC2tePoJWxWsYNsrgZmVtl3Mad
- IzCFNdw9wpTCnMI9hXWFtIrSrMK8wqLCzC+1g+gKTQQAAA==
+ H4sIAAAAAAAA/41S204TURRd58x0OoylDOWOeEORtgiDxAcDTY2XEJtUTWjT
+ GHk6tBM40M6QOacNj/0KP8Av0ERj4oNpePSjjHvahqg1sS97r71mn732ZX78
+ /PYdwCNsMeRE0IhC2bjw6mHrPFS+F7UDLVu+98KPZMdvVLTQfql13kyCMVQL
+ 1d3yqegIrymCY+/N0alf13vFUao8Vt1CtbpX3GNw/36fhMmwNlaNJCwGqyAD
+ qYsMc9nRXnI1SsiSVAyMbK6Wgo1rDhJIMSQ6otn2GTKj71JIY2oCHC6DqU+k
+ YtgYb654XzSWfezr2qD8bDY3KkDq2Rz1RZnqKjNZILz5hGYx+p+my2ehbsrA
+ e+Vr0RBaEMdbHYMuyGJjxwYM7Iz4CxlH24QaDxne9bpphy9yp9ftO25bNl/s
+ dfOm3eu6bMfOmBn+km3zZ5MZyzWW+eNe9/KDxV3zYGUYvr18nybKdbhrL5t2
+ wrVWTTvpmrHCDolWGdbH20l85kogztVJqPvE1plmmKjI40DodkSTm8/DBrmp
+ sgz81+3WkR9VxVGzf5uwLpo1Eck4HpJOJWxHdX9fxsHSwUCxJpWkr0+DICQJ
+ GQYK23TABO2H/l9CdFHyWVoSxxIMwjbiE+eIKZLn5J38V0zmN75g+lM/L0/W
+ okzQ6w2y84MsZDATr57Q71UdQrOYG9b04suQT+Q/Y/rjP8ulBgnDcoMi88Qt
+ XDW2O2zM+m9T1lVTFhb/aMoYIgMP+n4dm+T3KWOZtK8fwihhpYQbJdzELYK4
+ XcIdrB6CKdzFvUNMKMworCncV0irOJxVmFNYUJj6BfKpA8lSBAAA
""",
"""
androidx/compose/runtime/MutableState.class:
- H4sIAAAAAAAAAIVRwW7TQBB9s3ZiJw3BDS2kAUqFhEg44FJxQKSqhBCISImQ
- miiqlNM2McGNs66y66hHfwsHPoIDsnrkoxDjFFWIqHCZmbf75s3umx8/v30H
- 8BJ7hCdSTRZxOLnwx/H8PNaBv0iUCeeB30uMPI2CvpEmcECE3uHgdfdMLqUf
- STX1P56eBWPTPlo/6t6ouRI7HAzaR22C93ejA5uw++9mB0WCOw3MUEZJQNhq
- ttYfQCg0WzyFmfqaud1cJ7aGhGKTmXmx2Z3FJgqV3wuMnEgjuV/MlxZbRXlw
- CDTjo4swR/tcTV4Q3mVptSzqopylqyTcgvupnqXPbDdLPTpwa3ZNfKB9cVz3
- rIZ4laUnl1+rl1+KlYbt2l7hse0WPScXOyA8vdm6P9fBD6MBYe8/Ruc+LK8+
- 7/WVPNefY7O6eD4zhFI/nCppkgVfl/txshgH78OIwc7xlcgw1CFPfKNUzE1h
- rDRbL1AAwWEDBC/LRYnRwxyhzHgDlWt8C9bvysLuKj/AI85vmVFlldsjWB14
- HWxyRC0PdzrYwvYIpHEX90a8PdQ1djQaGvd1DksaGxqVX2k2HnjCAgAA
+ H4sIAAAAAAAA/4VR0WoTURA9c3eT3aQxbmOradRaBTHxwa3FBzGlIKIYSBCa
+ EIQ83SZrvM1mt+TeDX3cb/HBj/BBlj76UeJsKkUM1Zc7c+aeOTOc+fHz23cA
+ L/CQ8FhGk0WsJuf+OJ6fxTrwF0lk1Dzwe4mRJ2HQN9IEDojQOxy86p7KpfRD
+ GU39Dyenwdi0j9ZL3Ws1V2KHg0H7qE3w/m50YBN2/93soEhwp4EZyjAJCFvN
+ 1voChEKzxVOYqa+Y2811YmtIKDaZmSeb3VlsQhX5vcDIiTSS+8V8abFVlD9u
+ /oBAM66fqxztczZ5TnibpdWyqItylq6CcAvup3qWPrXdLPXowK3ZNfGe9sVx
+ 3bMa4mWWfrz4Wr34Uqw0bNf2Co9st+g5udgB4cn1/v15E96OBoS9/7idm7G8
+ dMDrR/JMf47N6uPZzBBKfTWNpEkW/F3ux8liHLxTIYOd40uRodKKJ76Oopib
+ VBxp9l+gwD44bIDgi7koMdrNEcqMN1C5wjdg/c4sPFjF+9jj+IYZVVa5OYLV
+ gdfBZgc13OIUWx1s4/YIpHEH9RGfEDsaDY27Gvd0DksaGxqVX4ySaL/HAgAA
""",
"""
androidx/compose/runtime/MutableStateImpl.class:
- H4sIAAAAAAAAAI1RXU8TQRQ9M7vdLrWUpXwjfqFIW8RF4oOBpkZNjE2KJrRp
- jDwN7QYG2l3SmRIe+yv8Af4CTTQmPpiGR3+U8c62IWpN7MPej7Pn3nPv3B8/
- v30H8BibDHkRNjuRbF74jah9FqnA73RDLduBv9fV4rAVVLXQQbl91kqCMdSK
- tZ3KiTgXfkuER/6bw5OgoXdLo1BlrL7FWm23tMvg/V2fhM2wNlaPJBwGpyhD
- qUsMc7nRWfJ1IuRIygRWLl9Pw8W1FBJIMyTORasbMGRH69LIYGoCHB6DrY+l
- YtgYby/zXrSWexTo+qD9bC4/KkDquTzNRUx1xUwWKd58SrtY8a/pymmkWzL0
- 9wItmkILwnj73KILMmOSDOyUoAtpsi2Kmo8Y3vV7mRRf5Kl+L3bcdVy+2O8V
- bLff89i2m7Wz/BXb4s8ns45nLfMn/d7lB4d79v7KMH17+T5DkJfinrtsuwnP
- WbXdpGcbhW0SrTGsj/cc5sLVUJyp40jHwMNTzTBRlUeh0N0OLW2/iJrkpioy
- DF5324dBp2aqzVmihmjVRUeafAimqlG30wheSpMs7Q8U61JJ+vssDCOSkFGo
- sEW3S4CelD5ujkl+nR6JYwkWxS7MdXOElMhz8qnCV0wWNr5g+lPMy5N1iAnM
- oxDbmIUsZoA4+r1riqJZzA17+pSZykThM6Y//rNdekAYths0mSds4WqwneFg
- zn+Hcq6GcrD4x1DWMLKwEfv7eED+JTGWSfv6AawyVsq4QRY3jblVxm3cOQBT
- WMXdA0wozCjcU1hTyCiTzirMKSwoTP0Cc/1T9U0EAAA=
+ H4sIAAAAAAAA/41S204TURRdZ2Y6HcbSDuWOeEORtgiDxAcDTY2aEJsUTWjT
+ GHk6tBM40M6QnlPCY7/CD/ALNNGY+GAaHv0o4z7Thqg1sS97r71mn732ZX78
+ /PYdwBNsMuR52OxEonnpN6L2eSQDv9MNlWgH/n5X8aNWUFVcBeX2eSsJxlAr
+ 1nYqp/yC+y0eHvtvjk6DhtotjVKVseoWa7Xd0i6D9/f7JCyG1bFqJGEz2EUR
+ ClVimM2N9pKvU0KOpDQwc/l6Cg5uuEggxZC44K1uwJAdfZdCGpkJGPAYLHUi
+ JMP6eHPpfdFYznGg6oPyM7n8qACp5/LUF2XK68xkkfDGM5rFjD9NVc4i1RKh
+ vx8o3uSKE2e0L0y6INPG0QYM7Iz4S6GjLULNxwzv+r20aywYbr8XO8OxHWOh
+ 3ytYTr/nsW0na2WNV2zLeDGZtT1zyXja7119sA3POlgehm+v3qeJ8lzDc5Ys
+ J+HZK5aT9CytsE2iNYa18Xaiz1wN+bk8iVRMbJ4phomqOA656nZocutl1CSX
+ qYgweN1tHwWdmn6tbxM1eKvOO0LHQ9KtRt1OI9gTOlg8GCjWhRT09XkYRiQh
+ olBiiw6YoP3Q/0uILko+R0sysAiTsAN94jwxJfIGebfwFZOF9S+Y+hTnFcja
+ lAnMYT22cRaymNarJ/R7VZfQDGaHNX19GfKJwmdMffxnudQgYVhuUGSOuPnr
+ xnaGjdn/bcq+bsrGwh9NmUNk4lHs17BBfo8ylkj75iHMMpbLuFXGbdwhiLtl
+ 3MPKIZjEfTw4xITEtMSqxEOJtNThjMSsxLxE5hfups0nUgQAAA==
""",
"""
androidx/compose/runtime/ProduceStateScope.class:
- H4sIAAAAAAAAAI1T328SQRCeXSh3INUr/gJarVqNSox3Ep+EEI2GFEO1EfSF
- p+U4cOHYJbd72Efin+KDf4PxwRB8848yzkFpY7G2DzezM/PNN/vju1+/v/8A
- gKewQ6DARCeQvHNgu3I4ksqzg1BoPvTs/UB2QtdraKa9hitHngGEQLPcfFbv
- szGzfSZ69tt233N1qbKaqp9KvBdq1vYXxOVms1QpEbBO9hsQJ3DvXBwGJAik
- 2SfG9SuuIhhu80F9ILXPhd0fD+1uKFzNpVB29XDllJZ1VwYy1Fx4yn4pkVyE
- LAKUHq4eiUD3LNrysv5ecLyWs6aUC5V/D7pbl0HP7nu6HTCOA5gQUrPFsDeh
- 70eHR9jO/2BSR0hEbSx3sedp1mGaYY4OxzHUAImMQYAMMHXAo8jBVecJnnU6
- 2UrRLE1NJ0fOIsuIWlHG7Gank0LcnE4sUjQz8QzdJQ59vW3F8tSJF9PWWn6e
- dQwnsTv7+vznNzKdzL4kqGXOPtN4ipq5aFqRwKPT9bIiRNw+aRK4fz6JIRoI
- JKU4EkdmeR/HakAFNgQbqY9Sz5seDzT2NHhPMB0G2LP5bkFdE2OuOHK/OL5q
- fK6T1X0WsKGnveAvWKohw8D1qtxHxtxhz4cVPpQzhTXctBG9EP4HJiQhBjcx
- opCCbfQJrF5Afwu/dYpBOoLO7RIYg9tzfwPuoK9idR1JL7YgVoNLNbDQwkZk
- MjW4DFdaQBRchWstSCq4riCrIKfAVJBXsKlga75I/gEnsHRUOwQAAA==
+ H4sIAAAAAAAA/41T328SQRCeXSgcSPWKv4BWq7ZGJcY7iU9CiEbTFENrI+gL
+ T8tx4MKxS273sI/EP8UH/wbjgyH45h9lnINeG4u1fbid2ZlvvtnZ/e7X7+8/
+ AOAZbBMoMtHxJe8cWo4cjqRyLT8Qmg9d68CXncBxG5ppt+HIkZsEQqBZaT6v
+ 99mYWR4TPettu+86ulxdDtXPJN4LNGt7C+JKs1mulgmYp+uTECdw/0IcSUgQ
+ yLBPjOvXXIUwPObD+kBqjwurPx5a3UA4mkuhrJ0jzy5HeUf6MtBcuMp6JZFc
+ BCwElB8tj0Sgex5tJcq/Fxyv5bwulWL1342269LvWX1Xt33GsQETQmq2aLYf
+ eF44PMK2/geTOkQiai06xZ6rWYdphjE6HMdQAyRcjHABAmSA8UMe7mz0Ok9x
+ 4OlkI01zND2dHBuTRDtqhhGjm5tOinFjOjFJycjGs3SX2PTNphkrUDteypgr
+ hXnUTtqJ3dnXFz+/kelk9iVBTWP2mcbT1MiH3UoEHp8tmiU14gykSeDBxXSG
+ aBwwJcWxQrLRpZxIAmXYEGykPko9L3oy0FjT4D3BdOBjzfq7BXVNjLniyP3y
+ 5L7xzU5nD5jPhq52/b9g6YYMfMfd4R4y5o9qPizxoaYprOChk+Hb4M9gQApi
+ sIk7Cmm4gzaB2Uto7+K3SnGTmT9juEbAGNyb29uwhXYHs6tIerkFsRpcqYFZ
+ gzXIogtXa3ANrreAKLgBN1uQUpBTkFdQUGAoWFewoeDW3En9Ae+dQkpABAAA
""",
"""
androidx/compose/runtime/SnapshotStateKt$produceState$1.class:
- H4sIAAAAAAAAAI1T3U4TURD+znb7w1poqYCAf6gVtkVZICZqCiSGSNJYNaGk
- MeFq2V3Kge1Zsnu24bJP4QP4BJpoTLwwDZc+lHHOtjEoCF7s/GXmm2/OzP74
- +e07gCdYZXhqCzcMuHtiOUHnOIg8K4yF5B3Pagr7ODoIZFPa0nsly8dh4MaO
- l7jllSwYFTcO7a5t+bZoW2/3Dj1H1hr/xlOFazs7tY0aQ/Hvwix0hjuXF2eR
- YcisccHlBsOkeb57pUUJJvVQRsqstPLI4ZqBNPIM6a7txx5D6XxdHmMojEBD
- kUGXBzxieH7JJJe+DE03WlYcue23Bh1zbU8OzQmzcr49cTMrxJo4J3K8cRRI
- nwvrtSdt15Y2xbRON0VLY0pkGdgRhU648pbJclcY1vu9UaPfM7RpzdByepX1
- ezljut9bzZX0kvas31tm21NFbVaZ707f66cfMoahFdOzei5V1BUI3cPcFQsk
- Jub/PkwWZYb82ddh2L9gaxdEhvMfdjvWfiwcyQMRWVtDa7VWuYplHvNYoDP7
- g9HSkWQYafK2sGUcEhl9M3BJFRpceG/izp4X7th7fnIigaO2F3LlD4P5uhBe
- uOnbUeTRgRReCscPIi7atKWDwGUwmkEcOt4WV9kz2wNCLR5xKn8hREAc1BxY
- oUNLg4H+IZTU5ZGu0iI1TNMHOll1iotkbZFWEaP6FaPVxS8Y/5TkPSI5BrX8
- eehYoPx5PCZvapBNqNeBxJo4g26QNZnkKGyLPEY6Xf2M8Y+/YTNJcCGByw8S
- hnADkBvkLyXQLGkGzBAUiMZDmMOcFJYTXaFRgXXKnKGq2V2k6rhZxy2SuK3E
- nTruYm4XLMI93N9FJlLmgwhjESYjTEUo/AJAwj8ArQQAAA==
+ H4sIAAAAAAAA/41T3U4TURD+znb7w1poqYCAiqhVtkVZqCZqCiSGSNJYNaGk
+ MeFq6S7lQHuW7DltuOxT+AA+gSYaEy9Mw6UPZZyzbQwKAhc7f5n55pszsz9/
+ ff8B4CmeMDxzhRcG3Dt2GkH7KJC+E3aE4m3fqQn3SO4HqqZc5b9W+aMw8DoN
+ P3LzK0kwKq4euF3Xabmi6bzbPfAbqlz9P54uXN3eLq+XGbL/FiZhMsxdXJxE
+ giGxygVX6wyT9tnuhTol2NRDGzG7UE8jhWsW4kgzxLtuq+Mz5M7WpTGGzAgM
+ ZBlMtc8lw4sLJrnwZWi60bzmyN1WfdAx1fTV0JywC2fbEze7QKyJcyTHq4eB
+ anHhvPGV67nKpZjR7sZoaUyLlBZgYIcUP+baWybLW2FY6/dGrX7PMqYNy0iZ
+ Rdbvpazpfq+Uypk543m/t8y2prLGrDbfn3wwTz4mLMvIxmfNVCxrapASw/wl
+ WyQ69lVfJ4kHDOnTT8Swd87qzokMH+Gg23b2OqKheCCkszm0SuXCZSzTWIBN
+ t/YXo6VDxTBS403hqk5IZMyNwCOVqXLhv+20d/1w291tRXcSNPQKQ679YTBd
+ EcIPN1qulD5dSeaVaLQCyUWTVrUfeAxWLeiEDX+T6+yZrQGhOpecyl8KERAH
+ PQdW6NritEP6kZDT50d6kRZpYJo+2jH0PT4ia5O0jljFbxgtLn7F+Oco7zHJ
+ MejlP4RJo46QXiJvapBNqNf1mZA1cQrdImsyytHYjr4i0vHiF4x/+gObiIIL
+ EVx6kDCEG4DcIN+JoFnUDJjBMkmTKBSGOTEaUesiSqTXKHOGqmZ3EKvgZgW3
+ KriNOTJxp4J53N0Bk7iH+ztISG3mJcYkJiWmJDK/AeLwkAKyBAAA
+ """,
+ """
+ androidx/compose/runtime/SnapshotStateKt$produceState$2.class:
+ H4sIAAAAAAAA/41T0U4TURA9d7ttl7XQUgEBFVGrbouyUEzUFEgMkaSxakKb
+ xoSnZbuWC+1dsnu34bFf4Qf4BZpoTHwwDY9+lHHutjEoCjzs3DOTmTPn3pn9
+ 8fPbdwCPscbwxBGtwOetY9v1u0d+6NlBJCTvenZdOEfhvi/r0pHeS1k4CvxW
+ 5HqxWyinwai4duD0HLvjiLb9Zu/Ac2Wl9n8+VbjeaFQ2Kwy5vwvT0BkWzi9O
+ I8WQWueCy02Gaets92KTEizqoUDCKjYzMHDFRBIZhmTP6UQeQ/5sXQYTyI5B
+ Q45Bl/s8ZHh2zk3OfRm63XhBaeROpznsaLQ9OYJTVvFse9JmFUk1aY7tZO3Q
+ lx0u7FeedFqOdCimdXsJGhpTxlAGDOyQ4sdceSuEWqsMG4P+uDnom9qsZmqG
+ XmKDvmHODvplI6/ntaeD/grbmclp8wq+PXmvn3xImaaWS87rRiKnK5Iyw+IF
+ UyQ51mVfJ417DJnTT8Rw/I/RXSoyepaDXtd+FwlXcl+E9vYIlSvFi3Rn8AAW
+ bd8fGpcPJcNYnbeFI6OA5OlbfouObI0L73XU3fOChrPXiTfHd9VQA678UTBT
+ FcILtjpOGHq0N9kXwu34IRdtGt6+32Iw634UuN42V9lzO0NBTR5yKn8uhE8a
+ 1D2wSvuXpKnSr4W8Wkg6l2i0Gmbpo6lDbehDQtt0qohZ+orx0tIXTH6K8x6R
+ nYBahzJ0rFF+GcvkzQyzifWqWhxCU6fYTULTcY7ittVe0Zksfcbkx9+0qTi4
+ FtNlhgkjuiHJNfLtmJrFzYA5rJDVcR/FUU6CrqjOEskCNihzjqrmd5Go4noV
+ N6q4iQWCuFXFIm7vgoW4g7u7SIUKFkJMhJgOMRMi+wttlNMGxAQAAA==
+ """,
+ """
+ androidx/compose/runtime/SnapshotStateKt$produceState$3.class:
+ H4sIAAAAAAAA/41T3U4TURD+znbbLmuhpQJCVUStui3KQjFRU2hiiCSNVRNK
+ GpNeLdu1HGjPkt3Thss+hQ/gE2iiMfHCNFz6UMY528agIHix85eZb74zM/vj
+ 57fvAB5jneGJI1qBz1vHtut3j/zQs4OekLzr2XXhHIX7vqxLR3ovZf4o8Fs9
+ 14vc/HoSjIprB07fsTuOaNtv9g48V5Zr/8ZThRu7u+VKmSHzd2ESOsPixcVJ
+ JBgSG1xwWWGYtc52LzQowaIeyohZhUYKBq6YiCPFEO87nZ7HkD1bl8IU0hPQ
+ kGHQ5T4PGZ5d8JILJ0Ovm8wrjtzpNEYdjbYnx+aMVTjbnrhZBWJNnCM5XTv0
+ ZYcL+5UnnZYjHYpp3X6MlsaUMJQAAzuk+DFX3ipZrTWGzeFg0hwOTG1eMzVD
+ L7LhwDDnh4OSkdWz2tPhYJXtzGW0nDLfnrzXTz4kTFPLxHO6EcvoCqTEsHTJ
+ FomO9b/TSeIeQ+r0iBja56yuec4tjadw0O/a73rCldwXob09tkrlwmU0U3gA
+ i47tD0orh5Jhos7bwpG9gNjoW36LVLrGhfe6193zgl1nrxMdiu+qHQZc+eNg
+ qiqEF2x1nDD06EzSL4Tb8UMu2rSrfb/FYNb9XuB621xlL+yMCDV4yKn8uRA+
+ cVDvwBqdW5yWSH8Ssur+SC/TJjXM00dLhjrIh2Rtk1YRs/gVk8XlL5j+FOU9
+ IjkFtf1N6KhQ/iZWyJsbZRPqVXUnZM2cQjfJmo1yFLatzoh0vPgZ0x9/wyai
+ YCWCS40SxnAjkGvk2xE0i5oBC1glqeM+CuOcGD1R6SJKEUWaBlXlmohVcb2K
+ G1XcxCKZuFXFEm43wULcwd0mEqEy8yGmQsyGmAuR/gWU18KgswQAAA==
""",
"""
androidx/compose/runtime/SnapshotStateKt.class:
- H4sIAAAAAAAAAKVYW1PbVhD+jm1sIxwQBgKIBEjiBDAXG5ombXBJU3LB5drg
- 0KY0TYQtQGBLro5MkzemD33sj+gvaJ+SNDMdJn3rT+mP6HSPLBvb2AZSz0jn
- tvudb/fs2dX473//+BPATRwwjKpGxjL1zMtY2szlTa7FrIJh6zkttm6oeb5r
- 2uu2amuLdgCMQd5TD9RYVjV2Yqtbe1qaZr0M7RnN0g+0jCO5us0wO7pUKzg7
- ttRwp/sV6rMMjxKpOyf150ZTqbOCJEh0jpCuLZnWTmxPs7csVTd4TDUMk9Z1
- k/orpr1SyGZJauxMmMlcPhtAK4M/oRu6PcfQU8/KjRDaEJIg4QLD9TMhB9DB
- 0HKgZgsaQ/gkJjk4V7DVraz2oQ5erlD/YAdXgpQc3Nh1ldJF112U0NvcKZU6
- AfSTMyrtXtK5LWyfGm1CsipohQaRTDYw9zwwJYOjZ1cJYJDBO1qMiGEJQ7jC
- 0Flp0bKaFwZNnpkJKRCH54nFOvZs/C8bCTmRWpxNbZxyrLVKAdyQMCIsC+Ut
- M1NIFy1j2K4ToXVm9k07qxuxvYNcbLtgpItX86Hbm2kWjqVg/qdxMJ97v8Rk
- 4/3WKsxbT5t59w5MliDTpmUWbN3QeGzeJBWj4CSaRFngCWUNUhivQ/ZUO0vh
- d72x3LwzFrFFcpFmiY+ynisWdM/MCmKKYbDCObpha5ahZmNJw7YIQU/zAOKU
- 89K7WnrfzZ1rqqXmNBJkGGl+3OsCZMfJjjP4SMI0bjLcPmvpiVSGVmQ6gFsS
- botUMtjcawF8SnEpsrWuZjeK+dW3r72aZhg+LfLIOTua7Sq9GD0tDhuHmqVt
- Z2kcW6QAymuW/YoOu06KzzcI49MCY9yJjHNsn4hSuJGSKGT2rs7ngrhXjARn
- OYh5hu7ROhxDmMODNtzBQ4YLET2yHTl2EEtSkYoIuIrJ4dMvb0CoED6D0thf
- xI6XQXebeKSqzJ3nROrVcYYfz30kJ0vkOQ9GVGC6IysItUHBVyVHH5vvOvl4
- YuTMVb+ztPOyZqsZ1VZpzpM78NKXIBOvAB3jvuh4aP6lLnp04T2ZacbeHB2u
- SEeHkqfPI3mCPqctD71u6zluZVosrZce2VOlTnrKEMkpwbAv7FnwxNlVX/Do
- UPbM+GWvQhPvf/V7ZJ8SllvKIn5XRBmUA0q7M9nqvKV4sLjUSg2TJUJuK2uF
- 6gFfUJ7J7WWRjmMRWYjMBOVOxdfH4uGZSblLGQuysBQuCfeU+vHe+MWwP+zI
- xbsFbLBvIfDXG3Z06OzRr9yUFQHnokfL2xH+gIsvyZcUf5i8Eb+88P5nyVEc
- VBLykEL4tYqh+ooly8oAw+9/8pCbg/3i/GaaBknNtzdLMUyc7/Nq/FwfL2yR
- HrpcEJ+6bkBWZt0mm5+owLMVebxB+SWRwZLIg5e2RpXMNEr7pV45GHIVzal9
- m6rEvJmhy9WxRIArhdyWZqXEVRKczbSoJZYuxu5k67q+Y6h2waL+wOMi26Rx
- oHOdlu8dl16qy7Wr5RJaJRZKGoZmzWdVzjUaSutmwUprD3WxWb8LsXECnqqq
- Bz6IH309owV+eJGj0S1qyeMIvYP0NPoa7UeQfxMXHQa9/c6aDFNIFOXQiTC1
- eUcmgB9cqSC1/ehC90ncXoHbdwSlFrevIW5PDe4ALrm4w7QqfsF3GHr6GlcF
- JqvAVFyEyzUI1xA5iTBCCKO1CJddhOs1CGOIkgcFwhohiRQYngjH3uDjd7gt
- LPzkCHeqLfTjhmPhcFEas46Fopeghzm9KXxGGsUdx50dJepNCH70WPR0OFkY
- k867SGUOd11jnrnH2hMNf0FUJsL36e2di77FI4ZqNu2IOWyEFe10cgtIOrx6
- 8CUWHV49WHJ59WAey2Venzs8urzu/tVcVrDqcsmRaAu1vVVcbvmik2/x2IPf
- y2yEhR0UIRfpS810/mxooXEQ68RIGNOLFJ44jHrLjHpdRqInIsXrcltzuPX4
- 6nADCXFnO/qedMb9sB2lLArU/kLzG7T115vwJvFNEk/pjW+T2MR3SfLs95tg
- HM/xYhNXOFo4VI4tjk4OP8cAR5rjGkeGQ+PY5ujieMLRzTHGscgxy5Hg2OGY
- 4tjl0Dn2nOE+R5RjjmOJY55jmeMuxwrH6n8UZ3xFehEAAA==
+ H4sIAAAAAAAA/91XbVMb1xV+riSkZS3DshgD6xgTW7ZBGISJa7dGpXVwiFXA
+ L4HQONhNFmmBBWlX3bsiuG+haZt/0Q/tL0g/JalnOoz7rT+l/QudTs9d7UpC
+ b0j2ZKZTzazu3XvPec5zXvbs3X/8569/A3ALf2CY0K2cY5u5w1TWLhRtbqSc
+ kuWaBSO1ZulFvmu7a67uGstuDIxB2dMP9FRet3ZSj7b2jCythhn6coZjHhg5
+ T/LRNsP8xEq94PzkSktL92vU5xneT6/fbdRfmFhf7xQkTaILhHRlxXZ2UnuG
+ u+XopsVTumXZtG/aNH9ouw9L+TxJTXaEmSkU8zH0MkTTpmW6CwxDzbzciOMM
+ 4jJknGW42hFyDP0MPQd6vmQwqI2YFOBCydW38sbrBni1Rv21A1wLEgS4dehq
+ pcuhOy9juH1QanViGKVg1Pq9YnJX+D4z0YbkiaIVGkQy08LdbmACh5Odq8Qw
+ xhCeKFfEuIxLeJthoNajVb0oHJrumAkpEIdP0stN/Nl4Ix8JOb2+PL++cUpa
+ 65ViuCbjuvAsXnTsXClb9oxhu0mFNlnZt928aaX2Dgqp7ZKVLT+aS/5srl05
+ BsX8z9bF3LW99HRre49r3FvL2kX/GZgOILO2Y5dc0zJ4atEmFavkNZp0ReBD
+ 6hqkMNWE7Kl+BuV3tbXconcvaovkEu0aH3U9X0zyc+ZImGEYqwmOabmGY+n5
+ VMZyHUIwszyGWep52V0ju+/3zse6oxcMEmS43j7dawJkx+uOc3hHxk3cYrjT
+ 6asnUVtaiZsx3JZxR7SSsfZRi+EHVJeiW5t6fqPcXyP7xoubDOOnVR7DYWcV
+ /F3U9L+7qun/uyp/zcKYi+HHMu6JwhBZnmPYaZLCze8kY/9qnbHuDf7PJ0iE
+ l0tYeu1UvRPDAxkZkarBJvGhzrRjuP4T++nEaeFvHWDH2M7TfWqZwlY0HPcF
+ udjEWrFF9k4Lx5QXjy7Mp5MUZFISp0h31+QLEh6V27C3LeEJw7mJJhzjWMHa
+ GdzFOsPZhJnYTlQDxDJ0QkwIuJrF8dNrNiZUCJ9Bax0vYscroLttInLijNlN
+ Rpodohk+6zoljefTLhMjjr/0gnqG+Blo+CQIdNV9P8jVhesdH7kHAsurhqvn
+ dFentVDhIEyfYUz8SeIPlMt9MQnR5qEpZvTKDeVustCfjo8+ko+P5NBISA5J
+ EW+s3Ib9MVQdFbEZLASXEqoqKAMVHLrXLpGCJqkRNfQgNMsuR6TjIyU0F1XC
+ Gi28+nM0pEQ0VempiER9EW1MiWl93mKv9y/PSuWtXhqYIhPymYpWvBnwWW1H
+ 6auI9FdFFCEyJykDWmSEzao0G/Rn08o5bVJiqqwGaueD+ezI7LAaVT252SFh
+ QBp9EPv7N+z4yLOmac87s/YGNi5o++1snFfe0uKqRNhlhIuX39jimHZLuSTA
+ fVvJinHyaNz3SFbe1qIqZXv28oNXX8qe4hUtrSQ0wq9XjDdXDDJXAbj66osQ
+ lZE0Kgp1ru0jUfeZz6iV3ejuS26qq+8ktkwXtRKIr2r/8as94LUx3vCWna85
+ MrZ4xZLIWCDy3qFr0KHZtgJ76y88DOUEzZl9l96li3aOWkn/CgE+LBW2DGdd
+ NA7B2c6KY6tjint/sXfN3LF0t+TQ/MIHZbYZ68DkJm3fq57y6ROgfrdyWj8h
+ Fs9YluEs5nXODbqV1+ySkzWWTGFs1IfYaICnA3wIEdGy0ItR9CCKMH5Fd7dp
+ pIgj/hLy0+TX6DuG8pXoaPg1/Ue9PQW/ERJlOQxApfFzTyaGI19KonEUgzjX
+ iDsscEeOodXjjrTEHarDvYC3fNxx2hU/6SUuPf0al7/yunAVU/MRLtYhXEGi
+ EeE6IUzUI1z0Ea7WIUwiSREUCI8JSfR69Yaa+gbfe4k7wsPvH+PuSQ+juOZ5
+ OF6WxrznoZil6WLebAY/JI2yxSnPokyzG4IfXb+lq99752Da+w+oLPhUPiZo
+ kVZ12qdyT1B5twmVOY9KsizdlMoiXaEKqbBP6kdNSQ1FakidpHbfp7bhR2lk
+ Sn2fqAWxygiCP2kgGCeT1ViNVAiOYAnLfrXUxuq9drGSagitYNVP/HP/ERhK
+ qo89Qh/Qf3gh+S0+ZDjJpo+sBuHqoyrfwE89XkP4CE89XkMU+nLghvAEmxVe
+ Dz0eg+EKi9rgPCMOZS4FEu2hcfgEl9uR5PS3+DSEv1TYCA/7yYXzhCwY3SK1
+ foLTiZFwZhhbyHqMhiuMhn1GYiaeqiCVPwtS18gNJPSF9/iE6UsdHtt7/pjx
+ x995IL/E72n8I+nliIqxiXAG2xnsZLALM4M97GeQR2ETjMOCvYlrHD0cRY6f
+ cwxwRDkucDgcVzg4h8tR4hjkyHKc45jkeMoxz5HmOOBY5Jjh+IzjkOOFt/IL
+ jgWOJMeSd3ufY4XjY44nHJscqxzPOJ7/F4J6TSkwFwAA
""",
"""
androidx/compose/runtime/SnapshotStateList.class:
- H4sIAAAAAAAAAI1QXUsbQRQ9M5tkdY26ftTGj9pXG8Q1UhCsCFYoBLYWmpCX
- PE2yg45JZmRnInnc3+I/6FOhD7L46I8S70ZftC/OwLn3nDncj3l4/HcH4Cs+
- M9SFTlKjkknUN6NrY2WUjrVTIxm1tLi2l8a1nHAyVtb5YAw7x+2j+ErciGgo
- 9EX0q3cl++7byf8SQ/hW81FiqBwrrdwJg7fzpVNFBX6AMmYYSu5SWYbd+P0T
- UZOleGDcUOnop3QiEU6Qxkc3Hu3HCvAZ2ICkiSrYPmVJg7bIs2rAazzIs4CH
- BHlWy7N6aSbPQnbA9/n38v1thYde4T+gEm1G9RC+mmBv4GjqM5NIhsVYaXk+
- HvVk2ha9ISnLsemLYUekquAv4mxLXWjhxinlQcuM0778oYqH9d/PO3aUVeQ8
- 1dpQC2W0RQOcPqg4NEfxX4TrxKIpB8r1v5j9QwnHBmFlKm7SBarPBgSYo+hh
- a+ry8Gkaa9imeEieKnnmu/CaWGhikRBhAUtNLGOlC2axig9dlCzmLNYsPlr4
- TxVS9+dFAgAA
+ H4sIAAAAAAAA/41Qy0ocQRQ9VT0v24n2+IijJpqlGcRWEQQVIQrCQJuAM8xm
+ VjXThdY8qqSrRlz2t/gHWQVcSOMyHxW8Pbox2WRz7j2nDvfeU7//PD4BOMAX
+ hobQcWJUfB/2zfjWWBkmE+3UWIYtLW7tjXEtJ5yMlHVlMIatk/ZRNBB3IhwJ
+ fR3+6A1k3x2f/isxBH9rZRQYSidKK3fK4G197VRRQtlHERWGgrtRlmE7+v+L
+ aEktGho3Ujq8lE7EwgnS+PjOo3wsh0oOYGBD0u9Vznapi/coSpZWfV7nfpb6
+ PCDI0nqWNgqVLA3YPt/lZ8XnhxIPvNy/TyPaLJ8UvDtjZ+jo9HMTS4b5SGn5
+ fTLuyaQteiNSFiLTF6OOSFTO38SZlrrWwk0S6v2WmSR9eaHyh9Wr16AdZRU5
+ v2ltaIUy2mIPnH7pLUr+aYRrxMIpB4qNX5j5SQ3HOmFpKq7jE2H11QAfs1Q9
+ fJ66PGxM6yo2qR6Sp0qeD114Tcw1Md9EgBq1WGhiEUtdMItlfOyiYDFrsWJR
+ tyi/AEh/yvNKAgAA
""",
"""
androidx/compose/runtime/SnapshotStateMap.class:
- H4sIAAAAAAAAAI1QTU8bMRB99ibZsKSwQKGBftBjoVIXUE80QqKVkCKWVmqq
- veTkZC0wSexo7SCO+1v4Bz1V6qFa9ciPqjoOvbTlgCW/N/P8bM/M7a/vPwC8
- xUuGHaHzwqj8OhmaydRYmRQz7dREJj0tpvbCuJ4TTp6JaQjG0OmcHqaX4kok
- Y6HPk0+DSzl077J7tKP/JYb4Xy1EjaHRUVq5I4bg1U7WQgNhhDqaDDV3oSzD
- 6/TBRdIfK+nIuLHSyZl0IhdOkMYnVwF1zDyEDGxE0rXy2R5F+T5DUpVLEW/z
- iDdpx1UZVWW7KndrzaqMGRGL+QHfC97Xf940eFzz1w7opVPaGaOnEf9Vy5uR
- o/o/mFwyLKdKy4+zyUAWX8RgTMpqaoZinIlC+fyPuNBT51q4WUFx1DOzYihP
- lD/Y/HzXbaasIuex1oa+UEZb7IPTqPyiOvzkCLcoS+Y5UN/9hoWvFHA8JWzM
- xRd4Rti6MyDCInGA53NXQKeeN7FNfEieFnke9RF0sdTFMiFiDytdrGKtD2bx
- GOt91C0WLTYsnli0LcLftbcdUGUCAAA=
+ H4sIAAAAAAAA/42Qy04bMRSGf3tyY0hhoFxCb7S7QqUOoK4gQqKVKkUMrdRU
+ s8nKyVhgktjR2EEs51n6Bl0hdYFGLHkoxHFg08uiC//nnM+/j318e/frGsAH
+ vGHYEjrLjcou44EZT4yVcT7VTo1l3NViYs+M6zrh5ImY1MEY2u3j/eRcXIh4
+ JPRp/LV/LgfuIP0HO/wbMUR/sjoqDLW20sodMgRvt9ImaqiHqKLBUHFnyjK8
+ S/77kXTHUjI0bqR0fCKdyIQTxPj4IqCJmZeGFzCwIfFL5asdyrJdhrgsFkLe
+ 4iFv0IrKIiyLVllsVxplETEKLOJ7fCf4WL35UeNRxR/bo07HtFLmm0a/Pej9
+ 0NEQn0wmGRYTpeWX6bgv8++iPyKynJiBGKUiV75+hHNddaqFm+aUh10zzQfy
+ s/IbG98eRk6VVeQ80trQFcpoi11w+q/Hqfz3kT6nKp7VQHX7CnM/KeF4QVqb
+ wVd4Sdp8MCDEPMWAqHcF2JzFZ3hNcZ88TfI86SHoYKGDxQ4iLFGK5Q6eYqUH
+ ZrGKtR6qFvMW6xYtiw2L+j3Q7/VyagIAAA==
""",
"""
androidx/compose/runtime/State.class:
- H4sIAAAAAAAAAH1QTUvDQBB9k7RpjF/xu1YQj9WDUfEgfoEXoVARbBGhp7Vd
- 69p0I91t8Zjf4sEf4UGCR3+UOFFPKu7hzbw3O7Nv5+39+QXALlYIq0J3Bonq
- PETtpH+fGBkNhtqqvowaVlhZAhGqh839+p0YiSgWuhudX9/Jtj04/i0Rwp9a
- CQWC35X2UsRDSZivrv/VV6yuN5scZ+q9xMZKR2fSio6wgjWnP3LZLuVQIlCP
- pQeVsy3OOtuEwyydCpyyE2Rp4IQ5+K5/U87SDc/P0pDWaMfZci5mQ7fi7GXp
- 1etT4fXR8yoFvxAW8xk7hLX6/5tgI9QktoHi6OsrYUOLe3Ob2M/6Zs8Sxhqq
- q4UdDrgcNJLhoC1PVcxk+eJr1qUy6jqWJ1on3KQSbTx+H0Xkh3hVHnjlKDNz
- 4MP9zlwsf8YlVDge8Y0x7glacGsYr2GCEZM5TNUwjbAFMpjBbAuewZzBvMGC
- waLJaekDuDg1Tf4BAAA=
+ H4sIAAAAAAAA/31Qy07jQBCsthPHmJeT5RECQhzDHtaAOKx4SXtBihSERCKE
+ lNOQDGGIM0aZScTR38KBj+CALI77USvaYU+AuHR1VU/3VPfff88vAPaxQdgU
+ ujdKVO8h6ibD+8TIaDTWVg1l1LLCyhKIUD9qHzTvxEREsdD96Pz6Tnbt4cln
+ iRB+1EooEPy+tJciHkvCUn37q75ifbvdZiw3B4mNlY7OpBU9YQVrznDisl3K
+ g58HEGjA+oPK2Q5nvV3CUZYuBE7VCbI0cMI8+K5/U83Sn56fpSFt0Z6z41xU
+ Qrfm/M7Sq9enwuuj59UKfiEs5jP2CFvN78/BbqhNuYHi5H2fsKXFvblN7LT+
+ a2AJMy3V18KOR1wOWsl41JWnKmaydvE+61IZdR3LP1on3KQSbTz+H0VMd+N7
+ eeC7Y42ZAx/u/8xFbYpVrDMe84sZ7gk6cBuYbWCugXkscIrFBkKUOyCDCn50
+ 4BksGSwbrBismpyW3gBNs/uhAwIAAA==
+ """
+ )
+
+ val Effects: TestFile = bytecodeStub(
+ filename = "Effects.kt",
+ filepath = "androidx/compose/runtime",
+ checksum = 0xb63b1aec,
+ """
+ package androidx.compose.runtime
+
+ @Composable
+ fun SideEffect(
+ effect: () -> Unit
+ ) {
+ effect()
+ }
+
+ class DisposableEffectScope {
+ inline fun onDispose(
+ crossinline onDisposeEffect: () -> Unit
+ ): DisposableEffectResult = object : DisposableEffectResult {
+ override fun dispose() {
+ onDisposeEffect()
+ }
+ }
+ }
+
+ interface DisposableEffectResult {
+ fun dispose()
+ }
+
+ private class DisposableEffectImpl(
+ private val effect: DisposableEffectScope.() -> DisposableEffectResult
+ )
+
+ @Composable
+ @Deprecated("Provide at least one key", level = DeprecationLevel.ERROR)
+ fun DisposableEffect(
+ effect: DisposableEffectScope.() -> DisposableEffectResult
+ ): Unit = error("Provide at least one key.")
+
+ @Composable
+ fun DisposableEffect(
+ key1: Any?,
+ effect: DisposableEffectScope.() -> DisposableEffectResult
+ ) {
+ remember(key1) { DisposableEffectImpl(effect) }
+ }
+
+ @Composable
+ fun DisposableEffect(
+ key1: Any?,
+ key2: Any?,
+ effect: DisposableEffectScope.() -> DisposableEffectResult
+ ) {
+ remember(key1, key2) { DisposableEffectImpl(effect) }
+ }
+
+ @Composable
+ fun DisposableEffect(
+ key1: Any?,
+ key2: Any?,
+ key3: Any?,
+ effect: DisposableEffectScope.() -> DisposableEffectResult
+ ) {
+ remember(key1, key2, key3) { DisposableEffectImpl(effect) }
+ }
+
+ @Composable
+ fun DisposableEffect(
+ vararg keys: Any?,
+ effect: DisposableEffectScope.() -> DisposableEffectResult
+ ) {
+ remember(*keys) { DisposableEffectImpl(effect) }
+ }
+
+ internal class LaunchedEffectImpl(
+ private val task: suspend () -> Unit
+ )
+
+ @Deprecated("Provide at least one key", level = DeprecationLevel.ERROR)
+ @Composable
+ fun LaunchedEffect(
+ block: suspend () -> Unit
+ ): Unit = error("Provide at least one key")
+
+ @Composable
+ fun LaunchedEffect(
+ key1: Any?,
+ block: suspend () -> Unit
+ ) {
+ remember(key1) { LaunchedEffectImpl(block) }
+ }
+
+ @Composable
+ fun LaunchedEffect(
+ key1: Any?,
+ key2: Any?,
+ block: suspend () -> Unit
+ ) {
+ remember(key1, key2) { LaunchedEffectImpl(block) }
+ }
+
+ @Composable
+ fun LaunchedEffect(
+ key1: Any?,
+ key2: Any?,
+ key3: Any?,
+ block: suspend () -> Unit
+ ) {
+ remember(key1, key2, key3) { LaunchedEffectImpl(block) }
+ }
+
+ @Composable
+ fun LaunchedEffect(
+ vararg keys: Any?,
+ block: suspend () -> Unit
+ ) {
+ remember(*keys) { LaunchedEffectImpl(block) }
+ }
+ """,
+ """
+ META-INF/main.kotlin_module:
+ H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuSSSMxLKcrPTKnQS87PLcgvTtUr
+ Ks0rycxNFeJ0TUtLTS4p9i4R4gpKzU3NTUot8i7h4uNiKUktLhFiCwGS3iVK
+ DFoMAHVSFrpbAAAA
+ """,
+ """
+ androidx/compose/runtime/DisposableEffectImpl.class:
+ H4sIAAAAAAAA/51TS08UQRD+enbZx4iyLPJGQEFZQJgFvS0hUYRkkxUNS4gJ
+ p2a2gV5me8h0L8Eb0Yu/w3/gwWg8GMLRH2Ws3ocgxgAmM9VV1f1VfV1V/ePn
+ t+8AnuIJwxxXlSiUlWPPD2uHoRZeVFdG1oT3Qmqy+U4gVnd3hW+KtcMgCcaQ
+ qfIj7gVc7Xmvdqq0k0SMIbEklTTLDBO50kFoAqm86lHN260r38hQaW+tpS0U
+ prcYPlx1ammudG1qZT88FIXZ6wM2hK4HprDcoDJRCqM9ryrMTsQlUeBKhYY3
+ 6ayHZr0eBAW6n2ggU0gzjF6gLpURkeKBV1QmIrj0dRK3GHr9feEftPCvecRr
+ gg4yTOVKl+tXuOAp2yB7xKsTt3HHRSe6GGI5a3eg20UcWYbxqyrciTTupuGg
+ lyFu9qVm8K5fHdtpuvH7qzp00wb9R38YutssXgrDK9xw8jm1oxgNMLMiZQUY
+ 2AH5j6W18qRVFhii05MR1xlwXCdzeuLS19Av/KcnKWfg9GTRybPno9n+jDPU
+ k41nnXy8ITvysbOPCSeVsDKT3Bj71/6bs3dxq1E8m3mRWT7ZNu/zrjDkb1oy
+ hoUbV43ms5179dgIGslQtUlsvm0EdZsAPX9gGNJluae4qUeCYXijGbuojqSW
+ FPnZ+WOgUVoJK3SoqySVWK/XdkS0abPby4Y+D7Z4JK3dck5ejvX7FfwR1C2H
+ 9cgXa9JiBluYrb+yI0/jHG+0Omunm6xZshwM4jGtCfKnmoNADyWBGObIKtG+
+ Q2tmNut+RWbmC3pmZj+j71MDOU/yDp1MUAwXQ+ii1SNfXxODfgzY2SLN5mOt
+ fEliAiRZK6GDhYacwSKtK+QdIgLD24gVMVLEvSJGMUYqxou4jwfbYBoTmNxG
+ SmNA46FGWuORxpRGTmNaI/ELZm5MUZ8FAAA=
+ """,
+ """
+ androidx/compose/runtime/DisposableEffectResult.class:
+ H4sIAAAAAAAA/5VPzU4CMRicrwvsuiou/qIPQPTiAjHx4MlEjWswJphw4VTY
+ YgrL1tBCOPJcHgxnH8r4LTyBSTOd+X4605/fr28ANzgjxDJPZ0any3hopp/G
+ qng2z52eqvhBW9ZykKnH0UgNXVfZeeZ8ECEay4WMM5l/xG+DMfd8eAQ/3Wwo
+ gnd51SPUOhPjMp3Hr8rJVDp5RxDThcfWVEBQAAg04fpSF6rJLG0RGutVNRR1
+ EYpovQr5iEgEo/p61RZNegkicSGa3nOjmG4TWp1/foKDsG+4LdnriWPxbuaz
+ oXrSGec/727Xe9pqXr3Pc+Ok0ya3FbZECZvgJUIZFWYCJxs8xinft/y0z52g
+ Dy/BToIwwS72mGI/QRUHfZBFhFofJYtDiyOLMuMfURqWxJYBAAA=
+ """,
+ """
+ androidx/compose/runtime/DisposableEffectScope$onDispose$1.class:
+ H4sIAAAAAAAA/8VUXVPTQBQ9mxZaQoHwIQIqVkFtA5Km4hcwzDBYxmpRh2p9
+ 4CltQ1mabpgk7fDk9CfpjI6jD06f/VGON0mRjjry8eJD9t69e/bs3bP35vuP
+ r98ALGOVYcUQVcfm1SOtYjcObdfUnKbweMPUnnCX5kbZMnN7e2bFK1bsQ3Pe
+ FmHcnNdjYAzKgdEyNMsQNe1l+YBgMUQYtDOz7phu06JNfQz9a1xwb51hLlWo
+ 257FhXbQamh7TVHxuC1cbavrZVbTJYbsaai14/U3RLu6HmxSTi4Qns+QPO2w
+ BGQMDkBCgiGSSpcSiGFYRhQjDFFvn7sMa4WLy0iPEKuGE4bZf+cSwwTpxEXL
+ rhN4IpUu/C4/ZTuJy4O4hCl6hnMqxDB6HNk2PaNqeAbFpEYrQuXC/CHuD2Bg
+ dYofcX/2nryqzpDqtEfkTluWpqTAKJLKOu24PNVpZ6UMexZXpBkpE3k66eOz
+ DPrZNQuLhFKhk5fOp3QMKYaBX3IzFE8vrnMnloCKBQY5DLpLdSortfcYLjzT
+ EYalFe2mUzGfmOVmLXfkmcKlE+lefS3DalJy74rbG696aOTnAYesFpPH3pa8
+ kNSTPZCLV56sFmR9Tl/U9eUVmuRkUqrIa8Lwmg4lE920q2RGClyYL5qNsum8
+ 9skYxgp2xbBKhsP9eTeYyAthOpuW4bomdcRITlQs2+WiRpW0b1dJnfDuW9xH
+ T/xNCIbpnTD3Enc5sW4IYXtG8D4MV7predH6YxU6tWeUqqMfTFH8fiVfp+KU
+ MEMfdRjiZLMUWScrkZXVhU8YUj9D+RDg7tFIu9GHYfozgrooQGEUY37Jk9fL
+ OkDeOCFZwPnY7wiyg+pHDH3BNMPbE1I5IFICKp84EUK7xP24H2BYgAKm8YDG
+ KNJYxMOA4y4ekf3PlUFXBOVDr0ACXd1FJI9reczmcR1JcnEjj5uY2wVzMY9b
+ u4i6vnvbxbiLO1ihzb5WS/RpASjzE1u+Yp2EBgAA
+ """,
+ """
+ androidx/compose/runtime/DisposableEffectScope.class:
+ H4sIAAAAAAAA/51UW2/TSBT+xrnYNaVJw603CgsBWgq1E9gLpCBBF7RBoSAC
+ lVCfJs60TOuMkcep+ljxsP9hX/cX7D4VLdIqKm/8KMQZJy3d7kMpljzn/p05
+ c87Mp8///AvgNu4wzHPVjiPZ3vKCqPM20sKLuyqRHeH9KjXJvBWKR6urIkia
+ QfRW2GAMxXW+yb2QqzXvWWudTDYyDPkFqWRynyEzM7s8jBzyLrKwGbLJG6kZ
+ /MbxUtUYhiLVNwmG5kxjI0pCqbz1zY632lVBIiOlvccDzq/NfnuCF0J3w4Qy
+ tI5CXdizv6Liave/K8nlRhSveesiacVcEjhXKkp4P9FSlCx1w5C8CvvF9sMd
+ FBimD+xOqkTEiodeXSUx4chA2xhlOBO8EcHGAOg5j3lHkCPDtZnG4UbVDmia
+ BmStZnp1CqddlHCG4e7xelTe33O5YuMclXp0l9LZGHcxhgkG75inaWOKYaQs
+ y6vlA7PB6gwXj0rMMLrn8lQkvM0TTjqrs5mhu8DM4pgFBLdB+i1pJJ+4doVh
+ pbc94VpjlmsVe9uu5VipYNhUZ431tquWzx7mdv/Mk/hkqpiZsPxsdcTJFnMT
+ Tilbsnzbz/+2+7vz8T3rbe++s2w35+z+UfWZSVFlJnHlO4artFfUwUrdvpOe
+ 30jo/i1GbTqkQkMqsdTttET80uCY0Cjg4TKPpZEHyqGmXFM86cbET77oZ6+r
+ TaklmR98nVyG8mHr/uz9x224rpSIF0OutSDRbUbdOBCPpUk2PoBY/h88KrDo
+ +TCfRSdDrwmtPkmeaRDR3PUdOH+n5gqt+VR5AlVah/sOGIJLdJS0wwRlgpeR
+ gWnr6blS8T3OZu59wNjruR1M9nD+r30sl6iDEboWpRTvIsU4hDGNC2Sh6AGy
+ 4QpkZbiVxp6kJ7W/kxGiP9Jvs4GQwU8pMKOxN984fk5DPPxCdJH0P9CGL60g
+ U8flOsp1XMFVYnGtjhnMroBpXMfcChwNV+OGRl7jhMZNjYLGPGm+AB149ZDV
+ BQAA
+ """,
+ """
+ androidx/compose/runtime/EffectsKt.class:
+ H4sIAAAAAAAA/+1Y21Mb1xn/Vhe0LALL4q6kjmJIA8JYWnExWBjH5hKrljGV
+ bKhL63QRC16QdhXtSgYnbdxmOtOX/APpQ2f63Je8JG4z43qat/5Rnf7O0UpI
+ aAHBOEwfyoz2fHvOd/l9t3P28O///OM1EU3S7wW6quhbRUPb2o9mjXzBMNVo
+ saRbWl6NLm1vq1nLvG/5SBAosKuUlWhO0XeiDzd3seAjt0BSRttSK4wCDY2k
+ 9gwrp+nR3XI+ul3Ss5Zm6GZ02aZiidE1geKncc1V1x/rmpWY50IfpI6FucDf
+ lc2cmgCElFHcie6q1mZR0aBU0XXDUioGVgxrpZTLgatN5YhFkgS6UgdG0y21
+ qCu5aFK3ihDXsqaP/AL1Zp+p2T1bflUpKnkVjAJ9OJI6GpVE3UyGKdkBfj91
+ 0SWJOinQaM/BeR8FgU/Ty8aeKlDPyGizBT/1UG8HdVOfQOHTIo7ELWqmHaBW
+ EyXzmP/pNK658eOzctRoJmsU1MRY6wJp1SzlqunvriJZVAtFNatY6hY88+VV
+ 01R2EKeB1aJRRiWGFSucUxXTChu6Gt5TDwTy5tSymhNo8KgKeJBiS1DkXUqn
+ H6YFeu8w1slcTt1RchkUj7q0n1ULjN9HV6HoOFvXRRoWSLSMSt6PZs+uBj/9
+ lD6UyEMjyPOchhKfR4GNOJdNhMYkGqJrpyZahhdTTtXYQp6/PrvcxWV++KSG
+ RjfafT/esvZkvpDzUZzFNiTRBE0KNDasDSvD40U1r+Y31eJ4beMbPio8LAsk
+ JAXqhMT2cFVAIA/Sj6VgcyAF+vjkTeIMifrb29B0cak7Q1jjlRBieNKaj+eK
+ 3+sfR/f/ZEQnKhHFMD2ycb5w/fkcghcXC+adKdJS9cukZGm56J1iUTnAmf0x
+ 9lZoP3i4LdCokxvJUYdJPyXpZxLdo/tnCjV2kHZNL5Qsc1gr46xy0CxQV0pB
+ jJ6pW9UzWGvhdLUZskbRgHu6auJjB6HRS3wDPGSwP5TGmg1XYuXdzBnZPZHS
+ Il0R6LNz7fdvC8vYselujFBln16TaJ1t0aPHpqNRim3QX72lbfJtedwydmx/
+ f/nRtqgL9wZbz+fn20EuHCoK7HJV6QPVUrYUS0HPuvJlN65JAnuI7EE4/fcY
+ 4cLivsaoGKgtWXD98ObltPTmpeQKuPgw4Gr48RX2EN0gLldGtiZ62Rh6H4sh
+ V0yI9wdcoe6gJ+iKefjTG3P/669tLrHtni80VmUSA76QZ0CIiSeyTzWzi4H2
+ FgRvnyQoBqQWVMxXVfQFOkL+oCgKQS4U8189VZiNgc5QKtDVAoh3A5dCA9Au
+ BUWuR4gFgm22Lfc93w+vhDcvucLLobsnKGxZTTA006SmZeHu0LItfFxYWlbV
+ Gbpuq2pZpCfUF+jlIn4Ev8rch4D/weVBJQ6yUsYeROxD1u6G+vtF7KxHu0Dy
+ mQ93XJCrtpf2LRV3cEOvgnh0wJWGT9kgEg742UVYsvv/+h6O30j9NlS792eM
+ UjGrLqqbpZ2adXY/LCu5kioIZubBndU6NdJ9rkOKZMJValkaC8vhOpbT/70C
+ iXg4bW9SJ4pUmSATSUnykHxNlidvypI8MRS/KctTUjzGiWkpPsOJG9LEJCdm
+ qjyzNk88ZvPEZZsnHj/0g0fgfM4A2GRsSOZwJmc5MS1NVYgb0vQNTsxIM1Oc
+ mJVmJxkBOHKsQsEdOc4pAFqS8KW3YGzhjn8phUyvlJj/j1jZsCwbWSW3phQ1
+ 9m5Ptme0HV2xSkXQ76Qr8JJ6WTM1LN85vD3ianl0tfafnQY26fB/Drj82zJr
+ DvqkSvUsawxGj1MpkUwu3PzZXwcNkpfa8PYcb3OYR+NRVyTY8YouR/5O/QKt
+ f8NOGtrHU8LYRiJ1Ujsd4N1f4cb8AMYXnM9Hn2Fsw4qI8XP8fDiuIEAwNUgh
+ LDFTt8jNhbsrpr6noSfBD76l0e9o/J81e0yLRO/X2erG+/WarXfot9yL3+EX
+ xPq7mPuJSVfoPZXCkDsRSdR2Og8kLBi9YxyJ+5Z7/nuaeDL2HU3V+97H4zQC
+ XSYwjMCeST00yrFFoEDCyjTdgDIRUZ0B5WJKaZZucoy9HLlgI481YetkoZf5
+ 4V5FmLAR7kOpl0G4VkE47572MIjXHCHKUGkBgoxgWTAb5xAnsdaBlVsA5sHo
+ 5xDdXKoKsY/mQQmcYmBdNti5JrDd7hrYRsi3bchfQnUbxv7xCuRpj3vayzCP
+ O2JOQEEJmBIIaQlzcxzzPFb8sHkHSL3gqGBmueqvYe6vYe6nu6BcnGLo3Tb6
+ j5rQD3qOoG/0YcH24Y922IciwWX4UCmPSOT1K0o9qOXAqUz8UNFLZTTFAsqw
+ TFdpsZaDAaB4wL3oreWgGy20Qg859iFaAkWcqi+YReeCaa9DvurUXJlqcz1y
+ bq6VhuZK1zXXz8/fXKtOzZWpNdc6i9ovmqvgMXQ9B6bHsPcclbDW0FxPTmyu
+ dF2sHp/eXKtOzZU5bK511lwOEJ9C5T4gPEWw9mH2k4bm2mixudJ1zfXLVptr
+ 1am5MnXNtc6aywHzM75b+zH2YOwjraG5fn3m5krXNdevztZcq8c3V6apuZzL
+ xE8FxP0FGqmAMnyB5vq0obk+abG56gvm6WnN5aEvOKNJLzH1/++vi//+4ttQ
+ Aen4DfKsbJA7SZtJyiZpi9QkbdNOEvWtbSA5tEt7G9Rv0qBJOZzVJuVN0k0y
+ TCqYNMsn5026a9ISpxdMWjUpbdKnJt02KWFS1KSQSV6TirwuumDVwq/EtZf/
+ C7XJpgW2HAAA
+ """,
+ """
+ androidx/compose/runtime/LaunchedEffectImpl.class:
+ H4sIAAAAAAAA/5VSW08TURD+znbZbleUsgKWi4iCUKiwhfhWQqJETJOKBpSY
+ 8HS6Xcppt2fJXhoeG179F/4DH4zEB9Pgmz/KONuLIKiEZPfM5Xwz852Z+fHz
+ 6zcAT7HKkOOy4nuicmzZXuPICxzLj2QoGo5V4pG0D53Ki4MDxw6LjSM3CcaQ
+ rvEmt1wuq9brco1ukkgwaOtCinCDYTZbqnuhK6RVazasA0oRCk8G1lZPWy0s
+ 7jGI61Dry32A7fleFArpBNamR8xkxGPEOeAdFS5sFHKly8TIGdeaLXl+1ao5
+ YdnngmpwKb2Qd+tte+F25LoFBjXkQV1HimH6AjMhQ8eX3LWKMvQpWNhBErcY
+ Rqkxdr0X/Yb7vOEQkGEhe5XFBc9unKRKrAZxG3cMDGKIIZGN7QEMG1BhMsxc
+ 18BBpDCSgoLRmPahCBiWSzcYI722cl37b9r9vzWfYbiPeuWEvMJDTj6l0UzQ
+ 8rH40OMDDKxO/mMRW3nSKrSXH9qtCUPJKIaSbrcM+jp6x6ZfV/R2K9NurSl5
+ 9nzenEorExmdmYapm6qp5AfyqqmZaoblWT7x/ZS1W2cfNSWt7Sz+D/j+7ETt
+ g1Wqkzw7UUjq4zGlNRYTNfsPOp/GhYH9o1kEMbrdD1bqIUNqV1QlDyPfYZjc
+ 6Y6pKJsiEGXXeXa+nTTdTa9CoKES5dyOGmXHf8sJE/PwbO7ucV/Eds85dznX
+ 78X8I6mx60W+7WyJOGa8F7N3pTpWacPUznjMeOHIWiRLwTiWSGrk17vDo93V
+ kECOrBLdKyTTOdM4RXrpC+4u5T5j7FMn8gmddwipYQsGXmKI5DL5xroxuIdM
+ vA+kxfVYr14SKySTrFdQgdU5s8iT3CTvBBGY3EeiiKki7hcxjQekYqaIh3i0
+ DxZgFnP70ANkAjwOkAowH2Cho2sBRn4Bxaa1HQ8FAAA=
"""
)
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
index 48cb5ef..71b1990 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
@@ -45,6 +45,11 @@
val MutableStateMapOf = Name(PackageName, "mutableStateMapOf")
val ProduceState = Name(PackageName, "produceState")
val Remember = Name(PackageName, "remember")
+ val DisposableEffect = Name(PackageName, "DisposableEffect")
+ val RememberSaveable = Name(PackageName, "rememberSaveable")
+ val LaunchedEffect = Name(PackageName, "LaunchedEffect")
+ val ReusableContent = Name(PackageName, "ReusableContent")
+ val Key = Name(PackageName, "key")
}
object Ui {
val PackageName = Package("androidx.compose.ui")
diff --git a/compose/material/material-icons-core/build.gradle b/compose/material/material-icons-core/build.gradle
index 0276b88..615b415 100644
--- a/compose/material/material-icons-core/build.gradle
+++ b/compose/material/material-icons-core/build.gradle
@@ -15,65 +15,100 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
import androidx.compose.material.icons.generator.tasks.IconGenerationTask
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.compose.ui:ui:1.2.1")
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(libs.kotlinStdlib)
-
- samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
- }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
implementation(libs.kotlinStdlib)
}
}
- }
- dependencies {
- samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:ui:ui"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
IconGenerationTask.registerCoreIconProject(
project,
android,
- AndroidXComposePlugin.isMultiplatformEnabled(project)
+ true
)
androidx {
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index 0903147..b7e2fea 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
import androidx.build.RunApiTasks
import androidx.compose.material.icons.generator.tasks.IconGenerationTask
-import androidx.compose.material.icons.generator.tasks.ExtendedIconGenerationTask
plugins {
id("AndroidXPlugin")
@@ -26,7 +25,7 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
IconGenerationTask.registerExtendedIconMainProject(
project,
@@ -35,38 +34,62 @@
apply from: "shared-dependencies.gradle"
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make an analogous update in the
- * corresponding block below
- */
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.kotlinReflect)
- androidTestImplementation(libs.truth)
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(project(":compose:material:material-icons-core"))
+ implementation(libs.kotlinStdlibCommon)
+ implementation(project(":compose:runtime:runtime"))
+ }
+ }
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:ui:ui"))
- androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
- androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
- }
-}
+ commonTest {
+ dependencies {
+ }
+ }
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- kotlin {
- /*
- * When updating dependencies, make sure to make an analogous update in the
- * corresponding block above
- */
- sourceSets {
- androidAndroidTest.dependencies {
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:ui:ui"))
@@ -83,6 +106,21 @@
implementation(libs.truth)
}
}
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
@@ -102,43 +140,7 @@
}
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- def allThemeProjects = [
- project(":compose:material:material-icons-extended-filled"),
- project(":compose:material:material-icons-extended-outlined"),
- project(":compose:material:material-icons-extended-rounded"),
- project(":compose:material:material-icons-extended-sharp"),
- project(":compose:material:material-icons-extended-twotone")
- ]
-
- for (themeProject in allThemeProjects) {
- project.dependencies.add("embedThemesDebug", themeProject)
- project.dependencies.add("embedThemesRelease", themeProject)
- }
- // Compiling all of the icons in this project takes a while,
- // so when possible, we compile each theme in its own project and merge them here.
- // Hopefully we can revert this when parallel compilation is supported:
- // https://youtrack.jetbrains.com/issue/KT-46085
- android {
- libraryVariants.all { v ->
- if (v.name.toLowerCase().contains("debug")) {
- v.registerPostJavacGeneratedBytecode(configurations.embedThemesDebug)
- } else {
- v.registerPostJavacGeneratedBytecode(configurations.embedThemesRelease)
- }
- // Manually set up source jar generation
- ExtendedIconGenerationTask.registerSourceJarOnly(project, v)
- }
- }
-} else {
- // We're not sure how to compile these icons in parallel when multiplatform is enabled
- IconGenerationTask.registerExtendedIconThemeProject(
- project,
- android,
- AndroidXComposePlugin.isMultiplatformEnabled(project)
- )
-}
-
+IconGenerationTask.registerExtendedIconThemeProject(project, android)
androidx {
name = "Compose Material Icons Extended"
diff --git a/compose/material/material-icons-extended/generate.gradle b/compose/material/material-icons-extended/generate.gradle
index aed94d9..eb45afa 100644
--- a/compose/material/material-icons-extended/generate.gradle
+++ b/compose/material/material-icons-extended/generate.gradle
@@ -25,17 +25,9 @@
apply plugin: "com.android.library"
apply plugin: "AndroidXComposePlugin"
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
apply from: "${buildscript.sourceFile.parentFile}/shared-dependencies.gradle"
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- // We're not sure how to merge icons jars when multiplatform is enabled
- IconGenerationTask.registerExtendedIconThemeProject(
- project,
- android,
- AndroidXComposePlugin.isMultiplatformEnabled(project)
- )
-}
+IconGenerationTask.registerExtendedIconThemeProject(project, android)
dependencies.attributesSchema {
attribute(iconExportAttr)
diff --git a/compose/material/material-icons-extended/shared-dependencies.gradle b/compose/material/material-icons-extended/shared-dependencies.gradle
index f2cbca1..2de0ef0 100644
--- a/compose/material/material-icons-extended/shared-dependencies.gradle
+++ b/compose/material/material-icons-extended/shared-dependencies.gradle
@@ -18,36 +18,25 @@
// by its specific theme projects (each of which compile a specific theme)
import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
-dependencies {
- if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make an analogous update in the
- * corresponding block below
- */
- api(project(":compose:material:material-icons-core"))
- implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:runtime:runtime"))
- }
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
}
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
- api(project(":compose:material:material-icons-core"))
- implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:runtime:runtime"))
- }
+kotlin {
+ /*
+ * When updating dependencies, make sure to make an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ api(project(":compose:material:material-icons-core"))
+ implementation(libs.kotlinStdlibCommon)
+ implementation(project(":compose:runtime:runtime"))
}
}
}
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 31a99bb..999f59f 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,48 +23,15 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- api("androidx.compose.foundation:foundation:1.2.1")
- api("androidx.compose.runtime:runtime:1.2.1")
-
- implementation(libs.kotlinStdlibCommon)
- implementation("androidx.compose.animation:animation:1.2.1")
- implementation("androidx.compose.ui:ui-util:1.2.1")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:foundation:foundation"))
api(project(":compose:runtime:runtime"))
@@ -73,19 +39,55 @@
implementation(project(":compose:animation:animation"))
implementation(project(":compose:ui:ui-util"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ commonTest {
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:foundation:foundation"))
+ api(project(":compose:runtime:runtime"))
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:ui:ui-util"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:test-utils"))
implementation(libs.testRules)
@@ -94,6 +96,29 @@
implementation(libs.truth)
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
@@ -105,7 +130,7 @@
// Disable strict API mode for MPP builds as it will fail to compile androidAndroidTest
// sources, as it doesn't understand that they are tests and thinks they should have explicit
// visibility
- legacyDisableKotlinStrictApiMode = AndroidXComposePlugin.isMultiplatformEnabled(project)
+ legacyDisableKotlinStrictApiMode = true
}
android {
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, 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);
}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index d9fe905..a45a971 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, 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);
}
@@ -426,7 +427,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
- method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
}
@@ -531,7 +532,7 @@
}
public final class ModalBottomSheetKt {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.ModalBottomSheetState ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, androidx.compose.ui.unit.Density density, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean isSkipHalfExpanded);
method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.ModalBottomSheetState ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean isSkipHalfExpanded);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHalfExpanded);
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, 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);
}
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 82e2838..3019ae0 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
plugins {
@@ -24,70 +24,15 @@
id("AndroidXPaparazziPlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- api("androidx.compose.animation:animation-core:1.2.1")
- api(project(":compose:foundation:foundation"))
- api(project(":compose:material:material-icons-core"))
- api(project(":compose:material:material-ripple"))
- api("androidx.compose.runtime:runtime:1.2.1")
- api("androidx.compose.ui:ui:1.2.1")
- api(project(":compose:ui:ui-text"))
-
- implementation(libs.kotlinStdlibCommon)
- implementation("androidx.compose.animation:animation:1.2.1")
- implementation("androidx.compose.foundation:foundation-layout:1.2.1")
- implementation("androidx.compose.ui:ui-util:1.2.1")
-
- // TODO: remove next 3 dependencies when b/202810604 is fixed
- implementation("androidx.savedstate:savedstate:1.2.1")
- implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
- implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(project(":compose:material:material:material-samples"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.mockitoKotlin)
- androidTestImplementation(libs.testUiautomator)
-
- lintPublish project(":compose:material:material-lint")
-
- samples(project(":compose:material:material:material-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:animation:animation-core"))
api(project(":compose:foundation:foundation"))
@@ -101,8 +46,37 @@
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:ui:ui-util"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:animation:animation-core"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:ui:ui-util"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
// TODO: remove next 3 dependencies when b/202810604 is fixed
@@ -110,23 +84,27 @@
implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:material:material:material-samples"))
implementation(project(":compose:test-utils"))
implementation(project(":test:screenshot:screenshot"))
@@ -140,18 +118,39 @@
implementation(libs.mockitoKotlin)
implementation(libs.testUiautomator)
}
+ }
- desktopTest.dependencies {
- implementation(project(":compose:ui:ui-test-junit4"))
- implementation(libs.truth)
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
implementation(libs.junit)
- implementation(libs.skikoCurrentOs)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.skikoCurrentOs)
+ }
}
}
}
- dependencies {
- samples(project(":compose:material:material:material-samples"))
- }
+}
+
+dependencies {
+ lintPublish project(":compose:material:material-lint")
}
androidx {
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
index 2fe5d63..d705315 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
@@ -157,15 +157,10 @@
@JvmStatic
fun registerExtendedIconThemeProject(
project: Project,
- libraryExtension: LibraryExtension,
- isMpp: Boolean
+ libraryExtension: LibraryExtension
) {
- if (isMpp) {
- ExtendedIconGenerationTask.register(project, null)
- } else {
- libraryExtension.libraryVariants.all { variant ->
- ExtendedIconGenerationTask.register(project, variant)
- }
+ libraryExtension.libraryVariants.all { variant ->
+ ExtendedIconGenerationTask.register(project, variant)
}
// b/175401659 - disable lint as it takes a long time, and most errors should
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
index ccbd6ff..30fa679 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
@@ -50,6 +50,7 @@
import androidx.compose.material.samples.LeadingIconTabs
import androidx.compose.material.samples.LinearProgressIndicatorSample
import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.MenuWithScrollStateSample
import androidx.compose.material.samples.ModalBottomSheetSample
import androidx.compose.material.samples.ModalDrawerSample
import androidx.compose.material.samples.NavigationRailBottomAlignSample
@@ -398,6 +399,13 @@
MenuSample()
},
Example(
+ name = ::MenuWithScrollStateSample.name,
+ description = MenusExampleDescription,
+ sourceUrl = MenusExampleSourceUrl
+ ) {
+ MenuWithScrollStateSample()
+ },
+ Example(
name = ::ExposedDropdownMenuSample.name,
description = MenusExampleDescription,
sourceUrl = MenusExampleSourceUrl
@@ -703,14 +711,18 @@
description = TextFieldsExampleDescription,
sourceUrl = TextFieldsExampleSourceUrl
) {
- TextArea()
+ TextArea()
}
).map {
// By default text field samples are minimal and don't have a `width` modifier to restrict the
// width. As a result, they grow horizontally if enough text is typed. To prevent this behavior
// in Catalog app the code below restricts the width of every text field sample
it.copy(content = {
- Box(Modifier.wrapContentWidth().width(280.dp)) { it.content() }
+ Box(
+ Modifier
+ .wrapContentWidth()
+ .width(280.dp)
+ ) { it.content() }
})
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
index bf6f420..f9c6a77 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
@@ -29,6 +30,7 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -41,7 +43,9 @@
fun MenuSample() {
var expanded by remember { mutableStateOf(false) }
- Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart)) {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
}
@@ -61,4 +65,37 @@
}
}
}
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun MenuWithScrollStateSample() {
+ var expanded by remember { mutableStateOf(false) }
+ val scrollState = rememberScrollState()
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart)
+ ) {
+ IconButton(onClick = { expanded = true }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ scrollState = scrollState
+ ) {
+ repeat(30) {
+ DropdownMenuItem(onClick = { /* Handle item! */ }) {
+ Text("Item ${it + 1}")
+ }
+ }
+ }
+ LaunchedEffect(expanded) {
+ if (expanded) {
+ // Scroll to show the bottom menu items.
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 17c6cc8..23300d9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -22,7 +22,9 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -35,9 +37,11 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -336,6 +340,47 @@
// Should not have crashed.
}
+ @Test
+ fun withScrolledContent() {
+ rule.setMaterialContent {
+ Box(Modifier.fillMaxSize()) {
+ ExposedDropdownMenuBox(
+ modifier = Modifier.align(Alignment.Center),
+ expanded = true,
+ onExpandedChange = { }
+ ) {
+ val scrollState = rememberScrollState()
+ TextField(
+ value = "",
+ onValueChange = { },
+ label = { Text("Label") },
+ )
+ ExposedDropdownMenu(
+ expanded = true,
+ onDismissRequest = { },
+ scrollState = scrollState
+ ) {
+ repeat(100) {
+ Box(
+ Modifier
+ .testTag("MenuContent ${it + 1}")
+ .size(with(LocalDensity.current) { 70.toDp() })
+ )
+ }
+ }
+ LaunchedEffect(Unit) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+ rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+ }
+
@Composable
fun ExposedDropdownMenuForTest(
expanded: Boolean,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index ace0f2a..8ad3242 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -29,6 +31,8 @@
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isPopup
@@ -128,6 +132,42 @@
}
@Test
+ fun menu_scrolledContent() {
+ rule.setContent {
+ with(LocalDensity.current) {
+ Box(
+ Modifier
+ .requiredSize(20.toDp())
+ .background(color = Color.Blue)
+ ) {
+ val scrollState = rememberScrollState()
+ DropdownMenu(
+ expanded = true,
+ onDismissRequest = {},
+ scrollState = scrollState
+ ) {
+ repeat(100) {
+ Box(
+ Modifier
+ .testTag("MenuContent ${it + 1}")
+ .size(70.toDp())
+ )
+ }
+ }
+ LaunchedEffect(Unit) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+ rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+ }
+
+ @Test
fun menu_positioning_bottomEnd() {
val screenWidth = 500
val screenHeight = 1000
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 9043b15..cb2f6d2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -36,6 +36,10 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.testTag
@@ -56,11 +60,13 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -875,6 +881,7 @@
}
}
},
+ sheetGesturesEnabled = true,
content = { Box(Modifier.fillMaxSize()) }
)
}
@@ -926,6 +933,53 @@
}
@Test
+ fun modalBottomSheet_gesturesDisabled_doesNotParticipateInNestedScroll() =
+ runBlocking(AutoTestFrameClock()) {
+ lateinit var sheetState: ModalBottomSheetState
+ val sheetContentTag = "sheetContent"
+ val scrollConnection = object : NestedScrollConnection {}
+ val scrollDispatcher = NestedScrollDispatcher()
+ val sheetHeight = 300.dp
+ val sheetHeightPx = with(rule.density) { sheetHeight.toPx() }
+
+ rule.setContent {
+ sheetState = rememberModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Expanded,
+ )
+ ModalBottomSheetLayout(
+ sheetState = sheetState,
+ sheetContent = {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .requiredHeight(sheetHeight)
+ .nestedScroll(scrollConnection, scrollDispatcher)
+ .testTag(sheetContentTag),
+ )
+ },
+ sheetGesturesEnabled = false,
+ content = { Box(Modifier.fillMaxSize()) },
+ )
+ }
+
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+
+ val offsetBeforeScroll = sheetState.requireOffset()
+ scrollDispatcher.dispatchPreScroll(
+ Offset(x = 0f, y = -sheetHeightPx),
+ NestedScrollSource.Drag,
+ )
+ rule.waitForIdle()
+ assertWithMessage("Offset after scroll is equal to offset before scroll")
+ .that(sheetState.requireOffset()).isEqualTo(offsetBeforeScroll)
+
+ val highFlingVelocity = Velocity(x = 0f, y = with(rule.density) { 500.dp.toPx() })
+ scrollDispatcher.dispatchPreFling(highFlingVelocity)
+ rule.waitForIdle()
+ assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+ }
+
+ @Test
fun modalBottomSheet_anchorsChange_retainsCurrentValue() {
lateinit var state: ModalBottomSheetState
var amountOfItems by mutableStateOf(0)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
index 94c0373..e1bd14d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
@@ -17,11 +17,13 @@
package androidx.compose.material
import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.ScrollState
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.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -70,6 +72,15 @@
* tapping outside the menu's bounds
* @param offset [DpOffset] to be added to the position of the menu
*/
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "DropdownMenu(expanded,onDismissRequest, modifier, offset, " +
+ "rememberScrollState(), properties, content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
@Composable
fun DropdownMenu(
expanded: Boolean,
@@ -78,6 +89,69 @@
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ offset = offset,
+ scrollState = rememberScrollState(),
+ properties = properties,
+ content = content
+)
+
+/**
+ * <a href="https://material.io/components/menus#dropdown-menu" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * 
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded whether the menu is expanded or not
+ * @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
+ * outside the menu's bounds
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param properties [PopupProperties] for further customization of this popup's behavior
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ properties: PopupProperties = PopupProperties(focusable = true),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -100,6 +174,7 @@
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
+ scrollState = scrollState,
modifier = modifier,
content = content
)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index 4a46e56..660f1c2 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -30,6 +31,7 @@
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.internal.ExposedDropdownMenuPopup
@@ -223,6 +225,7 @@
* @param onDismissRequest Called when the user requests to dismiss the menu, such as by
* tapping outside the menu's bounds
* @param modifier The modifier to apply to this layout
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
* @param content The content of the [ExposedDropdownMenu]
*/
@Composable
@@ -230,6 +233,7 @@
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
+ scrollState: ScrollState = rememberScrollState(),
content: @Composable ColumnScope.() -> Unit
) {
// TODO(b/202810604): use DropdownMenu when PopupProperties constructor is stable
@@ -261,6 +265,7 @@
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
+ scrollState = scrollState,
modifier = modifier.exposedDropdownSize(),
content = content
)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 3e80837..9d3dadbd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -21,8 +21,9 @@
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
@@ -33,14 +34,13 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.getValue
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -57,11 +57,11 @@
import kotlin.math.max
import kotlin.math.min
-@Suppress("ModifierParameter")
@Composable
internal fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
transformOriginState: MutableState<TransformOrigin>,
+ scrollState: ScrollState,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
@@ -126,7 +126,7 @@
modifier = modifier
.padding(vertical = DropdownMenuVerticalPadding)
.width(IntrinsicSize.Max)
- .verticalScroll(rememberScrollState()),
+ .verticalScroll(scrollState),
content = content
)
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 9d1da3f..8b9eef6 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -528,6 +528,7 @@
* @param sheetContent The content of the bottom sheet.
* @param modifier Optional [Modifier] for the entire component.
* @param sheetState The state of the bottom sheet.
+ * @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures.
* @param sheetShape The shape of the bottom sheet.
* @param sheetElevation The elevation of the bottom sheet.
* @param sheetBackgroundColor The background color of the bottom sheet.
@@ -547,6 +548,7 @@
modifier: Modifier = Modifier,
sheetState: ModalBottomSheetState =
rememberModalBottomSheetState(Hidden),
+ sheetGesturesEnabled: Boolean = true,
sheetShape: Shape = MaterialTheme.shapes.large,
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
@@ -585,13 +587,17 @@
.align(Alignment.TopCenter) // We offset from the top so we'll center from there
.widthIn(max = MaxModalBottomSheetWidth)
.fillMaxWidth()
- .nestedScroll(
- remember(sheetState.anchoredDraggableState, orientation) {
- ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
- state = sheetState.anchoredDraggableState,
- orientation = orientation
+ .then(
+ if (sheetGesturesEnabled) {
+ Modifier.nestedScroll(
+ remember(sheetState.anchoredDraggableState, orientation) {
+ ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+ state = sheetState.anchoredDraggableState,
+ orientation = orientation
+ )
+ }
)
- }
+ } else Modifier
)
.offset {
IntOffset(
@@ -604,7 +610,8 @@
.anchoredDraggable(
state = sheetState.anchoredDraggableState,
orientation = orientation,
- enabled = sheetState.anchoredDraggableState.currentValue != Hidden,
+ enabled = sheetGesturesEnabled &&
+ sheetState.anchoredDraggableState.currentValue != Hidden,
)
.onSizeChanged { sheetSize ->
val anchors = buildMap {
@@ -619,37 +626,45 @@
}
sheetState.anchoredDraggableState.updateAnchors(anchors, anchorChangeCallback)
}
- .semantics {
- if (sheetState.isVisible) {
- dismiss {
- if (sheetState.anchoredDraggableState.confirmValueChange(Hidden)) {
- scope.launch { sheetState.hide() }
- }
- true
- }
- if (sheetState.anchoredDraggableState.currentValue == HalfExpanded) {
- expand {
- if (sheetState.anchoredDraggableState.confirmValueChange(
- Expanded
- )
- ) {
- scope.launch { sheetState.expand() }
+ .then(
+ if (sheetGesturesEnabled) {
+ Modifier.semantics {
+ if (sheetState.isVisible) {
+ dismiss {
+ if (
+ sheetState.anchoredDraggableState.confirmValueChange(Hidden)
+ ) {
+ scope.launch { sheetState.hide() }
+ }
+ true
}
- true
- }
- } else if (sheetState.hasHalfExpandedState) {
- collapse {
- if (sheetState.anchoredDraggableState.confirmValueChange(
- HalfExpanded
- )
+ if (sheetState.anchoredDraggableState.currentValue
+ == HalfExpanded
) {
- scope.launch { sheetState.halfExpand() }
+ expand {
+ if (sheetState.anchoredDraggableState.confirmValueChange(
+ Expanded
+ )
+ ) {
+ scope.launch { sheetState.expand() }
+ }
+ true
+ }
+ } else if (sheetState.hasHalfExpandedState) {
+ collapse {
+ if (sheetState.anchoredDraggableState.confirmValueChange(
+ HalfExpanded
+ )
+ ) {
+ scope.launch { sheetState.halfExpand() }
+ }
+ true
+ }
}
- true
}
}
- }
- },
+ } else Modifier
+ ),
shape = sheetShape,
elevation = sheetElevation,
color = sheetBackgroundColor,
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
index 4da69f2..ea91b6c 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateOf
@@ -74,7 +76,15 @@
* @param offset [DpOffset] to be added to the position of the menu
* @param content content lambda
*/
-@Suppress("ModifierParameter")
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "DropdownMenu(expanded,onDismissRequest, focusable, modifier, offset, " +
+ "rememberScrollState(), content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
@Composable
fun DropdownMenu(
expanded: Boolean,
@@ -83,6 +93,64 @@
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ focusable = focusable,
+ modifier = modifier,
+ offset = offset,
+ scrollState = rememberScrollState(),
+ content = content
+)
+
+/**
+ * A Material Design [dropdown menu](https://material.io/components/menus#dropdown-menu).
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ focusable: Boolean = true,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -110,6 +178,7 @@
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
+ scrollState = scrollState,
content = content
)
}
@@ -163,8 +232,19 @@
* @param expanded Whether the menu is currently open and visible to the user
* @param onDismissRequest Called when the user requests to dismiss the menu, such as by
* tapping outside the menu's bounds
+ * @param focusable Sets the ability for the menu to capture focus
+ * @param modifier The modifier for this layout.
+ * @param content The content lambda.
*/
-@Suppress("ModifierParameter")
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "CursorDropdownMenu(expanded,onDismissRequest, focusable, modifier, " +
+ "rememberScrollState(), content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a CursorDropdownMenu function with a ScrollState parameter"
+)
@Composable
fun CursorDropdownMenu(
expanded: Boolean,
@@ -172,6 +252,40 @@
focusable: Boolean = true,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
+) = CursorDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ focusable = focusable,
+ modifier = modifier,
+ scrollState = rememberScrollState(),
+ content = content
+)
+
+/**
+ *
+ * A [CursorDropdownMenu] behaves similarly to [Popup] and will use the current position of the mouse
+ * cursor to position itself on screen.
+ *
+ * The [content] of a [CursorDropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus.
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun CursorDropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ focusable: Boolean = true,
+ modifier: Modifier = Modifier,
+ scrollState: ScrollState = rememberScrollState(),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -188,6 +302,7 @@
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
+ scrollState = scrollState,
content = content
)
}
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index c263c79..b1aaced 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -17,7 +17,7 @@
import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.KmpPlatformsKt
plugins {
id("AndroidXPlugin")
@@ -25,49 +25,61 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
-
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the analogous update in the
- * corresponding block below
- */
- implementation(libs.kotlinStdlibCommon)
-
- api("androidx.annotation:annotation:1.1.0")
-
- api(project(":compose:foundation:foundation"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:foundation:foundation"))
}
+ }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
+ commonTest {
+ dependencies {
}
+ }
- desktopMain.dependencies {
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
implementation(libs.kotlinStdlib)
}
}
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ }
}
}
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 9eb3383..495dd73 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -14,10 +14,10 @@
* limitations under the License.
*/
+import androidx.build.KmpPlatformsKt
import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,69 +25,73 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- implementation(libs.kotlinStdlibCommon)
- api("androidx.compose.runtime:runtime:1.2.1")
- api("androidx.compose.ui:ui:1.2.1")
- api("androidx.compose.ui:ui-unit:1.2.1")
- implementation("androidx.window:window:1.0.0")
-
- testImplementation(libs.kotlinTest)
- testImplementation(libs.truth)
-
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
-
- samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- kotlin {
- android()
- jvm("desktop")
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
api(project(":compose:ui:ui-unit"))
}
+ }
- jvmMain.dependencies {
+ commonTest {
+ dependencies {
+
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
implementation(libs.kotlinStdlib)
}
+ }
- androidMain.dependencies {
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ // Because dependencies are pinned in the android/common code.
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-unit"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
implementation("androidx.window:window:1.0.0")
}
+ }
- androidMain.dependsOn(jvmMain)
- desktopMain.dependsOn(jvmMain)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
- androidTest.dependencies {
- implementation(libs.kotlinTest)
- implementation(libs.truth)
+ }
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:test-utils"))
implementation(project(":compose:foundation:foundation"))
implementation(libs.testRules)
@@ -96,9 +100,24 @@
implementation(libs.truth)
}
}
- }
- dependencies {
- samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.kotlinTest)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+
+ }
+ }
+ }
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
index 7374b08..1f62b814 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
@@ -31,6 +31,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Assume.assumeFalse
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,6 +65,8 @@
@Test
fun darkTheme() {
+ assumeFalse("See b/272301182", Build.VERSION.SDK_INT == 33)
+
composeTestRule.setMaterialContent(darkColorScheme()) {
Column(Modifier.testTag(Tag)) {
Spacer(Modifier.size(10.dp))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
index 0df58c9..26f91f1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -146,7 +146,6 @@
}
}
- @OptIn(ExperimentalMaterial3Api::class)
@Test
fun menu_scrolledContent() {
rule.setContent {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 3693245..3c05d96 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
@@ -33,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -112,6 +114,25 @@
}
@Test
+ fun lightTheme_customHeight() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource, Modifier.height(64.dp))
+ }
+
+ assertNavigationBarMatches(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "navigationBar_lightTheme_customHeight"
+ )
+ }
+
+ @Test
fun darkTheme_defaultColors() {
val interactionSource = MutableInteractionSource()
@@ -211,14 +232,16 @@
*
* @param interactionSource the [MutableInteractionSource] for the first [NavigationBarItem], to
* control its visual state.
+ * @param modifier the [Modifier] applied to the navigation bar
* @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
*/
@Composable
private fun DefaultNavigationBar(
interactionSource: MutableInteractionSource,
+ modifier: Modifier = Modifier,
setUnselectedItemsAsDisabled: Boolean = false,
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Filled.Favorite, null) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index e9a0168..2f6c21f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.tokens.NavigationBarTokens
@@ -266,42 +267,43 @@
@Test
fun navigationBarItemContent_withLabel_sizeAndPosition() {
rule.setMaterialContent(lightColorScheme()) {
- Box {
- NavigationBar {
- NavigationBarItem(
- modifier = Modifier.testTag("item"),
- icon = {
- Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
- },
- label = {
- Text("ItemText")
- },
- selected = true,
- onClick = {}
- )
- }
+ NavigationBar {
+ NavigationBarItem(
+ modifier = Modifier.testTag("item"),
+ icon = {
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+ },
+ label = {
+ Text("ItemText")
+ },
+ selected = true,
+ onClick = {}
+ )
}
}
val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
.getUnclippedBoundsInRoot()
- val textBounds = rule.onNodeWithText("ItemText", useUnmergedTree = true)
- .getUnclippedBoundsInRoot()
- // Distance from the bottom of the item to the text bottom, and from the top of the icon to
- // the top of the item
- val verticalPadding = NavigationBarItemVerticalPadding
-
- val itemBottom = itemBounds.height + itemBounds.top
- // Text bottom should be `verticalPadding` from the bottom of the item
- textBounds.bottom.assertIsEqualTo(itemBottom - verticalPadding)
+ // Distance from the top of the item to the top of the icon for the default height
+ val verticalPadding = 16.dp
rule.onNodeWithTag("icon", useUnmergedTree = true)
- // The icon should be centered in the item
+ // The icon should be horizontally centered in the item
.assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
// The top of the icon is `verticalPadding` below the top of the item
.assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+ val iconBottom = iconBounds.top + iconBounds.height
+ // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+ // bottom of the icon
+ rule.onNodeWithText("ItemText", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+ .top
+ .assertIsEqualTo(
+ iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+ )
}
@Test
@@ -367,6 +369,49 @@
}
@Test
+ fun navigationBarItemContent_customHeight_withLabel_sizeAndPosition() {
+ val defaultHeight = NavigationBarTokens.ContainerHeight
+ val customHeight = 64.dp
+
+ rule.setMaterialContent(lightColorScheme()) {
+ NavigationBar(Modifier.height(customHeight)) {
+ NavigationBarItem(
+ modifier = Modifier.testTag("item"),
+ icon = {
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+ },
+ label = { Text("Label") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // Vertical padding is removed symmetrically from top and bottom for smaller heights
+ val verticalPadding = 16.dp - (defaultHeight - customHeight) / 2
+
+ val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ // The icon should be horizontally centered in the item
+ .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
+ // The top of the icon is `verticalPadding` below the top of the item
+ .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+ val iconBottom = iconBounds.top + iconBounds.height
+ // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+ // bottom of the item
+ rule.onNodeWithText("Label", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+ .top
+ .assertIsEqualTo(
+ iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+ )
+ }
+
+ @Test
fun navigationBar_selectNewItem() {
rule.setMaterialContent(lightColorScheme()) {
var selectedItem by remember { mutableStateOf(0) }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
index 0acd46a..5948252 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -75,7 +75,6 @@
* @param content the content of this dropdown menu, typically a [DropdownMenuItem]
*/
@OptIn(ExperimentalMaterial3Api::class)
-@Suppress("ModifierParameter")
@Deprecated(
level = DeprecationLevel.HIDDEN,
replaceWith = ReplaceWith(
@@ -147,7 +146,6 @@
* @param properties [PopupProperties] for further customization of this popup's behavior
* @param content the content of this dropdown menu, typically a [DropdownMenuItem]
*/
-@Suppress("ModifierParameter")
@Composable
fun DropdownMenu(
expanded: Boolean,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 318985d..4b558c6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -61,7 +61,6 @@
import kotlin.math.max
import kotlin.math.min
-@Suppress("ModifierParameter")
@Composable
internal fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 2746436..5f1bf664 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -535,8 +535,8 @@
* [animationProgress].
*
* When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
- * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
- * the spec.
+ * near the top of the item and the [labelPlaceable] will be placed beneath it with padding,
+ * according to the spec.
*
* When [animationProgress] is 1 (representing the selected state), the positions will be the same
* as above.
@@ -573,11 +573,13 @@
): MeasureResult {
val height = constraints.maxHeight
- // Label should be `ItemVerticalPadding` from the bottom
- val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
+ val contentTotalHeight = iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+ NavigationBarIndicatorToLabelPadding.roundToPx() + labelPlaceable.height
+ val contentVerticalPadding = ((height - contentTotalHeight) / 2)
+ .coerceAtLeast(IndicatorVerticalPadding.roundToPx())
- // Icon (when selected) should be `ItemVerticalPadding` from the top
- val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
+ // Icon (when selected) should be `contentVerticalPadding` from top
+ val selectedIconY = contentVerticalPadding
val unselectedIconY =
if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
@@ -588,6 +590,10 @@
// animationProgress.
val offset = (iconDistance * (1 - animationProgress)).roundToInt()
+ // Label should be fixed padding below icon
+ val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+ NavigationBarIndicatorToLabelPadding.roundToPx()
+
val containerWidth = constraints.maxWidth
val labelX = (containerWidth - labelPlaceable.width) / 2
@@ -626,12 +632,13 @@
internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
/*@VisibleForTesting*/
-internal val NavigationBarItemVerticalPadding: Dp = 16.dp
+internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp
private val IndicatorHorizontalPadding: Dp =
(NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
-private val IndicatorVerticalPadding: Dp =
+/*@VisibleForTesting*/
+internal val IndicatorVerticalPadding: Dp =
(NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
private val IndicatorVerticalOffset: Dp = 12.dp
\ No newline at end of file
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt
new file mode 100644
index 0000000..70fd1cd
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt
@@ -0,0 +1,253 @@
+/*
+ * 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:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import androidx.compose.lint.Names
+import androidx.compose.lint.isInPackageName
+import androidx.compose.lint.isVoidOrUnit
+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.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.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiParameter
+import java.util.EnumSet
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UExpressionList
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.UVariable
+import org.jetbrains.uast.skipParenthesizedExprDown
+import org.jetbrains.uast.toUElement
+
+/**
+ * Detector to warn when [Unit] is being passed "opaquely" as an argument to any of the methods in
+ * [getApplicableMethodNames]. An argument is defined as an opaque unit key if all the following
+ * are true:
+ * - The argument is an expression of type `Unit`
+ * - The argument is being passed to a parameter of type `Any?`
+ * - The argument is not the `Unit` literal
+ * - The argument is not a trivial variable or property read expression
+ */
+class OpaqueUnitKeyDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> = listOf(
+ Names.Runtime.Remember.shortName,
+ Names.Runtime.RememberSaveable.shortName,
+ Names.Runtime.DisposableEffect.shortName,
+ Names.Runtime.LaunchedEffect.shortName,
+ Names.Runtime.ProduceState.shortName,
+ Names.Runtime.ReusableContent.shortName,
+ Names.Runtime.Key.shortName,
+ )
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!method.isInPackageName(Names.Runtime.PackageName)) return
+
+ method.parameterList.parameters.forEach { parameter ->
+ val arg = node.getArgumentForParameter(parameter.parameterIndex())
+ if (parameter.isNullableAny()) {
+ if (arg?.isOpaqueUnitExpression() == true) {
+ reportOpaqueUnitArgKey(
+ context = context,
+ method = method,
+ methodInvocation = node,
+ parameter = parameter,
+ argument = arg
+ )
+ }
+ } else if (parameter.isPotentiallyVarArgs() && arg is UExpressionList) {
+ arg.expressions.forEach { varArg ->
+ if (varArg.isOpaqueUnitExpression()) {
+ reportOpaqueUnitArgKey(
+ context = context,
+ method = method,
+ methodInvocation = node,
+ parameter = parameter,
+ argument = varArg
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun reportOpaqueUnitArgKey(
+ context: JavaContext,
+ method: PsiMethod,
+ methodInvocation: UCallExpression,
+ parameter: PsiParameter,
+ argument: UExpression
+ ) {
+ val rootExpression = methodInvocation.resolveRootExpression()
+ val rootExpressionLocation = context.getLocation(rootExpression)
+
+ context.report(
+ OpaqueUnitKey,
+ argument,
+ context.getLocation(argument),
+ "Implicitly passing `Unit` as argument to ${parameter.name}",
+ fix()
+ .name(
+ "Move expression outside of `${method.name}`'s arguments " +
+ "and pass `Unit` explicitly"
+ )
+ .composite(
+ if (rootExpression.isInPhysicalBlock()) {
+ // If we're in a block where we can add an expression without breaking any
+ // syntax rules, promote the argument's expression to a sibling.
+ fix()
+ .replace()
+ .range(rootExpressionLocation)
+ .beginning()
+ .with("${argument.asSourceString()}\n")
+ .reformat(true)
+ .build()
+ } else {
+ // If we're not in a block, then introduce one for cheap by wrapping the
+ // call with Kotlin's `run` function to a format that appears as follows:
+ //
+ // ```
+ // run {
+ // theArgument()
+ // theMethod(...)
+ // }
+ // ```
+ fix()
+ .composite(
+ fix()
+ .replace()
+ .range(rootExpressionLocation)
+ .beginning()
+ .with("kotlin.run {\n${argument.asSourceString()}\n")
+ .reformat(true)
+ .shortenNames()
+ .build(),
+ fix()
+ .replace()
+ .range(rootExpressionLocation)
+ .end()
+ .with("\n}")
+ .reformat(true)
+ .build()
+ )
+ },
+
+ // Replace the old parameter with the Unit literal
+ fix()
+ .replace()
+ .range(context.getLocation(argument))
+ .with(FqUnitName)
+ .shortenNames()
+ .build(),
+ )
+ )
+ }
+
+ private fun UCallExpression.resolveRootExpression(): UExpression {
+ var root: UExpression = this
+ var parent: UExpression? = root.getParentExpression()
+ while (parent != null && parent !is UBlockExpression) {
+ if (!parent.isVirtual) { root = parent }
+ parent = parent.getParentExpression()
+ }
+ return root
+ }
+
+ private fun UExpression.isInPhysicalBlock(): Boolean {
+ var parent: UElement? = this
+ while (parent != null) {
+ if (parent is UBlockExpression) {
+ return !parent.isVirtual
+ }
+ parent = parent.uastParent
+ }
+ return false
+ }
+
+ private val UElement.isVirtual get() = sourcePsi == null
+
+ private fun UExpression.getParentExpression(): UExpression? {
+ return when (val parent = uastParent) {
+ is UVariable -> parent.uastParent as UDeclarationsExpression
+ is UExpression -> parent
+ else -> null
+ }
+ }
+
+ private fun PsiParameter.isNullableAny(): Boolean {
+ val element = toUElement() as UParameter
+ return element.type.canonicalText == FqJavaObjectName &&
+ element.getAnnotations().any { it.qualifiedName == FqKotlinNullableAnnotation }
+ }
+
+ private fun PsiParameter.isPotentiallyVarArgs(): Boolean {
+ return type.canonicalText == "$FqJavaObjectName[]"
+ }
+
+ private fun UExpression.isOpaqueUnitExpression(): Boolean {
+ return getExpressionType().isVoidOrUnit && !isUnitLiteral()
+ }
+
+ private fun UExpression.isUnitLiteral(): Boolean {
+ val expr = skipParenthesizedExprDown() ?: this
+ if (expr !is USimpleNameReferenceExpression) return false
+
+ return (expr.tryResolveUDeclaration() as? UClass)?.qualifiedName == FqUnitName
+ }
+
+ companion object {
+ private const val FqJavaObjectName = "java.lang.Object"
+ private const val FqUnitName = "kotlin.Unit"
+ private const val FqKotlinNullableAnnotation = "org.jetbrains.annotations.Nullable"
+
+ val OpaqueUnitKey = Issue.create(
+ "OpaqueUnitKey",
+ "Passing an expression which always returns `Unit` as a key argument",
+ "Certain Compose functions including `remember`, `LaunchedEffect`, and " +
+ "`DisposableEffect` declare (and sometimes require) one or more key parameters. " +
+ "When a key parameter changes, it is a signal that the previous invocation is " +
+ "now invalid. In certain cases, it may be required to pass `Unit` as a key to " +
+ "one of these functions, indicating that the invocation never becomes invalid. " +
+ "Using `Unit` as a key should be done infrequently, and should always be done " +
+ "explicitly by passing the `Unit` literal. This inspection checks for " +
+ "invocations where `Unit` is being passed as a key argument in any form other " +
+ "than the `Unit` literal. This is usually done by mistake, and can harm " +
+ "readability. If a Unit expression is being passed as a key, it is always " +
+ "equivalent to move the expression before the function invocation and pass the " +
+ "`Unit` literal instead.",
+ Category.CORRECTNESS, 3, Severity.WARNING,
+ Implementation(
+ OpaqueUnitKeyDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index bc749d0..8a2444e 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -41,6 +41,7 @@
MutableCollectionMutableStateDetector.MutableCollectionMutableState,
ProduceStateDetector.ProduceStateDoesNotAssignValue,
RememberDetector.RememberReturnType,
+ OpaqueUnitKeyDetector.OpaqueUnitKey,
UnrememberedStateDetector.UnrememberedState
)
override val vendor = Vendor(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt
new file mode 100644
index 0000000..597189a
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt
@@ -0,0 +1,1744 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.lint
+
+import androidx.compose.lint.test.Stubs
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class OpaqueUnitKeyDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = OpaqueUnitKeyDetector()
+
+ override fun getIssues() = listOf(OpaqueUnitKeyDetector.OpaqueUnitKey)
+
+ // region remember test cases
+
+ @Test
+ fun remember_withUnitLiteralKey_doesNotError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x = remember(Unit) { listOf(1, 2, 3) }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun remember_withUnitPropertyRead_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ val unitProperty = Unit
+
+ @Composable
+ fun Test() {
+ val x = remember(unitProperty) { listOf(1, 2, 3) }
+ }
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x = remember(unitProperty) { listOf(1, 2, 3) }
+ ~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+- val x = remember(unitProperty) { listOf(1, 2, 3) }
++ unitProperty
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+ """
+ )
+ }
+
+ @Test
+ fun remember_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x = remember(produceUnit()) { listOf(1, 2, 3) }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x = remember(produceUnit()) { listOf(1, 2, 3) }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x = remember(produceUnit()) { listOf(1, 2, 3) }
++ produceUnit()
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+ """
+ )
+ }
+
+ @Test
+ fun remember_withUnitComposableInvocation_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
+ }
+
+ @Composable
+ fun AnotherComposable() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
+ ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
++ AnotherComposable()
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+ """
+ )
+ }
+
+ @Test
+ fun remember_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 7: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+- fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
++ fun test() = kotlin.run {
++ produceUnit()
++ remember(kotlin.Unit) { listOf(1, 2, 3) }
++ }
+ """
+ )
+ }
+
+ @Test
+ fun remember_withIfStatementThatReturnsUnit_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ val x = remember(
+ if (condition) {
+ doSomething()
+ } else {
+ doSomethingElse()
+ }
+ ) { listOf(1, 2, 3) }
+ }
+
+ fun doSomething() {}
+ fun doSomethingElse() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ if (condition) {
+ ^
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x = remember(
+- if (condition) {
+- doSomething()
+- } else {
+- doSomethingElse()
+- }
++ if (condition) {
++ doSomething()
++ }else {
++ doSomethingElse()
++ }
++ val x = remember(
++ kotlin.Unit
+ """
+ )
+ }
+
+ @Test
+ fun remember_withIfStatementCoercedToAny_doesNotReportError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ val x = remember(
+ if (condition) {
+ doSomething()
+ } else {
+ 42
+ }
+ ) { listOf(1, 2, 3) }
+ }
+
+ fun doSomething() {}
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun remember_twoKeys_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+ val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
++ produceUnit()
++ val x = remember(42, kotlin.Unit) { listOf(1, 2, 3) }
+ """
+ )
+ }
+
+ // endregion remember test cases
+
+ // region produceState test cases
+
+ @Test
+ fun produceState_withUnitLiteralKey_doesNotError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x by produceState("123", Unit) { /* Do nothing. */ }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun produceState_withUnitPropertyRead_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ val unitProperty = Unit
+
+ @Composable
+ fun Test() {
+ val x by produceState("123", unitProperty) { /* Do nothing. */ }
+ }
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x by produceState("123", unitProperty) { /* Do nothing. */ }
+ ~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+- val x by produceState("123", unitProperty) { /* Do nothing. */ }
++ unitProperty
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+ """
+ )
+ }
+
+ @Test
+ fun produceState_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x by produceState("123", produceUnit()) { /* Do nothing. */ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x by produceState("123", produceUnit()) { /* Do nothing. */ }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x by produceState("123", produceUnit()) { /* Do nothing. */ }
++ produceUnit()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+ """
+ )
+ }
+
+ @Test
+ fun produceState_withUnitComposableInvocation_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
+ }
+
+ @Composable
+ fun AnotherComposable() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
+ ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
++ AnotherComposable()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+ """
+ )
+ }
+
+ @Test
+ fun produceState_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 7: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+- fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
++ fun test() = kotlin.run {
++ produceUnit()
++ produceState("123", kotlin.Unit) { /* Do nothing. */ }
++ }
+ """
+ )
+ }
+
+ @Test
+ fun produceState_withIfStatementThatReturnsUnit_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ val x by produceState(
+ initialValue = "123",
+ if (condition) {
+ doSomething()
+ } else {
+ doSomethingElse()
+ }
+ ) { /* Do nothing. */ }
+ }
+
+ fun doSomething() {}
+ fun doSomethingElse() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ if (condition) {
+ ^
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x by produceState(
++ if (condition) {
++ doSomething()
++ }else {
++ doSomethingElse()
++ }
++ val x by produceState(
+- if (condition) {
+- doSomething()
+- } else {
+- doSomethingElse()
+- }
++ kotlin.Unit
+ """
+ )
+ }
+
+ @Test
+ fun produceState_withIfStatementCoercedToAny_doesNotReportError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ val x by produceState(
+ initialValue = "123",
+ if (condition) {
+ doSomething()
+ } else {
+ 42
+ }
+ ) { /* Do nothing */ }
+ }
+
+ fun doSomething() {}
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun produceState_twoKeys_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.SnapshotState,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ val x by produceState("123", produceUnit()) { /* Do nothing */ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ val x by produceState("123", produceUnit()) { /* Do nothing */ }
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- val x by produceState("123", produceUnit()) { /* Do nothing */ }
++ produceUnit()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing */ }
+ """
+ )
+ }
+
+ // endregion produceState test cases
+
+ // region DisposableEffect test cases
+
+ @Test
+ fun disposableEffect_withUnitLiteralKey_doesNotError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ DisposableEffect(Unit) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun disposableEffect_withUnitPropertyRead_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ val unitProperty = Unit
+
+ @Composable
+ fun Test() {
+ DisposableEffect(unitProperty) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ DisposableEffect(unitProperty) {
+ ~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+- DisposableEffect(unitProperty) {
++ unitProperty
++ DisposableEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun disposableEffect_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ DisposableEffect(produceUnit()) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ DisposableEffect(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- DisposableEffect(produceUnit()) {
++ produceUnit()
++ DisposableEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun disposableEffect_withUnitComposableInvocation_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ DisposableEffect(AnotherComposable()) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+
+ @Composable
+ fun AnotherComposable() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ DisposableEffect(AnotherComposable()) {
+ ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- DisposableEffect(AnotherComposable()) {
++ AnotherComposable()
++ DisposableEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun disposableEffect_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun test() = DisposableEffect(produceUnit()) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ fun test() = DisposableEffect(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 7: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+- fun test() = DisposableEffect(produceUnit()) {
++ fun test() = kotlin.run {
++ produceUnit()
++ DisposableEffect(kotlin.Unit) {
+@@ -12 +14
++ }
+ """
+ )
+ }
+
+ @Test
+ fun disposableEffect_withIfStatementThatReturnsUnit_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ DisposableEffect(
+ if (condition) {
+ doSomething()
+ } else {
+ doSomethingElse()
+ }
+ ) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+
+ fun doSomething() {}
+ fun doSomethingElse() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ if (condition) {
+ ^
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 9: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- DisposableEffect(
+- if (condition) {
+- doSomething()
+- } else {
+- doSomethingElse()
+- }
++ if (condition) {
++ doSomething()
++ }else {
++ doSomethingElse()
++ }
++ DisposableEffect(
++ kotlin.Unit
+ """
+ )
+ }
+
+ @Test
+ fun disposableEffect_withIfStatementCoercedToAny_doesNotReportError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ DisposableEffect(
+ if (condition) {
+ doSomething()
+ } else {
+ 42
+ }
+ ) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+
+ fun doSomething() {}
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun disposableEffect_twoKeys_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ DisposableEffect(42, produceUnit()) {
+ onDispose {
+ // Do nothing.
+ }
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+ DisposableEffect(42, produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- DisposableEffect(42, produceUnit()) {
++ produceUnit()
++ DisposableEffect(42, kotlin.Unit) {
+ """
+ )
+ }
+
+ // endregion DisposableEffect test cases
+
+ // region LaunchedEffect test cases
+
+ @Test
+ fun launchedEffect_withUnitLiteralKey_doesNotError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ LaunchedEffect(Unit) {
+ // Do nothing.
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun launchedEffect_withUnitPropertyRead_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ val unitProperty = Unit
+
+ @Composable
+ fun Test() {
+ LaunchedEffect(unitProperty) {
+ // Do nothing.
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ LaunchedEffect(unitProperty) {
+ ~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+- LaunchedEffect(unitProperty) {
++ unitProperty
++ LaunchedEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun launchedEffect_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ LaunchedEffect(produceUnit()) {
+ // Do nothing.
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ LaunchedEffect(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- LaunchedEffect(produceUnit()) {
++ produceUnit()
++ LaunchedEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun launchedEffect_withUnitComposableInvocation_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ LaunchedEffect(AnotherComposable()) {
+ // Do nothing.
+ }
+ }
+
+ @Composable
+ fun AnotherComposable() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ LaunchedEffect(AnotherComposable()) {
+ ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- LaunchedEffect(AnotherComposable()) {
++ AnotherComposable()
++ LaunchedEffect(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun launchedEffect_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun test() = LaunchedEffect(produceUnit()) {
+ // Do nothing.
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ fun test() = LaunchedEffect(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 7: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+- fun test() = LaunchedEffect(produceUnit()) {
++ fun test() = kotlin.run {
++ produceUnit()
++ LaunchedEffect(kotlin.Unit) {
+@@ -10 +12
++ }
+ """
+ )
+ }
+
+ @Test
+ fun launchedEffect_withIfStatementThatReturnsUnit_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ LaunchedEffect(
+ if (condition) {
+ doSomething()
+ } else {
+ doSomethingElse()
+ }
+ ) {
+ // Do nothing.
+ }
+ }
+
+ fun doSomething() {}
+ fun doSomethingElse() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+ if (condition) {
+ ^
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 9: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- LaunchedEffect(
+- if (condition) {
+- doSomething()
+- } else {
+- doSomethingElse()
+- }
++ if (condition) {
++ doSomething()
++ }else {
++ doSomethingElse()
++ }
++ LaunchedEffect(
++ kotlin.Unit
+ """
+ )
+ }
+
+ @Test
+ fun launchedEffect_withIfStatementCoercedToAny_doesNotReportError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ LaunchedEffect(
+ if (condition) {
+ doSomething()
+ } else {
+ 42
+ }
+ ) {
+ // Do nothing.
+ }
+ }
+
+ fun doSomething() {}
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun launchedEffect_twoKeys_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Effects,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ LaunchedEffect(42, produceUnit()) {
+ // Do nothing.
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+ LaunchedEffect(42, produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- LaunchedEffect(42, produceUnit()) {
++ produceUnit()
++ LaunchedEffect(42, kotlin.Unit) {
+ """
+ )
+ }
+
+ // endregion LaunchedEffect test cases
+
+ // region key() test cases
+
+ @Test
+ fun key_withUnitLiteralKey_doesNotError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ key(Unit) {
+ // Do nothing.
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun key_withUnitPropertyRead_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ val unitProperty = Unit
+
+ @Composable
+ fun Test() {
+ key(unitProperty) {
+ // Do nothing.
+ }
+ }
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ key(unitProperty) {
+ ~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 10: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+- key(unitProperty) {
++ unitProperty
++ key(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun key_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ key(produceUnit()) {
+ // Do nothing.
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ key(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- key(produceUnit()) {
++ produceUnit()
++ key(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun key_withUnitComposableInvocation_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ key(AnotherComposable()) {
+ // Do nothing.
+ }
+ }
+
+ @Composable
+ fun AnotherComposable() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ key(AnotherComposable()) {
+ ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- key(AnotherComposable()) {
++ AnotherComposable()
++ key(kotlin.Unit) {
+ """
+ )
+ }
+
+ @Test
+ fun key_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun test() = key(produceUnit()) {
+ // Do nothing.
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ fun test() = key(produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 7: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+- fun test() = key(produceUnit()) {
++ fun test() = kotlin.run {
++ produceUnit()
++ key(kotlin.Unit) {
+@@ -10 +12
++ }
+ """
+ )
+ }
+
+ @Test
+ fun key_withIfStatementThatReturnsUnit_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ key(
+ if (condition) {
+ doSomething()
+ } else {
+ doSomethingElse()
+ }
+ ) {
+ // Do nothing.
+ }
+ }
+
+ fun doSomething() {}
+ fun doSomethingElse() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ if (condition) {
+ ^
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 9: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- key(
+- if (condition) {
+- doSomething()
+- } else {
+- doSomethingElse()
+- }
++ if (condition) {
++ doSomething()
++ }else {
++ doSomethingElse()
++ }
++ key(
++ kotlin.Unit
+ """
+ )
+ }
+
+ @Test
+ fun key_withIfStatementCoercedToAny_doesNotReportError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test(condition: Boolean) {
+ key(
+ if (condition) {
+ doSomething()
+ } else {
+ 42
+ }
+ ) {
+ // Do nothing.
+ }
+ }
+
+ fun doSomething() {}
+ """
+ )
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun key_twoKeys_withUnitFunctionCall_reportsError() {
+ lint()
+ .files(
+ Stubs.Remember,
+ Stubs.Composable,
+ Stubs.Composables,
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun Test() {
+ key(42, produceUnit()) {
+ // Do nothing.
+ }
+ }
+
+ fun produceUnit() {}
+ """
+ )
+ )
+ .run()
+ .expect(
+ """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+ key(42, produceUnit()) {
+ ~~~~~~~~~~~~~
+0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+- key(42, produceUnit()) {
++ produceUnit()
++ key(42, kotlin.Unit) {
+ """
+ )
+ }
+
+ // endregion key() test cases
+}
+/* ktlint-enable max-line-length */
\ No newline at end of file
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index b4d8073..d23376d 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -15,9 +15,8 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,77 +24,54 @@
id("com.android.library")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /* When updating dependencies, make sure to make the an an analogous update in the
- corresponding block below */
- api project(":compose:runtime:runtime")
- api "androidx.annotation:annotation:1.1.0"
-
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.testCore)
- testImplementation(libs.testRules)
-
- androidTestImplementation projectOrArtifact(':compose:ui:ui')
- androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
- androidTestImplementation projectOrArtifact(":compose:test-utils")
- androidTestImplementation "androidx.fragment:fragment:1.3.0"
- androidTestImplementation projectOrArtifact(":activity:activity-compose")
- androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
-
- lintPublish(project(":compose:runtime:runtime-saveable-lint"))
-
- samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /* When updating dependencies, make sure to make the an an analogous update in the
- corresponding block above */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api project(":compose:runtime:runtime")
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
implementation(libs.kotlinStdlib)
api "androidx.annotation:annotation:1.1.0"
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation project(':compose:ui:ui')
implementation project(":compose:ui:ui-test-junit4")
implementation project(":compose:test-utils")
@@ -112,10 +88,32 @@
implementation(libs.mockitoCore)
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
- dependencies {
- samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
- }
+}
+
+dependencies {
+ samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
+ lintPublish(project(":compose:runtime:runtime-saveable-lint"))
}
androidx {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 0fee604..35456b2 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -765,6 +765,7 @@
public static final class Snapshot.Companion {
method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+ method public boolean isApplyObserverNotificationPending();
method public void notifyObjectsInitialized();
method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
method public androidx.compose.runtime.snapshots.ObserverHandle registerApplyObserver(kotlin.jvm.functions.Function2<? super java.util.Set<?>,? super androidx.compose.runtime.snapshots.Snapshot,kotlin.Unit> observer);
@@ -775,6 +776,7 @@
method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
property public final androidx.compose.runtime.snapshots.Snapshot current;
+ property public final boolean isApplyObserverNotificationPending;
}
public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 1fa82d6..5c926bd 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -833,6 +833,7 @@
public static final class Snapshot.Companion {
method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+ method public boolean isApplyObserverNotificationPending();
method public void notifyObjectsInitialized();
method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
method @androidx.compose.runtime.InternalComposeApi public int openSnapshotCount();
@@ -844,6 +845,7 @@
method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
property public final androidx.compose.runtime.snapshots.Snapshot current;
+ property public final boolean isApplyObserverNotificationPending;
}
public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5ce4dbc..b855aea 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -806,6 +806,7 @@
method @kotlin.PublishedApi internal androidx.compose.runtime.snapshots.Snapshot createNonObservableSnapshot();
method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+ method public boolean isApplyObserverNotificationPending();
method public void notifyObjectsInitialized();
method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
method public androidx.compose.runtime.snapshots.ObserverHandle registerApplyObserver(kotlin.jvm.functions.Function2<? super java.util.Set<?>,? super androidx.compose.runtime.snapshots.Snapshot,kotlin.Unit> observer);
@@ -818,6 +819,7 @@
method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
property public final androidx.compose.runtime.snapshots.Snapshot current;
+ property public final boolean isApplyObserverNotificationPending;
}
public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index b9b65e5..f62e6ba 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -24,69 +23,63 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidTestImplementation(projectOrArtifact(":compose:ui:ui"))
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
- androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:runtime:runtime"))
- androidTestImplementation(projectOrArtifact(":compose:test-utils"))
- androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
-
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.kotlinTestJunit)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.kotlinStdlib)
- androidTestImplementation(libs.kotlinReflect)
- androidTestImplementation(libs.truth)
- }
-}
-
-android {
- namespace "androidx.compose.runtime.integrationtests"
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(libs.kotlinCoroutinesCore)
implementation(projectOrArtifact(":compose:ui:ui"))
}
- jvmMain.dependencies {
+ }
+
+ commonTest {
+ dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
implementation(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
}
- androidMain.dependencies {
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api(libs.kotlinCoroutinesAndroid)
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core-ktx:1.1.0")
}
- desktopMain.dependencies {
- api(libs.kotlinCoroutinesSwing)
- }
+ }
- commonTest.dependencies {
- implementation(kotlin("test-junit"))
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api(libs.kotlinCoroutinesSwing)
+ }
}
- androidAndroidTest.dependencies {
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(projectOrArtifact(":compose:ui:ui"))
implementation(projectOrArtifact(":compose:material:material"))
implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
@@ -98,9 +91,23 @@
implementation(libs.truth)
}
}
+
+ androidTest {
+ dependsOn(jvmTest)
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
}
+android {
+ namespace "androidx.compose.runtime.integrationtests"
+}
+
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
incremental = false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 06d1f47..d5d312b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -16,6 +16,7 @@
package androidx.compose.runtime.snapshots
+import androidx.compose.runtime.AtomicInt
import androidx.compose.runtime.AtomicReference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
@@ -280,6 +281,13 @@
val current get() = currentSnapshot()
/**
+ * Returns whether any threads are currently in the process of notifying observers about
+ * changes to the global snapshot.
+ */
+ val isApplyObserverNotificationPending: Boolean
+ get() = pendingApplyObserverCount.get() > 0
+
+ /**
* Take a snapshot of the current value of all state objects. The values are preserved until
* [Snapshot.dispose] is called on the result.
*
@@ -1767,20 +1775,36 @@
return result
}
+/**
+ * Counts the number of threads currently inside `advanceGlobalSnapshot`, notifying observers of
+ * changes to the global snapshot.
+ */
+private var pendingApplyObserverCount = AtomicInt(0)
+
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
var previousGlobalSnapshot = snapshotInitializer as GlobalSnapshot
+
+ var modified: IdentityArraySet<StateObject>? = null // Effectively val; can be with contracts
val result = sync {
previousGlobalSnapshot = currentGlobalSnapshot.get()
+ modified = previousGlobalSnapshot.modified
+ if (modified != null) {
+ pendingApplyObserverCount.add(1)
+ }
takeNewGlobalSnapshot(previousGlobalSnapshot, block)
}
// If the previous global snapshot had any modified states then notify the registered apply
// observers.
- val modified = previousGlobalSnapshot.modified
- if (modified != null) {
- val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
- observers.fastForEach { observer ->
- observer(modified, previousGlobalSnapshot)
+ modified?.let {
+ try {
+ val observers: List<(Set<Any>, Snapshot) -> Unit> =
+ sync { applyObservers.toMutableList() }
+ observers.fastForEach { observer ->
+ observer(it, previousGlobalSnapshot)
+ }
+ } finally {
+ pendingApplyObserverCount.add(-1)
}
}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 8efc203..3ec3fb7 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -269,6 +269,33 @@
}
@Test
+ fun applyObserverNotificationIsPendingWhileSendingApplyNotifications() {
+ val state = mutableStateOf(0)
+
+ var notificationsPendingWhileObserving = false
+ val unregister = Snapshot.registerApplyObserver { _, _ ->
+ notificationsPendingWhileObserving = Snapshot.isApplyObserverNotificationPending
+ }
+
+ try {
+ // Normally not pending
+ assertFalse(Snapshot.isApplyObserverNotificationPending)
+
+ state.value = 1
+
+ Snapshot.sendApplyNotifications()
+
+ // Was pending while sending apply notifications
+ assertTrue(notificationsPendingWhileObserving)
+
+ // Not pending afterwards
+ assertFalse(Snapshot.isApplyObserverNotificationPending)
+ } finally {
+ unregister.dispose()
+ }
+ }
+
+ @Test
fun aNestedSnapshotCanBeTaken() {
val state = mutableStateOf<Int>(0)
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 5b5d26c..46eecc3 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,89 +23,93 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = false // b/276387374 TODO: KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
-
- api("androidx.activity:activity:1.2.0")
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
- api(project(":test:screenshot:screenshot"))
-
- implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui-unit"))
- implementation(projectOrArtifact(":compose:ui:ui-graphics"))
- implementation("androidx.activity:activity-compose:1.3.1")
- // old version of common-java8 conflicts with newer version, because both have
- // DefaultLifecycleEventObserver.
- // Outside of androidx this is resolved via constraint added to lifecycle-common,
- // but it doesn't work in androidx.
- // See aosp/1804059
- implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
- implementation(libs.testCore)
- implementation(libs.testRules)
-
- // This has stub APIs for access to legacy Android APIs, so we don't want
- // any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-
- testImplementation(libs.truth)
-
- androidTestImplementation(libs.truth)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
- }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(projectOrArtifact(":compose:runtime:runtime"))
implementation(projectOrArtifact(":compose:ui:ui-unit"))
implementation(projectOrArtifact(":compose:ui:ui-graphics"))
implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
}
+ }
+ androidMain.dependencies {
+ api("androidx.activity:activity:1.2.0")
+ implementation "androidx.activity:activity-compose:1.3.1"
+ api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ api(project(":test:screenshot:screenshot"))
+ // This has stub APIs for access to legacy Android APIs, so we don't want
+ // any dependency on this module.
+ compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ implementation(libs.testCore)
+ implementation(libs.testRules)
+ }
- androidMain.dependencies {
- api("androidx.activity:activity:1.2.0")
- implementation "androidx.activity:activity-compose:1.3.1"
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
- api(project(":test:screenshot:screenshot"))
- // This has stub APIs for access to legacy Android APIs, so we don't want
- // any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
- implementation(libs.testCore)
- implementation(libs.testRules)
+ commonTest {
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.truth)
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.truth)
implementation(projectOrArtifact(":compose:material:material"))
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index 1a05daf..15e6e16 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,53 +23,69 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.1.0")
-
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(project(":compose:ui:ui-util"))
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinTest)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:runtime:runtime"))
+ implementation("androidx.compose.runtime:runtime:1.2.1")
implementation(project(":compose:ui:ui-util"))
}
- jvmMain.dependencies {
+ }
+
+ commonTest {
+ dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ }
+
+ jvmMain {
+ dependencies {
implementation(libs.kotlinStdlib)
}
- androidMain.dependencies {
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
}
- commonTest.dependencies {
- implementation(kotlin("test-junit"))
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
}
}
}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 2042050..18cdc50 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 invoke();
+ method public long produce();
}
@androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class FilterQuality {
@@ -634,6 +634,7 @@
method public void reset();
method public default void rewind();
method public void setFillType(int);
+ method public default void transform(float[] matrix);
method public void translate(long offset);
property public abstract int fillType;
property public abstract boolean isConvex;
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 cb69cd3..75d0b80 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 invoke();
+ method public long produce();
}
@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 {
@@ -637,6 +637,7 @@
method public void reset();
method public default void rewind();
method public void setFillType(int);
+ method public default void transform(float[] matrix);
method public void translate(long offset);
property public abstract int fillType;
property public abstract boolean isConvex;
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index 032a956b..08b7e05 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 invoke();
+ method public long produce();
}
public final class DegreesKt {
@@ -669,6 +669,7 @@
method public void reset();
method public default void rewind();
method public void setFillType(int);
+ method public default void transform(float[] matrix);
method public void translate(long offset);
property public abstract int fillType;
property public abstract boolean isConvex;
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 815c1df..cb9f0da 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,102 +23,72 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.2.0")
- api(project(":compose:ui:ui-unit"))
-
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(project(":compose:ui:ui-util"))
- implementation(libs.kotlinStdlibCommon)
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.kotlinTestJunit)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(project(":compose:ui:ui-graphics:ui-graphics-samples"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
-
- lintPublish(project(":compose:ui:ui-graphics-lint"))
-
- samples(projectOrArtifact(":compose:ui:ui-graphics:ui-graphics-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
-
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui-unit"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-util"))
}
+ }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.2.0")
+ commonTest {
+ dependencies {
+ implementation(kotlin("test"))
}
+ }
+ if (desktopEnabled) {
skikoMain {
dependsOn(commonMain)
dependencies {
api(libs.skikoCommon)
+ implementation(project(":compose:runtime:runtime"))
}
}
+ }
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.2.0")
+ }
+ }
+
+ if (desktopEnabled) {
desktopMain {
- dependsOn skikoMain
+ dependsOn(jvmMain)
+ dependsOn(skikoMain)
dependencies {
implementation(libs.kotlinStdlib)
implementation(libs.kotlinStdlibJdk8)
}
}
+ }
- commonTest {
- dependencies {
- implementation(kotlin("test"))
- }
+ jvmTest {
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
- }
-
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:ui:ui-graphics:ui-graphics-samples"))
implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":compose:test-utils"))
@@ -128,9 +97,26 @@
implementation(libs.espressoCore)
implementation(libs.junit)
}
+ }
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
desktopTest {
resources.srcDirs += "src/desktopTest/res"
+ dependsOn(jvmTest)
dependencies {
implementation(project(":compose:ui:ui-test-junit4"))
implementation(libs.junit)
@@ -140,9 +126,10 @@
}
}
}
- dependencies {
- samples(projectOrArtifact(":compose:ui:ui-graphics:ui-graphics-samples"))
- }
+}
+
+dependencies {
+ lintPublish(project(":compose:ui:ui-graphics-lint"))
}
androidx {
@@ -161,7 +148,7 @@
namespace "androidx.compose.ui.graphics"
}
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+if (desktopEnabled) {
tasks.findByName("desktopTest").configure {
systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
}
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
index 4784e83..b825b33 100644
--- a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
@@ -26,8 +26,10 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.unit.dp
@Sampled
@@ -57,4 +59,108 @@
val sweep = Brush.sweepGradient(listOf(Color.Cyan, Color.Magenta))
Box(modifier = Modifier.size(120.dp).background(sweep))
}
+}
+
+@Sampled
+fun LinearGradientColorStopSample() {
+ Brush.linearGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Green,
+ 1.0f to Color.Blue,
+ start = Offset(0.0f, 50.0f),
+ end = Offset(0.0f, 100.0f)
+ )
+}
+
+@Sampled
+fun LinearGradientSample() {
+ Brush.linearGradient(
+ listOf(Color.Red, Color.Green, Color.Blue),
+ start = Offset(0.0f, 50.0f),
+ end = Offset(0.0f, 100.0f)
+ )
+}
+
+@Sampled
+fun HorizontalGradientSample() {
+ Brush.horizontalGradient(
+ listOf(Color.Red, Color.Green, Color.Blue),
+ startX = 10.0f,
+ endX = 20.0f
+ )
+}
+
+@Sampled
+fun HorizontalGradientColorStopSample() {
+ Brush.horizontalGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Green,
+ 1.0f to Color.Blue,
+ startX = 0.0f,
+ endX = 100.0f
+ )
+}
+
+@Sampled
+fun VerticalGradientColorStopSample() {
+ Brush.verticalGradient(
+ 0.1f to Color.Red,
+ 0.3f to Color.Green,
+ 0.5f to Color.Blue,
+ startY = 0.0f,
+ endY = 100.0f
+ )
+}
+
+@Sampled
+fun VerticalGradientSample() {
+ Brush.verticalGradient(
+ listOf(Color.Red, Color.Green, Color.Blue),
+ startY = 0.0f,
+ endY = 100.0f
+ )
+}
+
+@Sampled
+fun RadialBrushColorStopSample() {
+ val side1 = 100
+ val side2 = 200
+ Brush.radialGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Green,
+ 1.0f to Color.Blue,
+ center = Offset(side1 / 2.0f, side2 / 2.0f),
+ radius = side1 / 2.0f,
+ tileMode = TileMode.Repeated
+ )
+}
+
+@Sampled
+fun RadialBrushSample() {
+ val side1 = 100
+ val side2 = 200
+ Brush.radialGradient(
+ listOf(Color.Red, Color.Green, Color.Blue),
+ center = Offset(side1 / 2.0f, side2 / 2.0f),
+ radius = side1 / 2.0f,
+ tileMode = TileMode.Repeated
+ )
+}
+
+@Sampled
+fun SweepGradientColorStopSample() {
+ Brush.sweepGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Green,
+ 1.0f to Color.Blue,
+ center = Offset(0.0f, 100.0f)
+ )
+}
+
+@Sampled
+fun SweepGradientSample() {
+ Brush.sweepGradient(
+ listOf(Color.Red, Color.Green, Color.Blue),
+ center = Offset(10.0f, 20.0f)
+ )
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
index 6975ef7..def70c3 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
@@ -97,6 +97,37 @@
assertEquals(0, androidPath.resetCount)
}
+ @Test
+ fun testPathTransform() {
+ val width = 100
+ val height = 100
+ val image = ImageBitmap(width, height)
+ val canvas = Canvas(image)
+
+ val path = Path().apply {
+ addRect(Rect(0f, 0f, 50f, 50f))
+ transform(
+ Matrix().apply { translate(50f, 50f) }
+ )
+ }
+
+ val paint = Paint().apply { color = Color.Black }
+ canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+ paint.color = Color.Red
+ canvas.drawPath(path, paint)
+
+ image.toPixelMap().apply {
+ assertEquals(Color.Black, this[width / 2 - 3, height / 2 - 3])
+ assertEquals(Color.Black, this[width / 2, height / 2 - 3])
+ assertEquals(Color.Black, this[width / 2 - 3, height / 2])
+
+ assertEquals(Color.Red, this[width / 2 + 2, height / 2 + 2])
+ assertEquals(Color.Red, this[width - 2, height / 2 + 2])
+ assertEquals(Color.Red, this[width - 2, height - 2])
+ assertEquals(Color.Red, this[width / 2 + 2, height - 2])
+ }
+ }
+
class TestAndroidPath : android.graphics.Path() {
var resetCount = 0
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index 786ed1f..a1c9071 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -28,6 +28,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -197,6 +199,53 @@
)
}
+ @Test
+ fun testInvalidWidthBrush() {
+ // Verify that attempts to create a RadialGradient with a width of 0 do not throw
+ // IllegalArgumentExceptions for an invalid radius
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ brush.applyTo(Size(0f, 10f), paint, 1.0f)
+ }
+
+ @Test
+ fun testInvalidHeightBrush() {
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ // Verify that attempts to create a RadialGradient with a height of 0 do not throw
+ // IllegalArgumentExceptions for an invalid radius
+ brush.applyTo(Size(10f, 0f), paint, 1.0f)
+ }
+
+ @Test
+ fun testValidToInvalidWidthBrush() {
+ // Verify that attempts to create a RadialGradient with a non-zero width/height that
+ // is later attempted to be recreated with a zero width remove the shader from the Paint
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+ assertNotNull(paint.shader)
+
+ brush.applyTo(Size(0f, 10f), paint, 1.0f)
+ assertNull(paint.shader)
+ }
+
+ @Test
+ fun testValidToInvalidHeightBrush() {
+ // Verify that attempts to create a RadialGradient with a non-zero width/height that
+ // is later attempted to be recreated with a zero height remove the shader from the Paint
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+
+ brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+ assertNotNull(paint.shader)
+
+ brush.applyTo(Size(10f, 0f), paint, 1.0f)
+ assertNull(paint.shader)
+ }
+
private fun ImageBitmap.drawInto(
block: DrawScope.() -> Unit
) = CanvasDrawScope().draw(
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
index 1293ae9..8f2debb 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
@@ -335,7 +335,9 @@
*/
private fun drawLines(points: List<Offset>, paint: Paint, stepBy: Int) {
if (points.size >= 2) {
- for (i in 0 until points.size - 1 step stepBy) {
+ val frameworkPaint = paint.asFrameworkPaint()
+ var i = 0
+ while (i < points.size - 1) {
val p1 = points[i]
val p2 = points[i + 1]
internalCanvas.drawLine(
@@ -343,8 +345,9 @@
p1.y,
p2.x,
p2.y,
- paint.asFrameworkPaint()
+ frameworkPaint
)
+ i += stepBy
}
}
}
@@ -365,10 +368,13 @@
private fun drawRawPoints(points: FloatArray, paint: Paint, stepBy: Int) {
if (points.size % 2 == 0) {
- for (i in 0 until points.size - 1 step stepBy) {
+ val frameworkPaint = paint.asFrameworkPaint()
+ var i = 0
+ while (i < points.size - 1) {
val x = points[i]
val y = points[i + 1]
- internalCanvas.drawPoint(x, y, paint.asFrameworkPaint())
+ internalCanvas.drawPoint(x, y, frameworkPaint)
+ i += stepBy
}
}
}
@@ -390,18 +396,15 @@
// Float array is treated as alternative set of x and y coordinates
// x1, y1, x2, y2, x3, y3, ... etc.
if (points.size >= 4 && points.size % 2 == 0) {
- for (i in 0 until points.size - 3 step stepBy * 2) {
+ val frameworkPaint = paint.asFrameworkPaint()
+ var i = 0
+ while (i < points.size - 3) {
val x1 = points[i]
val y1 = points[i + 1]
val x2 = points[i + 2]
val y2 = points[i + 3]
- internalCanvas.drawLine(
- x1,
- y1,
- x2,
- y2,
- paint.asFrameworkPaint()
- )
+ internalCanvas.drawLine(x1, y1, x2, y2, frameworkPaint)
+ i += stepBy * 2
}
}
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 18705bc..f4272bb 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -190,6 +190,11 @@
internalPath.transform(mMatrix)
}
+ override fun transform(matrix: Matrix) {
+ mMatrix.setFrom(matrix)
+ internalPath.transform(mMatrix)
+ }
+
override fun getBounds(): Rect {
internalPath.computeBounds(rectF, true)
return Rect(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index bda9615..5f7a1d0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -57,7 +57,8 @@
* )
* ```
*
- * @see androidx.compose.ui.graphics.samples.GradientBrushSample
+ * @sample androidx.compose.ui.graphics.samples.LinearGradientColorStopSample
+ * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colorStops Colors and their offset in the gradient area
* @param start Starting position of the linear gradient. This can be set to
@@ -88,11 +89,12 @@
* ```
* Brush.linearGradient(
* listOf(Color.Red, Color.Green, Color.Blue),
- * start = Offset(0.0f, 50.0f)
+ * start = Offset(0.0f, 50.0f),
* end = Offset(0.0f, 100.0f)
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.LinearGradientSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colors Colors to be rendered as part of the gradient
@@ -129,6 +131,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.HorizontalGradientSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colors colors Colors to be rendered as part of the gradient
@@ -163,6 +166,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.HorizontalGradientColorStopSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -197,9 +201,9 @@
* startY = 0.0f,
* endY = 100.0f
* )
- *
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.VerticalGradientSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colors colors Colors to be rendered as part of the gradient
@@ -234,6 +238,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.VerticalGradientColorStopSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -273,6 +278,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.RadialBrushColorStopSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -305,13 +311,13 @@
* ```
* Brush.radialGradient(
* listOf(Color.Red, Color.Green, Color.Blue),
- * centerX = side1 / 2.0f,
- * centerY = side2 / 2.0f,
+ * center = Offset(side1 / 2.0f, side2 / 2.0f),
* radius = side1 / 2.0f,
* tileMode = TileMode.Repeated
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.RadialBrushSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colors Colors to be rendered as part of the gradient
@@ -353,6 +359,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.SweepGradientColorStopSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -384,6 +391,7 @@
* )
* ```
*
+ * @sample androidx.compose.ui.graphics.samples.SweepGradientSample
* @sample androidx.compose.ui.graphics.samples.GradientBrushSample
*
* @param colors List of colors to fill the sweep gradient
@@ -645,8 +653,14 @@
final override fun applyTo(size: Size, p: Paint, alpha: Float) {
var shader = internalShader
if (shader == null || createdSize != size) {
- shader = createShader(size).also { internalShader = it }
- createdSize = size
+ if (size.isEmpty()) {
+ shader = null
+ internalShader = null
+ createdSize = Size.Unspecified
+ } else {
+ shader = createShader(size).also { internalShader = it }
+ createdSize = size
+ }
}
if (p.color != Color.Black) p.color = Color.Black
if (p.shader != shader) p.shader = shader
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 ca53498..a17406a 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 invoke(): Color
+ fun produce(): Color
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index b12273e..e54eef0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -233,6 +233,13 @@
fun translate(offset: Offset)
/**
+ * Transform the points in this path by the provided matrix
+ */
+ fun transform(matrix: Matrix) {
+ // NO-OP to ensure runtime + compile time compatibility
+ }
+
+ /**
* Compute the bounds of the control points of the path, and write the
* answer into bounds. If the path contains 0 or 1 points, the bounds is
* set to (0,0,0,0)
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index 42cf68f..b4e0ed9 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -139,7 +139,7 @@
return this
}
- fun toNodes(): List<PathNode> = nodes
+ fun toNodes() = nodes
fun toPath(target: Path = Path()) = nodes.toPath(target)
@@ -156,7 +156,7 @@
* created [Path].
*/
fun List<PathNode>.toPath(target: Path = Path()): Path {
- // Rewind unsets the filltype so reset it here
+ // Rewind unsets the fill type so reset it here
val fillType = target.fillType
target.rewind()
target.fillType = fillType
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
index 92b808d..31ff184 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
+++ b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.PathOperation
@@ -187,6 +188,10 @@
// NO-OP
}
+ override fun transform(matrix: Matrix) {
+ // NO-OP
+ }
+
override fun getBounds(): Rect = Rect.Zero
override fun op(path1: Path, path2: Path, operation: PathOperation): Boolean = false
diff --git a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
index 332b62b..4dc92b9 100644
--- a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
+++ b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
@@ -225,4 +225,35 @@
assertTrue(path.isEmpty)
}
+
+ @Test
+ fun testTransform() {
+ val width = 100
+ val height = 100
+ val image = ImageBitmap(width, height)
+ val canvas = Canvas(image)
+
+ val path = Path().apply {
+ addRect(Rect(0f, 0f, 50f, 50f))
+ transform(
+ Matrix().apply { translate(50f, 50f) }
+ )
+ }
+
+ val paint = Paint().apply { color = Color.Black }
+ canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+ paint.color = Color.Red
+ canvas.drawPath(path, paint)
+
+ image.toPixelMap().apply {
+ assertEquals(Color.Black, this[width / 2 - 3, height / 2 - 3])
+ assertEquals(Color.Black, this[width / 2, height / 2 - 3])
+ assertEquals(Color.Black, this[width / 2 - 3, height / 2])
+
+ assertEquals(Color.Red, this[width / 2 + 2, height / 2 + 2])
+ assertEquals(Color.Red, this[width - 2, height / 2 + 2])
+ assertEquals(Color.Red, this[width - 2, height - 2])
+ assertEquals(Color.Red, this[width / 2 + 2, height - 2])
+ }
+ }
}
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
index 403e36e..0285785 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
@@ -172,6 +172,10 @@
internalPath.transform(Matrix33.makeTranslate(offset.x, offset.y))
}
+ override fun transform(matrix: Matrix) {
+ internalPath.transform(Matrix33.makeTranslate(0f, 0f).apply { setFrom(matrix) })
+ }
+
override fun getBounds(): Rect {
val bounds = internalPath.bounds
return Rect(
@@ -209,4 +213,63 @@
override val isConvex: Boolean get() = internalPath.isConvex
override val isEmpty: Boolean get() = internalPath.isEmpty
+
+ fun Matrix33.setFrom(matrix: Matrix) {
+ require(
+ matrix[0, 2] == 0f &&
+ matrix[1, 2] == 0f &&
+ matrix[2, 2] == 1f &&
+ matrix[3, 2] == 0f &&
+ matrix[2, 0] == 0f &&
+ matrix[2, 1] == 0f &&
+ matrix[2, 3] == 0f
+ ) {
+ "Matrix33 does not support arbitrary transforms"
+ }
+
+ // We'll reuse the array used in Matrix to avoid allocation by temporarily
+ // setting it to the 3x3 matrix used by android.graphics.Matrix
+ // Store the values of the 4 x 4 matrix into temporary variables
+ // to be reset after the 3 x 3 matrix is configured
+ val scaleX = matrix.values[Matrix.ScaleX] // 0
+ val skewY = matrix.values[Matrix.SkewY] // 1
+ val v2 = matrix.values[2] // 2
+ val persp0 = matrix.values[Matrix.Perspective0] // 3
+ val skewX = matrix.values[Matrix.SkewX] // 4
+ val scaleY = matrix.values[Matrix.ScaleY] // 5
+ val v6 = matrix.values[6] // 6
+ val persp1 = matrix.values[Matrix.Perspective1] // 7
+ val v8 = matrix.values[8] // 8
+
+ val translateX = matrix.values[Matrix.TranslateX]
+ val translateY = matrix.values[Matrix.TranslateY]
+ val persp2 = matrix.values[Matrix.Perspective2]
+
+ val v = matrix.values
+
+ v[0] = scaleX // MSCALE_X = 0
+ v[1] = skewX // MSKEW_X = 1
+ v[2] = translateX // MTRANS_X = 2
+ v[3] = skewY // MSKEW_Y = 3
+ v[4] = scaleY // MSCALE_Y = 4
+ v[5] = translateY // MTRANS_Y
+ v[6] = persp0 // MPERSP_0 = 6
+ v[7] = persp1 // MPERSP_1 = 7
+ v[8] = persp2 // MPERSP_2 = 8
+
+ for (i in 0..8) {
+ mat[i] = v[i]
+ }
+
+ // Reset the values back after the android.graphics.Matrix is configured
+ v[Matrix.ScaleX] = scaleX // 0
+ v[Matrix.SkewY] = skewY // 1
+ v[2] = v2 // 2
+ v[Matrix.Perspective0] = persp0 // 3
+ v[Matrix.SkewX] = skewX // 4
+ v[Matrix.ScaleY] = scaleY // 5
+ v[6] = v6 // 6
+ v[Matrix.Perspective1] = persp1 // 7
+ v[8] = v8 // 8
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 5e4501d..5db2d70 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,81 +23,42 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-android {
- lintOptions {
- disable("InvalidPackage")
- }
- namespace "androidx.compose.ui.test.junit4"
-}
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
-dependencies {
-
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- api(project(":compose:ui:ui-test"))
- api("androidx.activity:activity:1.2.1")
- api(libs.junit)
- api(libs.kotlinStdlib)
- api(libs.kotlinStdlibCommon)
- api(libs.testExtJunit)
-
- implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
- implementation("androidx.activity:activity-compose:1.3.0")
- implementation("androidx.annotation:annotation:1.1.0")
- implementation("androidx.lifecycle:lifecycle-common:2.5.1")
- implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
- implementation("androidx.test:core:1.5.0")
- implementation("androidx.test:monitor:1.6.0")
- implementation("androidx.test.espresso:espresso-core:3.5.0")
- implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
- implementation(libs.kotlinCoroutinesCore)
- implementation(libs.kotlinCoroutinesTest)
-
- testImplementation(project(":compose:animation:animation-core"))
- testImplementation(project(":compose:material:material"))
- testImplementation(project(":compose:test-utils"))
- testImplementation(libs.truth)
- testImplementation(libs.robolectric)
-
- androidTestImplementation(project(":compose:animation:animation"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation("androidx.fragment:fragment-testing:1.4.1")
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoKotlin)
- }
-}
-
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
api(project(":compose:ui:ui-test"))
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
implementation(libs.kotlinCoroutinesTest)
}
+ }
- jvmMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
api(libs.junit)
api(libs.kotlinStdlib)
api(libs.kotlinStdlibCommon)
compileOnly("androidx.annotation:annotation:1.1.0")
}
+ }
- androidMain.dependencies {
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.activity:activity:1.2.1")
implementation "androidx.activity:activity-compose:1.3.0"
api(libs.testExtJunit)
@@ -107,24 +67,31 @@
implementation(project(":compose:runtime:runtime-saveable"))
implementation("androidx.lifecycle:lifecycle-common:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
- implementation("androidx.test:core:1.4.0")
+ implementation("androidx.test:core:1.5.0")
implementation(libs.testMonitor)
implementation("androidx.test.espresso:espresso-core:3.3.0")
- implementation("androidx.test.espresso:espresso-idling-resource:3.3.0")
+ implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(project(":compose:animation:animation-core"))
- implementation(project(":compose:material:material"))
- implementation(project(":compose:test-utils"))
- implementation(libs.truth)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.truth)
+ implementation(libs.skiko)
+ }
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:animation:animation"))
implementation(project(":compose:test-utils"))
implementation(project(":compose:material:material"))
@@ -136,28 +103,50 @@
implementation(libs.dexmakerMockito)
implementation(libs.mockitoKotlin)
}
+ }
- desktopMain.dependencies {
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(project(":compose:animation:animation-core"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:test-utils"))
implementation(libs.truth)
- implementation(libs.skiko)
}
+ }
- desktopTest.dependencies {
- implementation(libs.truth)
- implementation(libs.junit)
- implementation(libs.kotlinTest)
- implementation(libs.skikoCurrentOs)
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:ui:ui-test-junit4"))
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.kotlinTest)
+ implementation(libs.skikoCurrentOs)
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ }
}
}
}
+}
- dependencies {
- // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
- // leaks into instrumented tests (b/214407011)
- testImplementation(libs.robolectric)
+
+android {
+ lintOptions {
+ disable("InvalidPackage")
}
+ namespace "androidx.compose.ui.test.junit4"
+}
+
+dependencies {
+ // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+ // leaks into instrumented tests (b/214407011)
+ testImplementation(libs.robolectric)
}
androidx {
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index 8af350a..6977ddf 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -182,6 +182,10 @@
}
}
+ /**
+ * @param matcher
+ * @param useUnmergedTree
+ */
override fun onNode(
matcher: SemanticsMatcher,
useUnmergedTree: Boolean
@@ -189,6 +193,10 @@
return SemanticsNodeInteraction(testContext, useUnmergedTree, matcher)
}
+ /**
+ * @param matcher
+ * @param useUnmergedTree
+ */
override fun onAllNodes(
matcher: SemanticsMatcher,
useUnmergedTree: Boolean
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 097486a..ead9fac 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,14 +23,115 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-text"))
+ api(project(":compose:ui:ui-unit"))
+ api(project(":compose:runtime:runtime"))
+ api(libs.kotlinStdlib)
+
+ implementation(project(":compose:ui:ui-util"))
+ }
+ }
+
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(libs.kotlinCoroutinesCore)
+ api(libs.kotlinCoroutinesTest)
+ api(libs.kotlinStdlibCommon)
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api(project(":compose:ui:ui-graphics"))
+
+ implementation("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.core:core-ktx:1.2.0")
+ implementation("androidx.test.espresso:espresso-core:3.5.0")
+ implementation(libs.testMonitor)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.junit)
+ implementation(libs.truth)
+ implementation(libs.skiko)
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidCommonTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(project(":compose:test-utils"))
+ implementation(libs.truth)
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependsOn(androidCommonTest)
+ dependencies {
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation("androidx.activity:activity-compose:1.3.1")
+ implementation(libs.mockitoCore)
+ implementation(libs.mockitoKotlin)
+ implementation(libs.dexmakerMockito)
+ implementation(libs.kotlinTest)
+ }
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependsOn(androidCommonTest)
+ dependencies {
+ implementation(libs.mockitoCore4)
+ implementation(libs.mockitoKotlin4)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
+ }
+}
android {
- if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- sourceSets {
- test.java.srcDirs += "src/androidCommonTest/kotlin"
- androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
- }
+ sourceSets {
+ test.java.srcDirs += "src/androidCommonTest/kotlin"
+ androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
}
lintOptions {
@@ -41,121 +141,9 @@
}
dependencies {
-
- if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- api("androidx.compose.runtime:runtime:1.2.1")
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-graphics"))
- api(project(":compose:ui:ui-text"))
- api(project(":compose:ui:ui-unit"))
- api(libs.kotlinCoroutinesCore)
- api(libs.kotlinCoroutinesTest)
- api(libs.kotlinStdlib)
- api(libs.kotlinStdlibCommon)
-
- implementation(project(":compose:ui:ui-util"))
- implementation("androidx.annotation:annotation:1.1.0")
- implementation("androidx.core:core-ktx:1.1.0")
- implementation("androidx.test.espresso:espresso-core:3.5.0")
- implementation("androidx.test:monitor:1.6.0")
-
- testImplementation(project(":compose:test-utils"))
- testImplementation(libs.truth)
- testImplementation(libs.robolectric)
- testImplementation(libs.mockitoCore4)
- testImplementation(libs.mockitoKotlin4)
-
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoKotlin)
- androidTestImplementation(libs.kotlinTest)
-
- samples(project(":compose:ui:ui-test:ui-test-samples"))
- }
-}
-
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- sourceSets {
- commonMain.dependencies {
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-text"))
- api(project(":compose:ui:ui-unit"))
- api(libs.kotlinStdlib)
-
- implementation(project(":compose:ui:ui-util"))
- }
-
- jvmMain.dependencies {
- api(project(":compose:runtime:runtime"))
- api(libs.kotlinCoroutinesCore)
- api(libs.kotlinCoroutinesTest)
- api(libs.kotlinStdlibCommon)
- }
-
- androidMain.dependencies {
- api(project(":compose:ui:ui-graphics"))
-
- implementation("androidx.annotation:annotation:1.1.0")
- implementation("androidx.core:core-ktx:1.2.0")
- implementation("androidx.test.espresso:espresso-core:3.3.0")
- implementation(libs.testMonitor)
- }
-
- androidCommonTest.dependencies {
- implementation(project(":compose:test-utils"))
- implementation(libs.truth)
- }
-
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.mockitoCore4)
- implementation(libs.mockitoKotlin4)
- }
-
- androidAndroidTest.dependencies {
- implementation(project(":compose:material:material"))
- implementation(project(":compose:ui:ui-test-junit4"))
- implementation("androidx.activity:activity-compose:1.3.1")
- implementation(libs.mockitoCore)
- implementation(libs.mockitoKotlin)
- implementation(libs.dexmakerMockito)
- implementation(libs.kotlinTest)
- }
-
- desktopMain.dependencies {
- implementation(libs.junit)
- implementation(libs.truth)
- implementation(libs.skiko)
- }
-
- androidCommonTest.dependsOn(commonTest)
- androidTest.dependsOn(androidCommonTest)
- androidAndroidTest.dependsOn(androidCommonTest)
- }
- }
-
- dependencies {
- // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
- // leaks into instrumented tests (b/214407011)
- testImplementation(libs.robolectric)
-
- samples(project(":compose:ui:ui-test:ui-test-samples"))
- }
+ // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+ // leaks into instrumented tests (b/214407011)
+ testImplementation(libs.robolectric)
}
androidx {
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
index f818fcc..5d24c91 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
@@ -129,10 +129,10 @@
|-Node #X at (l=X, t=X, r=X, b=X)px, Tag: 'box'
| [Disabled]
| |-Node #X at (l=X, t=X, r=X, b=X)px
- | Role = 'Button'
| Focused = 'false'
+ | Role = 'Button'
| Text = '[Button]'
- | Actions = [RequestFocus, OnClick, GetTextLayoutResult]
+ | Actions = [OnClick, RequestFocus, GetTextLayoutResult]
| MergeDescendants = 'true'
|-Node #X at (l=X, t=X, r=X, b=X)px
Text = '[Hello]'
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index c20c781..2903ae2 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,77 +23,15 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- implementation(libs.kotlinStdlibCommon)
- implementation(libs.kotlinCoroutinesCore)
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api(project(":compose:ui:ui-graphics"))
- api(project(":compose:ui:ui-unit"))
- api("androidx.annotation:annotation:1.1.0")
-
- // when updating the runtime version please also update the runtime-saveable version
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
-
- implementation(project(":compose:ui:ui-util"))
- implementation(libs.kotlinStdlib)
- implementation("androidx.core:core:1.7.0")
- implementation('androidx.collection:collection:1.0.0')
- implementation("androidx.emoji2:emoji2:1.2.0")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.mockitoCore4)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinReflect)
- testImplementation(libs.kotlinTest)
- testImplementation(libs.mockitoKotlin4)
-
- androidTestImplementation(project(":internal-testutils-fonts"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoKotlin)
-
- samples(projectOrArtifact(":compose:ui:ui-text:ui-text-samples"))
- }
-
- android {
- sourceSets {
- main {
- java.srcDirs += "${supportRootFolder}/text/text/src/main/java"
- }
- }
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(libs.kotlinCoroutinesCore)
@@ -107,53 +44,58 @@
implementation(project(":compose:ui:ui-util"))
}
+ }
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
+ commonTest {
+ dependencies {
}
+ }
+ if (desktopEnabled) {
skikoMain {
dependsOn(commonMain)
dependencies {
api(libs.skikoCommon)
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:runtime:runtime-saveable"))
}
}
+ }
- desktopMain {
- dependsOn(skikoMain)
- dependsOn(jvmMain)
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
}
+ }
- androidMain {
- dependsOn(commonMain)
- }
- androidMain.dependencies {
+ androidMain {
+ dependsOn(commonMain)
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core:1.7.0")
implementation("androidx.emoji2:emoji2:1.2.0")
implementation('androidx.collection:collection:1.0.0')
}
+ }
- androidMain.kotlin.srcDirs("${supportRootFolder}/text/text/src/main/java")
-
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(project(":internal-testutils-fonts"))
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.mockitoCore4)
- implementation(libs.truth)
- implementation(libs.kotlinReflect)
- implementation(libs.kotlinTest)
- implementation(libs.mockitoKotlin4)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:ui:ui-test-junit4"))
implementation(project(":internal-testutils-fonts"))
implementation(libs.testRules)
@@ -165,20 +107,50 @@
implementation(libs.truth)
implementation(libs.mockitoKotlin)
}
+ }
- desktopTest.dependencies {
- implementation(libs.truth)
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(project(":internal-testutils-fonts"))
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
implementation(libs.junit)
+ implementation(libs.truth)
+ implementation(libs.kotlinReflect)
implementation(libs.kotlinTest)
- implementation(libs.skikoCurrentOs)
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:ui:ui-test-junit4"))
}
}
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.kotlinTest)
+ implementation(libs.skikoCurrentOs)
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":internal-testutils-fonts"))
+ }
+ }
+ }
+
+ androidMain.kotlin.srcDirs("${supportRootFolder}/text/text/src/main/java")
}
- dependencies {
- samples(projectOrArtifact(":compose:ui:ui-text:ui-text-samples"))
- }
+}
+
+dependencies {
+ // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+ // leaks into instrumented tests (b/214407011)
+ testImplementation(libs.mockitoCore4)
+ testImplementation(libs.mockitoKotlin4)
}
androidx {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 386b478..1aef72f 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1348,7 +1348,7 @@
val paragraph = simpleParagraph(
text = "",
style = TextStyle(brush = brush),
- width = 0.0f
+ width = 1.0f
)
assertThat(paragraph.textPaint.shader).isNotNull()
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index 13f1be5..b69203b 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -15,9 +15,8 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,79 +24,70 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
-
- implementation(libs.kotlinStdlib)
-
- api "androidx.annotation:annotation:1.1.0"
-
- api("androidx.compose.runtime:runtime:1.2.1")
- api(project(":compose:ui:ui"))
-
- androidTestImplementation project(":compose:ui:ui-test-junit4")
-
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
-
- androidTestImplementation(libs.truth)
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
-
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlib)
- api "androidx.annotation:annotation:1.1.0"
-
- api("androidx.compose.runtime:runtime:1.2.1")
+ api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
}
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
- }
+ }
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(kotlin("test-junit"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.truth)
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
}
- androidAndroidTest.dependencies {
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:ui:ui-test-junit4"))
implementation(libs.junit)
@@ -112,9 +102,23 @@
implementation("androidx.activity:activity-compose:1.3.1")
}
}
- }
- dependencies {
- samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+
+ }
+ }
+ }
}
}
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index a9f9236..d6d3a42 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,47 +23,91 @@
id("com.android.library")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation(libs.kotlinStdlib)
- api("androidx.annotation:annotation:1.2.0")
- api("androidx.compose.runtime:runtime:1.2.1")
- testImplementation(libs.junit)
- }
-}
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:runtime:runtime"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.2.0")
}
+ }
- androidTest.dependencies {
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.junit)
}
}
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+
+ }
+ }
+ }
}
}
-
androidx {
name = "Compose Tooling API"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 507d5fcf..ea90aee 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,69 +23,51 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation(libs.kotlinStdlib)
-
- api("androidx.annotation:annotation:1.1.0")
- implementation(project(":compose:animation:animation"))
-
- api("androidx.compose.runtime:runtime:1.2.1")
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-tooling-preview"))
- api(project(":compose:ui:ui-tooling-data"))
- implementation("androidx.savedstate:savedstate-ktx:1.2.1")
- implementation("androidx.compose.material:material:1.0.0")
- implementation("androidx.activity:activity-compose:1.7.0")
- implementation("androidx.lifecycle:lifecycle-common:2.6.1")
-
- // kotlin-reflect and animation-tooling-internal are provided by Studio at runtime
- compileOnly(project(":compose:animation:animation-tooling-internal"))
- compileOnly(libs.kotlinReflect)
-
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.kotlinReflect)
- androidTestImplementation(project(":compose:animation:animation-tooling-internal"))
- androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
- androidTestImplementation(project(":compose:runtime:runtime-livedata"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui-tooling-preview"))
api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
api(project(":compose:ui:ui-tooling-data"))
}
- androidMain.dependencies {
+ }
+
+ commonTest {
+ dependencies {
+
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation(project(":compose:animation:animation"))
implementation("androidx.savedstate:savedstate-ktx:1.2.1")
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.material:material:1.0.0")
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.lifecycle:lifecycle-common:2.6.1")
@@ -94,14 +75,29 @@
compileOnly(project(":compose:animation:animation-tooling-internal"))
compileOnly(libs.kotlinReflect)
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
- implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ }
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(project(":compose:ui:ui-test-junit4"))
implementation(libs.junit)
@@ -117,10 +113,25 @@
implementation(project(":compose:runtime:runtime-livedata"))
}
}
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+
+ }
+ }
+ }
}
}
-
androidx {
name = "Compose Tooling"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
index ae1f769..0ecdc83 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
@@ -117,8 +117,10 @@
)
values[endTimeMs] = this.getValueFromNanos(millisToNanos(endTimeMs))
- for (millis in startTimeMs..endTimeMs step stepMs) {
+ var millis = startTimeMs
+ while (millis <= endTimeMs) {
values[millis] = this.getValueFromNanos(millisToNanos(millis))
+ millis += stepMs
}
values
}
@@ -145,8 +147,10 @@
)
values[endTimeMs] = this.animation.getValueFromNanos(millisToNanos(endTimeMs))
- for (millis in startTimeMs..endTimeMs step stepMs) {
+ var millis = startTimeMs
+ while (millis <= endTimeMs) {
values[millis] = this.animation.getValueFromNanos(millisToNanos(millis))
+ millis += stepMs
}
values
}
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index bfbfed7..627cd87 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,85 +23,88 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api(project(":compose:ui:ui-geometry"))
- api("androidx.annotation:annotation:1.1.0")
-
- implementation(libs.kotlinStdlib)
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(project(":compose:ui:ui-util"))
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.kotlinTest)
-
- samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui-geometry"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-util"))
}
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
- }
+ }
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(kotlin("test-junit"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.truth)
+ jvmMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
}
- androidAndroidTest.dependencies {
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.testExtJunit)
implementation(libs.espressoCore)
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
- dependencies {
- samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
- }
+}
+
+dependencies {
+ samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
}
androidx {
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index 8eeb75e..4c19723 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,59 +23,72 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinTest)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
}
+ }
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
-
- androidMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
-
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(kotlin("test-junit"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
+ jvmMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.truth)
}
}
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
}
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 7222c00..eeeb983 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2379,8 +2379,11 @@
}
public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
- method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+ method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public default boolean getShouldClearDescendantSemantics();
+ method public default boolean getShouldMergeDescendantSemantics();
+ property public default boolean shouldClearDescendantSemantics;
+ property public default boolean shouldMergeDescendantSemantics;
}
public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index bb73f64..6b29580 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2064,6 +2064,8 @@
public interface IntrinsicMeasureScope extends androidx.compose.ui.unit.Density {
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public default boolean isLookingAhead();
+ property @androidx.compose.ui.ExperimentalComposeUiApi public default boolean isLookingAhead;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
}
@@ -2604,8 +2606,11 @@
}
public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
- method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+ method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public default boolean getShouldClearDescendantSemantics();
+ method public default boolean getShouldMergeDescendantSemantics();
+ property public default boolean shouldClearDescendantSemantics;
+ property public default boolean shouldMergeDescendantSemantics;
}
public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 24148fc..c17196f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2427,8 +2427,11 @@
}
public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
- method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+ method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public default boolean getShouldClearDescendantSemantics();
+ method public default boolean getShouldMergeDescendantSemantics();
+ property public default boolean shouldClearDescendantSemantics;
+ property public default boolean shouldMergeDescendantSemantics;
}
public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 2b2f069..3beea81 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static androidx.inspection.gradle.InspectionPluginKt.packageInspector
@@ -26,130 +25,16 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- constraints {
- // In 1.4.0-alpha02 there was a change made in :compose:ui:ui which fixed an issue where
- // we were over-invalidating layout. This change caused a corresponding regression in
- // foundation's CoreText, where it was expecting a layout to happen but with this change
- // it would not. A corresponding fix for this was added in 1.4.0-alpha02 of
- // :compose:foundation:foundation. By adding this constraint, we are ensuring that the
- // if an app has this ui module _and_ the foundation module as a dependency, then the
- // version of foundation will be at least this version. This will prevent the bug in
- // foundation from occurring. This does _NOT_ require that the app have foundation as
- // a dependency.
- implementation(project(":compose:foundation:foundation")) {
- because 'prevents a critical bug in Text'
- }
- }
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- implementation(libs.kotlinStdlibCommon)
- implementation(libs.kotlinCoroutinesCore)
- // when updating the runtime version please also update the runtime-saveable version
- implementation(project(":compose:runtime:runtime"))
- api(project(":compose:runtime:runtime-saveable"))
-
- api(project(":compose:ui:ui-geometry"))
- api(project(":compose:ui:ui-graphics"))
- api(project(":compose:ui:ui-text"))
- api(project(":compose:ui:ui-unit"))
- api("androidx.annotation:annotation:1.5.0")
-
- // This has stub APIs for access to legacy Android APIs, so we don't want
- // any dependency on this module.
- compileOnly(project(":compose:ui:ui-android-stubs"))
-
- implementation(project(":compose:ui:ui-util"))
- implementation(libs.kotlinStdlib)
- implementation("androidx.autofill:autofill:1.0.0")
- implementation(libs.kotlinCoroutinesAndroid)
-
- // Used to generate debug information in the layout inspector. If not present,
- // we may fall back to more limited data.
- compileOnly(libs.kotlinReflect)
- testImplementation(libs.kotlinReflect)
-
- implementation("androidx.activity:activity-ktx:1.7.0")
- implementation(project(":core:core"))
- implementation('androidx.collection:collection:1.0.0')
- implementation("androidx.customview:customview-poolingcontainer:1.0.0")
- implementation("androidx.savedstate:savedstate-ktx:1.2.1")
- implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
- implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
- implementation("androidx.profileinstaller:profileinstaller:1.3.0")
- implementation("androidx.emoji2:emoji2:1.2.0")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.kotlinCoroutinesTest)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.mockitoCore4)
- testImplementation(libs.mockitoKotlin4)
- testImplementation(libs.robolectric)
- testImplementation(project(":compose:ui:ui-test-junit4"))
- testImplementation(project(":compose:test-utils"))
-
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testExtJunitKtx)
- androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.kotlinCoroutinesTest)
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.bundles.espressoContrib)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.mockitoKotlin)
- androidTestImplementation(libs.material)
- androidTestImplementation(project(":compose:animation:animation-core"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation(project(":compose:test-utils"))
- androidTestImplementation(project(":internal-testutils-fonts"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":internal-testutils-runtime"))
- androidTestImplementation(project(":test:screenshot:screenshot"))
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
- androidTestImplementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
- androidTestImplementation("androidx.core:core-ktx:1.9.0")
- androidTestImplementation("androidx.activity:activity-compose:1.7.0")
- androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
- androidTestImplementation("androidx.fragment:fragment:1.3.0")
-
- lintChecks(project(":compose:ui:ui-lint"))
- lintPublish(project(":compose:ui:ui-lint"))
-
- samples(project(":compose:ui:ui:ui-samples"))
- }
-}
-
-packageInspector(project, ":compose:ui:ui-inspection")
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(libs.kotlinCoroutinesCore)
@@ -163,8 +48,33 @@
api project(":compose:ui:ui-unit")
implementation(project(":compose:ui:ui-util"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ implementation(libs.kotlinReflect)
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:ui:ui-graphics"))
+ api(libs.skikoCommon)
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
implementation(libs.kotlinStdlib)
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
@@ -181,48 +91,32 @@
implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
implementation("androidx.emoji2:emoji2:1.2.0")
- }
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
+ implementation("androidx.profileinstaller:profileinstaller:1.3.0")
}
- skikoMain {
- dependsOn(commonMain)
- dependencies {
- api(project(":compose:ui:ui-graphics"))
- api(libs.skikoCommon)
- }
- }
+ }
+
+ if (desktopEnabled) {
desktopMain {
dependsOn(skikoMain)
+ dependsOn(jvmMain)
dependencies {
+ implementation(libs.kotlinStdlib)
implementation(libs.kotlinStdlibJdk8)
api(libs.kotlinCoroutinesSwing)
}
}
+ }
- commonTest.dependencies {
- implementation(libs.kotlinReflect)
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.kotlinCoroutinesTest)
- implementation(libs.junit)
- implementation(libs.truth)
- implementation(libs.mockitoCore4)
- implementation(libs.mockitoKotlin4)
- implementation(project(":compose:ui:ui-test-junit4"))
- implementation(project(":internal-testutils-fonts"))
- implementation(project(":compose:test-utils"))
- }
-
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation("androidx.fragment:fragment:1.3.0")
implementation("androidx.appcompat:appcompat:1.3.0")
implementation(libs.testUiautomator)
@@ -253,27 +147,75 @@
implementation("androidx.core:core-ktx:1.2.0")
implementation("androidx.activity:activity-compose:1.7.0")
}
+ }
- desktopTest.dependencies {
- implementation(libs.truth)
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.kotlinCoroutinesTest)
implementation(libs.junit)
- implementation(libs.mockitoCore)
- implementation(libs.mockitoKotlin)
- implementation(libs.skikoCurrentOs)
- implementation(project(":compose:material:material"))
+ implementation(libs.truth)
implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":internal-testutils-fonts"))
+ implementation(project(":compose:test-utils"))
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ implementation(libs.truth)
+ implementation(libs.junit)
+ implementation(libs.mockitoCore)
+ implementation(libs.mockitoKotlin)
+ implementation(libs.skikoCurrentOs)
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ }
}
}
}
- dependencies {
- samples(project(":compose:ui:ui:ui-samples"))
+}
- // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
- // leaks into instrumented tests (b/214407011)
- testImplementation(libs.robolectric)
+dependencies {
+
+ constraints {
+ // In 1.4.0-alpha02 there was a change made in :compose:ui:ui which fixed an issue where
+ // we were over-invalidating layout. This change caused a corresponding regression in
+ // foundation's CoreText, where it was expecting a layout to happen but with this change
+ // it would not. A corresponding fix for this was added in 1.4.0-alpha02 of
+ // :compose:foundation:foundation. By adding this constraint, we are ensuring that the
+ // if an app has this ui module _and_ the foundation module as a dependency, then the
+ // version of foundation will be at least this version. This will prevent the bug in
+ // foundation from occurring. This does _NOT_ require that the app have foundation as
+ // a dependency.
+ implementation(project(":compose:foundation:foundation")) {
+ because 'prevents a critical bug in Text'
+ }
}
}
+packageInspector(project, ":compose:ui:ui-inspection")
+
+dependencies {
+ lintChecks(project(":compose:ui:ui-lint"))
+ lintPublish(project(":compose:ui:ui-lint"))
+
+ // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+ // leaks into instrumented tests (b/214407011)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.mockitoCore4)
+ testImplementation(libs.mockitoKotlin4)
+}
+
androidx {
name = "Compose UI primitives"
type = LibraryType.PUBLISHED_LIBRARY
@@ -282,7 +224,7 @@
legacyDisableKotlinStrictApiMode = true
}
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+if (desktopEnabled) {
tasks.findByName("desktopTest").configure {
systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
}
@@ -304,24 +246,3 @@
// the androidx.compose.ui:ui-test library
testNamespace "androidx.compose.ui.tests"
}
-
-// Diagnostics for b/188565660
-def verifyKotlinModule(String variant) {
- project.afterEvaluate {
- def capitalVariant = variant.capitalize()
- def moduleFile = new File("${buildDir}/tmp/kotlin-classes/${variant}/META-INF/ui_${variant}.kotlin_module")
- tasks.named("compile${capitalVariant}Kotlin").configure { t ->
- t.doLast {
- // This file should be large, about 3.2K. If this file is short then many symbols will fail to resolve
- if (moduleFile.length() < 250) {
- throw new GradleException("kotlin_module file ($moduleFile) too short! See b/188565660 for more information. File text: ${moduleFile.text}")
- }
- }
- }
- }
-}
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- for (variant in ["debug", "release"]) {
- verifyKotlinModule(variant)
- }
-}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
index 343c89d..6d4e513 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
@@ -40,14 +40,17 @@
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -209,3 +212,53 @@
}
}
}
+
+@Sampled
+@Composable
+fun animateContentSizeAfterLookaheadPass() {
+ var sizeAnim by remember {
+ mutableStateOf<Animatable<IntSize, AnimationVector2D>?>(null)
+ }
+ var lookaheadSize by remember {
+ mutableStateOf<IntSize?>(null)
+ }
+ val coroutineScope = rememberCoroutineScope()
+ LookaheadScope {
+ // The Box is in a LookaheadScope. This means there will be a lookahead measure pass
+ // before the main measure pass.
+ // Here we are creating something similar to the `animateContentSize` modifier.
+ Box(
+ Modifier
+ .clipToBounds()
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+
+ val measuredSize = IntSize(placeable.width, placeable.height)
+ val (width, height) = if (isLookingAhead) {
+ // Record lookahead size if we are in lookahead pass. This lookahead size
+ // will be used for size animation, such that the main measure pass will
+ // gradually change size until it reaches the lookahead size.
+ lookaheadSize = measuredSize
+ measuredSize
+ } else {
+ // Since we are in an explicit lookaheadScope, we know the lookahead pass
+ // is guaranteed to happen, therefore the lookahead size that we recorded is
+ // not null.
+ val target = requireNotNull(lookaheadSize)
+ val anim = sizeAnim?.also {
+ coroutineScope.launch { it.animateTo(target) }
+ } ?: Animatable(target, IntSize.VectorConverter)
+ sizeAnim = anim
+ // By returning the animated size only during main pass, we are allowing
+ // lookahead pass to see the future layout past the animation.
+ anim.value
+ }
+
+ layout(width, height) {
+ placeable.place(0, 0)
+ }
+ }) {
+ // Some content that changes size
+ }
+ }
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
index a61a41e..020c4b9 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
@@ -51,7 +51,7 @@
import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.node.requireLayoutDirection
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.unit.IntSize
@@ -155,13 +155,12 @@
gesture.onCancelPointerInput()
}
- override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
- .apply {
- onClick {
- gesture.onTap()
- true
- }
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ onClick {
+ gesture.onTap()
+ true
}
+ }
}
}
@@ -184,13 +183,12 @@
}
class TapSemanticsNode(var onTap: () -> Unit) : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
- .apply {
- onClick {
- onTap()
- true
- }
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ onClick {
+ onTap()
+ true
}
+ }
}
class TapGestureWithClickSemantics(onTap: () -> Unit) : DelegatingNode() {
var onTap: () -> Unit
@@ -250,13 +248,12 @@
}
class TapSemanticsNode(var onTap: () -> Unit) : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
- .apply {
- onClick {
- onTap()
- true
- }
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ onClick {
+ onTap()
+ true
}
+ }
}
class TapGestureWithClickSemantics(onTap: () -> Unit) : DelegatingNode() {
var onTap: () -> Unit
@@ -340,10 +337,9 @@
@Composable
fun SemanticsModifierNodeSample() {
class HeadingNode : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
- .apply {
- heading()
- }
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ heading()
+ }
}
val HeadingElement = object : ModifierNodeElement<HeadingNode>() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 4e95ffd..337f866 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -29,15 +29,44 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.autofill.Autofill
+import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusOwner
+import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.RenderEffect
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.pointer.PointerIconService
+import androidx.compose.ui.modifier.ModifierLocalManager
+import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.LayoutNodeDrawScope
+import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.AccessibilityManager
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.SemanticsNodeCopy
+import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToMap
+import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.EmptySemanticsElement
import androidx.compose.ui.semantics.LiveRegionMode
@@ -78,11 +107,23 @@
import androidx.compose.ui.semantics.text
import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.PlatformTextInputPluginRegistry
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.toOffset
import androidx.core.view.ViewCompat
import androidx.core.view.ViewStructureCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -106,6 +147,7 @@
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -1593,6 +1635,7 @@
accessibilityDelegate.sendSemanticsPropertyChangeEvents(mapOf(nodeId to newTextNode))
}
+ @OptIn(InternalTestApi::class)
private fun createSemanticsNodeWithProperties(
id: Int,
mergeDescendants: Boolean,
@@ -1602,6 +1645,7 @@
layoutNode.modifier = Modifier.semantics(mergeDescendants) {
properties()
}
+ layoutNode.attach(MockOwner())
return SemanticsNode(layoutNode, true)
}
@@ -1615,6 +1659,7 @@
layoutNode.modifier = Modifier.semantics {
properties()
}
+ layoutNode.attach(MockOwner())
return SemanticsNode(layoutNode, true)
}
@@ -1641,3 +1686,228 @@
return false
}
}
+
+internal class MockOwner(
+ private val position: IntOffset = IntOffset.Zero,
+ override val root: LayoutNode = LayoutNode(),
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+) : Owner {
+ val onRequestMeasureParams = mutableListOf<LayoutNode>()
+ val onAttachParams = mutableListOf<LayoutNode>()
+ val onDetachParams = mutableListOf<LayoutNode>()
+ var layoutChangeCount = 0
+
+ override val rootForTest: RootForTest
+ get() = TODO("Not yet implemented")
+ override val hapticFeedBack: HapticFeedback
+ get() = TODO("Not yet implemented")
+ override val inputModeManager: InputModeManager
+ get() = TODO("Not yet implemented")
+ override val clipboardManager: ClipboardManager
+ get() = TODO("Not yet implemented")
+ override val accessibilityManager: AccessibilityManager
+ get() = TODO("Not yet implemented")
+ override val textToolbar: TextToolbar
+ get() = TODO("Not yet implemented")
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override val autofillTree: AutofillTree
+ get() = TODO("Not yet implemented")
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override val autofill: Autofill?
+ get() = TODO("Not yet implemented")
+ override val density: Density
+ get() = Density(1f)
+ override val textInputService: TextInputService
+ get() = TODO("Not yet implemented")
+ @OptIn(ExperimentalTextApi::class)
+ override val platformTextInputPluginRegistry: PlatformTextInputPluginRegistry
+ get() = TODO("Not yet implemented")
+ override val pointerIconService: PointerIconService
+ get() = TODO("Not yet implemented")
+ override val focusOwner: FocusOwner
+ get() = TODO("Not yet implemented")
+ override val windowInfo: WindowInfo
+ get() = TODO("Not yet implemented")
+
+ @Deprecated(
+ "fontLoader is deprecated, use fontFamilyResolver",
+ replaceWith = ReplaceWith("fontFamilyResolver")
+ )
+ @Suppress("DEPRECATION")
+ override val fontLoader: Font.ResourceLoader
+ get() = TODO("Not yet implemented")
+ override val fontFamilyResolver: FontFamily.Resolver
+ get() = TODO("Not yet implemented")
+ override val layoutDirection: LayoutDirection
+ get() = LayoutDirection.Ltr
+ @InternalCoreApi
+ override var showLayoutBounds: Boolean = false
+ override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
+ override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
+
+ override fun onRequestMeasure(
+ layoutNode: LayoutNode,
+ affectsLookahead: Boolean,
+ forceRequest: Boolean,
+ scheduleMeasureAndLayout: Boolean
+ ) {
+ onRequestMeasureParams += layoutNode
+ if (affectsLookahead) {
+ layoutNode.markLookaheadMeasurePending()
+ }
+ layoutNode.markMeasurePending()
+ }
+
+ override fun onRequestRelayout(
+ layoutNode: LayoutNode,
+ affectsLookahead: Boolean,
+ forceRequest: Boolean
+ ) {
+ if (affectsLookahead) {
+ layoutNode.markLookaheadLayoutPending()
+ }
+ layoutNode.markLayoutPending()
+ }
+
+ override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
+ }
+
+ override fun onAttach(node: LayoutNode) {
+ onAttachParams += node
+ }
+
+ override fun onDetach(node: LayoutNode) {
+ onDetachParams += node
+ }
+
+ override fun calculatePositionInWindow(localPosition: Offset): Offset =
+ localPosition + position.toOffset()
+
+ override fun calculateLocalPosition(positionInWindow: Offset): Offset =
+ positionInWindow - position.toOffset()
+
+ override fun requestFocus(): Boolean = false
+
+ override fun measureAndLayout(sendPointerUpdate: Boolean) {
+ }
+
+ override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
+ }
+
+ override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
+ }
+
+ override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
+ listener()
+ }
+
+ override fun onEndApplyChanges() {
+ }
+
+ override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
+ TODO("Not yet implemented")
+ }
+
+ val invalidatedLayers = mutableListOf<OwnedLayer>()
+
+ override fun createLayer(
+ drawBlock: (Canvas) -> Unit,
+ invalidateParentLayer: () -> Unit
+ ): OwnedLayer {
+ val transform = Matrix()
+ val inverseTransform = Matrix()
+ return object : OwnedLayer {
+ override fun updateLayerProperties(
+ scaleX: Float,
+ scaleY: Float,
+ alpha: Float,
+ translationX: Float,
+ translationY: Float,
+ shadowElevation: Float,
+ rotationX: Float,
+ rotationY: Float,
+ rotationZ: Float,
+ cameraDistance: Float,
+ transformOrigin: TransformOrigin,
+ shape: Shape,
+ clip: Boolean,
+ renderEffect: RenderEffect?,
+ ambientShadowColor: Color,
+ spotShadowColor: Color,
+ compositingStrategy: CompositingStrategy,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ) {
+ transform.reset()
+ // This is not expected to be 100% accurate
+ transform.scale(scaleX, scaleY)
+ transform.rotateZ(rotationZ)
+ transform.translate(translationX, translationY)
+ transform.invertTo(inverseTransform)
+ }
+
+ override fun isInLayer(position: Offset) = true
+
+ override fun move(position: IntOffset) {
+ }
+
+ override fun resize(size: IntSize) {
+ }
+
+ override fun drawLayer(canvas: Canvas) {
+ drawBlock(canvas)
+ }
+
+ override fun updateDisplayList() {
+ }
+
+ override fun invalidate() {
+ invalidatedLayers.add(this)
+ }
+
+ override fun destroy() {
+ }
+
+ override fun mapBounds(rect: MutableRect, inverse: Boolean) {
+ }
+
+ override fun reuseLayer(
+ drawBlock: (Canvas) -> Unit,
+ invalidateParentLayer: () -> Unit
+ ) {
+ }
+
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(transform)
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ matrix.timesAssign(inverseTransform)
+ }
+
+ override fun mapOffset(point: Offset, inverse: Boolean) = point
+ }
+ }
+
+ var semanticsChanged: Boolean = false
+ override fun onSemanticsChange() {
+ semanticsChanged = true
+ }
+
+ override fun onLayoutChange(layoutNode: LayoutNode) {
+ layoutChangeCount++
+ }
+
+ override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
+ TODO("Not yet implemented")
+ }
+
+ override var measureIteration: Long = 0
+ override val viewConfiguration: ViewConfiguration
+ get() = TODO("Not yet implemented")
+
+ override val sharedDrawScope = LayoutNodeDrawScope()
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 32fd60c..f761c28 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -4405,38 +4405,3 @@
)
internal typealias PointerEventHandler = (PointerEvent, PointerEventPass, IntSize) -> Unit
-
-private fun PointerEventHandler.invokeOverAllPasses(
- pointerEvent: PointerEvent,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- invokeOverPasses(
- pointerEvent,
- listOf(
- PointerEventPass.Initial,
- PointerEventPass.Main,
- PointerEventPass.Final
- ),
- size = size
- )
-}
-
-private fun PointerEventHandler.invokeOverPasses(
- pointerEvent: PointerEvent,
- vararg pointerEventPasses: PointerEventPass,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- invokeOverPasses(pointerEvent, pointerEventPasses.toList(), size)
-}
-
-private fun PointerEventHandler.invokeOverPasses(
- pointerEvent: PointerEvent,
- pointerEventPasses: List<PointerEventPass>,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- require(pointerEvent.changes.isNotEmpty())
- require(pointerEventPasses.isNotEmpty())
- pointerEventPasses.forEach {
- this.invoke(pointerEvent, it, size)
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
new file mode 100644
index 0000000..ddf5536
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.pointer
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.IntSize
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SuspendingPointerInputFilterCoroutineJobTest {
+ @OptIn(ExperimentalTestApi::class)
+ @get:Rule
+ val rule = createComposeRule(Dispatchers.Main)
+
+ @Test
+ @LargeTest
+ fun isPointerInputJobStillActive_cancelPointerEvent_assertsTrue() {
+ val latch = CountDownLatch(1)
+
+ var repeatActualNumber = 0
+ val repeatExpectedNumber = 4
+
+ // To create Pointer Events:
+ val emitter = PointerInputChangeEmitter()
+ val change = emitter.nextChange(Offset(5f, 5f))
+
+ // Used to manually trigger a PointerEvent created from our PointerInputChange.
+ val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
+ coroutineScope {
+ awaitEachGesture {
+ // First down event (triggered)
+ awaitFirstDown()
+ // Up event won't ever come since we never trigger the event, times out as
+ // a cancellation
+ waitForUpOrCancellation()
+
+ // By running this code after down/up, we are making sure the block of code
+ // for handling pointer input events is still active despite the cancel pointer
+ // input being called later.
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ repeat(repeatExpectedNumber) {
+ repeatActualNumber++
+ delay(100)
+ }
+ latch.countDown()
+ }
+ }
+ }
+ }
+
+ rule.setContent {
+ Box(
+ modifier = elementFor(
+ key1 = Unit,
+ instance = suspendingPointerInputModifierNode as Modifier.Node
+ )
+ )
+ }
+
+ rule.runOnIdle {
+ suspendingPointerInputModifierNode.onPointerEvent(
+ change.toPointerEvent(),
+ PointerEventPass.Main,
+ IntSize(10, 10)
+ )
+ }
+
+ suspendingPointerInputModifierNode.onCancelPointerInput()
+
+ val resultOfLatch = latch.await(3000, TimeUnit.MILLISECONDS)
+ assertTrue("Waiting for coroutine's tasks to finish", resultOfLatch)
+ assertEquals(repeatExpectedNumber, repeatActualNumber)
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index 1d3593d..c521919 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -24,10 +24,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.ValueElement
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.elementFor
@@ -273,7 +270,7 @@
)
}
- // Triggers cancel event
+ // Manually cancels the current pointer input event.
suspendingPointerInputModifierNode.onCancelPointerInput()
}
@@ -305,15 +302,16 @@
var currentEventAtEnd: PointerEvent? = null
val results = Channel<PointerEvent>(Channel.UNLIMITED)
- // Used to manually trigger a PointerEvent(s) created from our PointerInputChange(s).
+ // Used to manually trigger/test PointerEvents and other functionality.
val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
awaitPointerEventScope {
try {
// NOTE: This will never trigger 3 times. There are only two events
- // triggered followed by a onCancelPointerInput() call which doesn't trigger
- // an event because the previous event has down (press) set to false, so we
- // will always get an exception thrown with the last repeat's timeout
- // (we expect this).
+ // triggered (a down [press] event followed by an event with down [press] set to
+ // false). The resetPointerInputHandler() is called after that, but it won't
+ // trigger a cancel event if the previous event has the down [press] set to
+ // false. In this case, it does, so there are only ever two events, so the
+ // withTimeout() will trigger the timeout (expected).
repeat(3) {
withTimeout(200) {
results.trySend(awaitPointerEvent())
@@ -334,7 +332,6 @@
)
)
}
-
val bounds = IntSize(50, 50)
val emitter1 = PointerInputChangeEmitter(0)
val emitter2 = PointerInputChangeEmitter(1)
@@ -378,9 +375,9 @@
)
}
- // Manually triggers cancel event.
- // Note: This will not trigger an event in the customPointerInput block because the
- // previous events don't have any pressed pointers.
+ // Manually cancels the current pointer input event.
+ // Note: This will not trigger an event in the customPointerInput block because of the
+ // reasons listed above (previous event has down (press) set to false).
suspendingPointerInputModifierNode.onCancelPointerInput()
}
@@ -411,35 +408,48 @@
@Test
@MediumTest
- fun testCancelledHandlerBlock() {
+ fun testCancelPointerInput() {
+ val firstEventCheckpointInfo =
+ Pair(3, "First pointer event triggered to create Job.")
+ val cancelEventCheckpointInfo =
+ Pair(5, "Cancel pointer event triggered (shouldn't cancel Job).")
+ val invalidEventCheckpointNumber =
+ Pair(-1, "Should never execute.")
+
val counter = TestCounter()
- // Used to manually trigger a PointerEvent(s) created from our PointerInputChange(s).
+ // Used to manually trigger/test PointerEvents and other functionality.
val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
try {
awaitPointerEventScope {
try {
- counter.expect(3, "about to call awaitPointerEvent")
+ counter.expect(2, "Before awaitPointerEvent() call.")
- // With only one event triggered, this will stay stuck in the repeat
- // block until the Job is cancelled via
- // SuspendPointerInputModifierNode.resetHandling()
- repeat(2) {
+ // With only two events triggered (press and cancel), this will stay stuck
+ // in the repeat block until it is torn down, that is, until the
+ // test is over.
+ repeat(3) { repeatCount ->
awaitPointerEvent()
- counter.expect(
- 4,
- "One and only pointer event triggered to create Job."
- )
+ val checkpointInfo = when (repeatCount) {
+ 0 -> { firstEventCheckpointInfo }
+ 1 -> { cancelEventCheckpointInfo }
+ else -> {
+ fail("Should never be three events.")
+ invalidEventCheckpointNumber
+ }
+ }
+ counter.expect(checkpointInfo.first, checkpointInfo.second)
}
-
- fail("awaitPointerEvent returned; should have thrown for cancel")
+ fail("awaitPointerEvent() run 3+ times in repeat() block, should only " +
+ "have run twice (one event, one cancel).")
} finally {
- counter.expect(6, "inner finally block running")
+ counter.expect(7, "Inner finally block runs after " +
+ "teardown.")
}
}
} finally {
- counter.expect(7, "outer finally block running; inner " +
- "finally should have run")
+ counter.expect(8, "Outer finally block runs; inner finally " +
+ "block should have already run.")
}
}
@@ -457,16 +467,13 @@
val singleEventBounds = IntSize(20, 20)
rule.runOnIdle {
+ // Because the pointer input handler is triggered lazily in
+ // SuspendPointerInputModifierNode, it will not be triggered until the first event
+ // comes in, so this will be the first counter checkpoint.
counter.expect(
1,
- "Job to handle pointer input not created yet; awaitPointerEvent should " +
- "be suspended"
- )
-
- counter.expect(
- 2,
- "Trigger pointer input event to create Job for handing handle pointer" +
- " input (done lazily in SuspendPointerInputModifierNode)."
+ "Trigger pointer input handler through first pointer input event " +
+ "(handler triggered lazily)."
)
suspendingPointerInputModifierNode.onPointerEvent(
@@ -475,12 +482,96 @@
singleEventBounds
)
- counter.expect(5, "before cancelling handler; awaitPointerEvent " +
+ counter.expect(4, "Before onCancelPointerInput() handler; awaitPointerEvent " +
"should be suspended")
- // Cancels Job that manages pointer input events in SuspendPointerInputModifierNode.
+ // Manually cancels the current pointer input event.
+ suspendingPointerInputModifierNode.onCancelPointerInput()
+ counter.expect(6, "After onCancelPointerInput(), end of test, " +
+ " start teardown.")
+ }
+ }
+
+ @Test
+ @MediumTest
+ fun testResetHandlerBlock() {
+ val counter = TestCounter()
+
+ // Used to manually trigger/test PointerEvents and other functionality.
+ val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
+ try {
+ awaitPointerEventScope {
+ try {
+ counter.expect(2, "Before awaitPointerEvent() call.")
+
+ // With only one event triggered (press) to kick start the handler, this
+ // will stay stuck in the repeat block until it is torn down, that is,
+ // until the test is over.
+ repeat(2) { repeatCount ->
+ awaitPointerEvent()
+ when (repeatCount) {
+ 0 -> {
+ counter.expect(
+ 3,
+ "First/only pointer event triggered."
+ )
+ }
+ else -> {
+ fail("Should never be two or more events.")
+ counter.expect(-1, "Should never execute.")
+ }
+ }
+ }
+ fail("awaitPointerEvent repeated twice; should have only happened once " +
+ "and stayed suspended in repeat() waiting for a second event (that " +
+ "should never arrive).")
+ } finally {
+ fail("inner finally shouldn't call during teardown since coroutine job " +
+ "was cancelled with resetPointerInputHandler().")
+ }
+ }
+ } finally {
+ counter.expect(5, "outer finally block runs after " +
+ "resetPointerInputHandler().")
+ }
+ }
+
+ rule.setContent {
+ Box(
+ modifier = elementFor(
+ key1 = Unit,
+ instance = suspendingPointerInputModifierNode as Modifier.Node
+ )
+ )
+ }
+
+ val emitter = PointerInputChangeEmitter()
+ val singleEvent = emitter.nextChange(Offset(5f, 5f))
+ val singleEventBounds = IntSize(20, 20)
+
+ rule.runOnIdle {
+ // Because the pointer input handler is triggered lazily in
+ // SuspendPointerInputModifierNode, it will not be triggered until the first event
+ // comes in, so this will be the first counter checkpoint.
+ counter.expect(
+ 1,
+ "Trigger pointer input handler through first pointer input event " +
+ "(handler triggered lazily)."
+ )
+
+ suspendingPointerInputModifierNode.onPointerEvent(
+ singleEvent.toPointerEvent(),
+ PointerEventPass.Main,
+ singleEventBounds
+ )
+
+ counter.expect(4, "before resetPointerInputHandler(), handler should" +
+ "be suspended waiting for a second event (that never comes).")
+
+ // Cancels the pointer input handler in SuspendPointerInputModifierNode (and thus the
+ // Coroutine Job associated with it).
suspendingPointerInputModifierNode.resetPointerInputHandler()
- counter.expect(8, "after cancelling; finally blocks should have run")
+ counter.expect(6, "after resetPointerInputHandler(), end of test.")
}
}
@@ -774,72 +865,3 @@
assertThat(events).hasSize(2)
}
}
-
-private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
-
-private val PointerEvent.firstChange get() = changes.first()
-
-private class PointerInputChangeEmitter(id: Int = 0) {
- val pointerId = PointerId(id.toLong())
- var previousTime = 0L
- var previousPosition = Offset.Zero
- var previousPressed = false
-
- fun nextChange(
- position: Offset = Offset.Zero,
- down: Boolean = true,
- time: Long = 0
- ): PointerInputChange {
- return PointerInputChange(
- id = pointerId,
- time,
- position,
- down,
- previousTime,
- previousPosition,
- previousPressed,
- isInitiallyConsumed = false
- ).also {
- previousTime = time
- previousPosition = position
- previousPressed = down
- }
- }
-}
-
-private class TestCounter {
- private var count = 0
-
- fun expect(checkpoint: Int, message: String = "(no message)") {
- val expected = count + 1
- if (checkpoint != expected) {
- fail("out of order event $checkpoint, expected $expected, $message")
- }
- count = expected
- }
-}
-
-private fun elementFor(
- key1: Any? = null,
- instance: Modifier.Node
-) = object : ModifierNodeElement<Modifier.Node>() {
- override fun InspectorInfo.inspectableProperties() {
- debugInspectorInfo {
- name = "pointerInput"
- properties["key1"] = key1
- properties["instance"] = instance
- }
- }
-
- override fun create() = instance
- override fun update(node: Modifier.Node) {}
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SuspendPointerInputElement) return false
- if (key1 != other.key1) return false
- return true
- }
- override fun hashCode(): Int {
- return key1?.hashCode() ?: 0
- }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
index af27c0d..fb92e8f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
@@ -28,13 +28,17 @@
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.IntSize
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
import com.google.common.truth.Subject.Factory
import com.google.common.truth.Truth
+import org.junit.Assert
@OptIn(ExperimentalComposeUiApi::class)
internal fun PointerInputEventData(
@@ -555,3 +559,73 @@
type = this.type,
scrollDelta = this.scrollDelta
)
+
+// SuspendingPointerInputFilter test utilities
+internal fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
+
+internal val PointerEvent.firstChange get() = changes.first()
+
+internal class PointerInputChangeEmitter(id: Int = 0) {
+ val pointerId = PointerId(id.toLong())
+ var previousTime = 0L
+ var previousPosition = Offset.Zero
+ var previousPressed = false
+
+ fun nextChange(
+ position: Offset = Offset.Zero,
+ down: Boolean = true,
+ time: Long = 0
+ ): PointerInputChange {
+ return PointerInputChange(
+ id = pointerId,
+ time,
+ position,
+ down,
+ previousTime,
+ previousPosition,
+ previousPressed,
+ isInitiallyConsumed = false
+ ).also {
+ previousTime = time
+ previousPosition = position
+ previousPressed = down
+ }
+ }
+}
+
+internal class TestCounter {
+ private var count = 0
+
+ fun expect(checkpoint: Int, message: String = "(no message)") {
+ val expected = count + 1
+ if (checkpoint != expected) {
+ Assert.fail("out of order event $checkpoint, expected $expected, $message")
+ }
+ count = expected
+ }
+}
+
+internal fun elementFor(
+ key1: Any? = null,
+ instance: Modifier.Node
+) = object : ModifierNodeElement<Modifier.Node>() {
+ override fun InspectorInfo.inspectableProperties() {
+ debugInspectorInfo {
+ name = "pointerInput"
+ properties["key1"] = key1
+ properties["instance"] = instance
+ }
+ }
+
+ override fun create() = instance
+ override fun update(node: Modifier.Node) {}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SuspendPointerInputElement) return false
+ if (key1 != other.key1) return false
+ return true
+ }
+ override fun hashCode(): Int {
+ return key1?.hashCode() ?: 0
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 302768a..378d5b7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -65,8 +65,11 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.Constraints
@@ -83,6 +86,7 @@
import androidx.test.filters.MediumTest
import java.lang.Integer.max
import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlin.math.roundToInt
import kotlin.random.Random
@@ -1928,6 +1932,218 @@
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun testIsLookingAhead() {
+ var iterations by mutableStateOf(0)
+ val size = mutableMapOf<Boolean, IntSize>()
+ rule.setContent {
+ Box(Modifier.fillMaxSize()) {
+ LookaheadScope {
+ // Fill max size will cause the remeasure requests to go down the
+ // forceMeasureSubtree code path.
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Column(Modifier.fillMaxSize()) {
+ // This box will get a remeasure request when `iterations` changes.
+ // Subsequently this Box's size change will trigger a measurement pass
+ // from Column.
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ size[isLookingAhead] = IntSize(width, height)
+ layout(width, height) {
+ place(0, 0)
+ }
+ }
+ }
+ .intermediateLayout { measurable, _ ->
+ // Force a state-read (similar to animation but more
+ // reliable)
+ measurable
+ .measure(Constraints.fixed(200 + 100 * iterations, 200))
+ .run {
+ layout(width, height) {
+ place(0, 0)
+ }
+ }
+ }) {
+ Box(Modifier.size(100.dp))
+ }
+ }
+ SubcomposeLayout(
+ Modifier
+ .fillMaxSize()
+ .requiredSize(200.dp),
+ intermediateMeasurePolicy = { constraints ->
+ assertFalse(isLookingAhead)
+ measurablesForSlot(Unit)[0].measure(constraints)
+ layout(0, 0) {}
+ }
+ ) { constraints ->
+ assertTrue(isLookingAhead)
+ val placeable = subcompose(Unit) {
+ Box(Modifier.requiredSize(400.dp, 600.dp))
+ }[0].measure(constraints)
+ layout(500, 300) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ rule.runOnIdle {
+ assertEquals(IntSize(100, 100), size[true])
+ assertEquals(IntSize(200 + 100 * it, 200), size[false])
+ iterations++
+ }
+ }
+ }
+
+ class TestLayoutModifierNode(
+ var lookaheadIntrinsicResult: MutableMap<String, Int>,
+ var intrinsicResult: MutableMap<String, Int>
+ ) : LayoutModifierNode, Modifier.Node() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ return measurable.measure(constraints).run {
+ layout(width, height) {
+ place(0, 0)
+ }
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = measurable.maxIntrinsicHeight(width).also {
+ if (isLookingAhead) {
+ lookaheadIntrinsicResult["maxHeight"] = it
+ } else {
+ intrinsicResult["maxHeight"] = it
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ): Int = measurable.minIntrinsicHeight(width).also {
+ if (isLookingAhead) {
+ lookaheadIntrinsicResult["minHeight"] = it
+ } else {
+ intrinsicResult["minHeight"] = it
+ }
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = measurable.maxIntrinsicWidth(height).also {
+ if (isLookingAhead) {
+ lookaheadIntrinsicResult["maxWidth"] = it
+ } else {
+ intrinsicResult["maxWidth"] = it
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ): Int = measurable.minIntrinsicWidth(height).also {
+ if (isLookingAhead) {
+ lookaheadIntrinsicResult["minWidth"] = it
+ } else {
+ intrinsicResult["minWidth"] = it
+ }
+ }
+ }
+
+ data class TestElement(
+ val lookaheadIntrinsicResult: MutableMap<String, Int>,
+ val intrinsicResult: MutableMap<String, Int>
+ ) : ModifierNodeElement<TestLayoutModifierNode>() {
+ override fun create(): TestLayoutModifierNode =
+ TestLayoutModifierNode(lookaheadIntrinsicResult, intrinsicResult)
+
+ override fun update(node: TestLayoutModifierNode) {
+ node.lookaheadIntrinsicResult = lookaheadIntrinsicResult
+ node.intrinsicResult = intrinsicResult
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "TestElement"
+ properties["lookaheadIntrinsicResult"] = lookaheadIntrinsicResult
+ properties["intrinsicResult"] = intrinsicResult
+ }
+ }
+
+ @Test
+ fun testIsLookingAheadWithIntrinsics() {
+ val lookaheadIntrinsicsResult = mutableMapOf<String, Int>()
+ val intrinsicsResult = mutableMapOf<String, Int>()
+ val modifierList = listOf(
+ Modifier.width(IntrinsicSize.Max),
+ Modifier.width(IntrinsicSize.Min),
+ Modifier.height(IntrinsicSize.Max),
+ Modifier.height(IntrinsicSize.Min),
+ )
+ var iteration by mutableStateOf(0)
+ rule.setContent {
+ LookaheadScope {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Row(Modifier.width(IntrinsicSize.Max)) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .then(modifierList[iteration])
+ .then(
+ TestElement(
+ lookaheadIntrinsicsResult, intrinsicsResult
+ )
+ )
+ .layout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ if (isLookingAhead) {
+ layout(200, 250) {
+ place(0, 0)
+ }
+ } else {
+ layout(100, 150) {
+ place(0, 0)
+ }
+ }
+ }
+ }) {
+ Box(Modifier.size(10.dp))
+ }
+ }
+ }
+ }
+ }
+ repeat(3) {
+ rule.waitForIdle()
+ iteration++
+ }
+ rule.runOnIdle {
+ assertEquals(250, lookaheadIntrinsicsResult["maxHeight"])
+ assertEquals(250, lookaheadIntrinsicsResult["minHeight"])
+ assertEquals(200, lookaheadIntrinsicsResult["maxWidth"])
+ assertEquals(200, lookaheadIntrinsicsResult["minWidth"])
+ assertEquals(150, intrinsicsResult["maxHeight"])
+ assertEquals(150, intrinsicsResult["minHeight"])
+ assertEquals(100, intrinsicsResult["maxWidth"])
+ assertEquals(100, intrinsicsResult["minWidth"])
+ }
+ }
+
@Test
fun forceMeasureSubtreeWhileLookaheadMeasureRequestedFromSubtree() {
var iterations by mutableStateOf(0)
@@ -1960,13 +2176,17 @@
if (iterations % 2 == 0)
Modifier.size(100.dp)
else
- Modifier.intermediateLayout { measurable, constraints ->
- measurable.measure(constraints).run {
- layout(width, height) {
- place(5, 5)
- }
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ place(5, 5)
+ }
+ }
}
- }.padding(5.dp)
+ .padding(5.dp)
)
}
}
@@ -2099,6 +2319,7 @@
with(scope) {
this@composed
.intermediateLayout { measurable, constraints ->
+ assertFalse(isLookingAhead)
lookaheadSize = this.lookaheadSize
measureWithLambdas(
prePlacement = {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
index 5575793..e550f57 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
@@ -32,10 +32,18 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.test.fail
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
@@ -174,4 +182,60 @@
}
)
}
+
+ /**
+ * Test that an AndroidUiDispatcher can be wrapped by another ContinuationInterceptor
+ * without breaking the MonotonicFrameClock's ability to coordinate with its
+ * original dispatcher.
+ *
+ * Construct a situation where the Choreographer contains three frame callbacks:
+ * 1) checkpoint 1
+ * 2) the AndroidUiDispatcher awaiting-frame callback
+ * 3) checkpoint 2
+ * Confirm that a call to withFrameNanos made *after* these three frame callbacks
+ * are enqueued runs *before* checkpoint 2, indicating that it ran with the original
+ * dispatcher's awaiting-frame callback, even though we wrapped the dispatcher.
+ */
+ @Test
+ fun wrappedDispatcherPostsToDispatcherFrameClock() = runBlocking(Dispatchers.Main) {
+ val uiDispatcherContext = AndroidUiDispatcher.Main
+ val uiDispatcher = uiDispatcherContext[ContinuationInterceptor] as CoroutineDispatcher
+ val wrapperDispatcher = object : CoroutineDispatcher() {
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ uiDispatcher.dispatch(context, block)
+ }
+ }
+
+ val choreographer = Choreographer.getInstance()
+
+ val expectCount = AtomicInteger(1)
+ fun expect(value: Int) {
+ while (true) {
+ val old = expectCount.get()
+ if (old != value) fail("expected sequence $old but encountered $value")
+ if (expectCount.compareAndSet(value, value + 1)) break
+ }
+ }
+
+ choreographer.postFrameCallback {
+ expect(1)
+ }
+
+ launch(uiDispatcherContext, start = CoroutineStart.UNDISPATCHED) {
+ withFrameNanos {
+ expect(2)
+ }
+ }
+
+ choreographer.postFrameCallback {
+ expect(4)
+ }
+
+ withContext(uiDispatcherContext + wrapperDispatcher) {
+ withFrameNanos {
+ expect(3)
+ }
+ expect(5)
+ }
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 5731018..2c1d8dd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -106,22 +106,6 @@
}
@Test
- fun valueSemanticsAreEqual() {
- assertEquals(
- Modifier.semantics {
- text = AnnotatedString("text")
- contentDescription = "foo"
- popup()
- },
- Modifier.semantics {
- text = AnnotatedString("text")
- contentDescription = "foo"
- popup()
- }
- )
- }
-
- @Test
fun isTraversalGroupProperty() {
rule.setContent {
Surface(
@@ -1083,10 +1067,9 @@
properties: SemanticsPropertyReceiver.() -> Unit
): CoreSemanticsModifierNode {
return CoreSemanticsModifierNode(
- SemanticsConfiguration().apply {
- isMergingSemanticsOfDescendants = mergeDescendants
- properties()
- }
+ mergeDescendants = mergeDescendants,
+ isClearingSemantics = false,
+ properties = properties,
)
}
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 497be6e..da63070 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
@@ -911,13 +911,20 @@
}
}
- private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
+ @Suppress("NOTHING_TO_INLINE")
+ private inline operator fun ULong.component1() = (this shr 32).toInt()
+ @Suppress("NOTHING_TO_INLINE")
+ private inline operator fun ULong.component2() = (this and 0xFFFFFFFFUL).toInt()
+
+ private fun pack(a: Int, b: Int) = (a.toULong() shl 32 or b.toULong())
+
+ private fun convertMeasureSpec(measureSpec: Int): ULong {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
- MeasureSpec.EXACTLY -> size to size
- MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
- MeasureSpec.AT_MOST -> 0 to size
+ MeasureSpec.EXACTLY -> pack(size, size)
+ MeasureSpec.UNSPECIFIED -> pack(0, Constraints.Infinity)
+ MeasureSpec.AT_MOST -> pack(0, size)
else -> throw IllegalStateException()
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index ad8183c..3baff06 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -74,7 +74,6 @@
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsPropertiesAndroid
-import androidx.compose.ui.semantics.collapsedSemantics
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.AnnotatedString
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
index 5be4139..7dce6f5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
@@ -131,7 +131,7 @@
* A [MonotonicFrameClock] associated with this [AndroidUiDispatcher]'s [choreographer]
* that may be used to await [Choreographer] frame dispatch.
*/
- val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer)
+ val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer, this)
override fun dispatch(context: CoroutineContext, block: Runnable) {
synchronized(lock) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
index fbc7b21..a9fd7ed 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
@@ -21,13 +21,20 @@
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.coroutineContext
-class AndroidUiFrameClock(
- val choreographer: Choreographer
+class AndroidUiFrameClock internal constructor(
+ val choreographer: Choreographer,
+ private val dispatcher: AndroidUiDispatcher?
) : androidx.compose.runtime.MonotonicFrameClock {
+
+ constructor(
+ choreographer: Choreographer
+ ) : this(choreographer, null)
+
override suspend fun <R> withFrameNanos(
onFrame: (Long) -> R
): R {
- val uiDispatcher = coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
+ val uiDispatcher = dispatcher
+ ?: coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
return suspendCancellableCoroutine { co ->
// Important: this callback won't throw, and AndroidUiDispatcher counts on it.
val callback = Choreographer.FrameCallback { frameTimeNanos ->
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
index 562f991..0e155ef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.Size.Companion.Unspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
@@ -49,12 +48,6 @@
val EmptyPath = emptyList<PathNode>()
-inline fun PathData(block: PathBuilder.() -> Unit): List<PathNode> =
- with(PathBuilder()) {
- block()
- getNodes()
- }
-
const val DefaultPathName = ""
const val DefaultStrokeLineWidth = 0.0f
const val DefaultStrokeLineMiter = 4.0f
@@ -68,15 +61,18 @@
val DefaultTintColor = Color.Transparent
val DefaultFillType = PathFillType.NonZero
-fun addPathNodes(pathStr: String?): List<PathNode> =
- if (pathStr == null) {
- EmptyPath
- } else {
- PathParser().parsePathString(pathStr).toNodes()
- }
+inline fun PathData(block: PathBuilder.() -> Unit) = with(PathBuilder()) {
+ block()
+ getNodes()
+}
+
+fun addPathNodes(pathStr: String?) = if (pathStr == null) {
+ EmptyPath
+} else {
+ PathParser().parsePathString(pathStr).toNodes()
+}
sealed class VNode {
-
/**
* Callback invoked whenever the node in the vector tree is modified in a way that would
* change the output of the Vector
@@ -91,7 +87,6 @@
}
internal class VectorComponent : VNode() {
-
val root = GroupComponent().apply {
pivotX = 0.0f
pivotY = 0.0f
@@ -111,7 +106,7 @@
invalidateCallback.invoke()
}
- private var isDirty: Boolean = true
+ private var isDirty = true
private val cacheDrawScope = DrawCache()
@@ -119,7 +114,7 @@
internal var intrinsicColorFilter: ColorFilter? by mutableStateOf(null)
- var viewportWidth: Float = 0f
+ var viewportWidth = 0f
set(value) {
if (field != value) {
field = value
@@ -127,7 +122,7 @@
}
}
- var viewportHeight: Float = 0f
+ var viewportHeight = 0f
set(value) {
if (field != value) {
field = value
@@ -135,7 +130,7 @@
}
}
- private var previousDrawSize: Size = Unspecified
+ private var previousDrawSize = Unspecified
/**
* Cached lambda used to avoid allocating the lambda on each draw invocation
@@ -178,8 +173,7 @@
}
internal class PathComponent : VNode() {
-
- var name: String = DefaultPathName
+ var name = DefaultPathName
set(value) {
field = value
invalidate()
@@ -191,33 +185,33 @@
invalidate()
}
- var fillAlpha: Float = 1.0f
+ var fillAlpha = 1.0f
set(value) {
field = value
invalidate()
}
- var pathData: List<PathNode> = EmptyPath
+ var pathData = EmptyPath
set(value) {
field = value
isPathDirty = true
invalidate()
}
- var pathFillType: PathFillType = DefaultFillType
+ var pathFillType = DefaultFillType
set(value) {
field = value
renderPath.fillType = value
invalidate()
}
- var strokeAlpha: Float = 1.0f
+ var strokeAlpha = 1.0f
set(value) {
field = value
invalidate()
}
- var strokeLineWidth: Float = DefaultStrokeLineWidth
+ var strokeLineWidth = DefaultStrokeLineWidth
set(value) {
field = value
invalidate()
@@ -229,28 +223,28 @@
invalidate()
}
- var strokeLineCap: StrokeCap = DefaultStrokeLineCap
+ var strokeLineCap = DefaultStrokeLineCap
set(value) {
field = value
isStrokeDirty = true
invalidate()
}
- var strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin
+ var strokeLineJoin = DefaultStrokeLineJoin
set(value) {
field = value
isStrokeDirty = true
invalidate()
}
- var strokeLineMiter: Float = DefaultStrokeLineMiter
+ var strokeLineMiter = DefaultStrokeLineMiter
set(value) {
field = value
isStrokeDirty = true
invalidate()
}
- var trimPathStart: Float = DefaultTrimPathStart
+ var trimPathStart = DefaultTrimPathStart
set(value) {
if (field != value) {
field = value
@@ -259,7 +253,7 @@
}
}
- var trimPathEnd: Float = DefaultTrimPathEnd
+ var trimPathEnd = DefaultTrimPathEnd
set(value) {
if (field != value) {
field = value
@@ -268,7 +262,7 @@
}
}
- var trimPathOffset: Float = DefaultTrimPathOffset
+ var trimPathOffset = DefaultTrimPathOffset
set(value) {
if (field != value) {
field = value
@@ -279,13 +273,12 @@
private var isPathDirty = true
private var isStrokeDirty = true
- private var isTrimPathDirty = true
+ private var isTrimPathDirty = false
private var strokeStyle: Stroke? = null
private val path = Path()
-
- private val renderPath = Path()
+ private var renderPath = path
private val pathMeasure: PathMeasure by lazy(LazyThreadSafetyMode.NONE) { PathMeasure() }
@@ -296,14 +289,18 @@
}
private fun updateRenderPath() {
- // Rewind unsets the filltype so reset it here
- val fillType = renderPath.fillType
- renderPath.rewind()
- renderPath.fillType = fillType
-
if (trimPathStart == DefaultTrimPathStart && trimPathEnd == DefaultTrimPathEnd) {
- renderPath.addPath(path)
+ renderPath = path
} else {
+ if (renderPath == path) {
+ renderPath = Path()
+ } else {
+ // Rewind unsets the fill type so reset it here
+ val fillType = renderPath.fillType
+ renderPath.rewind()
+ renderPath.fillType = fillType
+ }
+
pathMeasure.setPath(path, false)
val length = pathMeasure.length
val start = ((trimPathStart + trimPathOffset) % 1f) * length
@@ -339,18 +336,15 @@
}
}
- override fun toString(): String {
- return path.toString()
- }
+ override fun toString() = path.toString()
}
internal class GroupComponent : VNode() {
-
private var groupMatrix: Matrix? = null
private val children = mutableListOf<VNode>()
- var clipPathData: List<PathNode> = EmptyPath
+ var clipPathData = EmptyPath
set(value) {
field = value
isClipPathDirty = true
@@ -387,61 +381,64 @@
// If the name changes we should re-draw as individual nodes could
// be modified based off of this name parameter.
- var name: String = DefaultGroupName
+ var name = DefaultGroupName
set(value) {
field = value
invalidate()
}
- var rotation: Float = DefaultRotation
+ var rotation = DefaultRotation
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var pivotX: Float = DefaultPivotX
+ var pivotX = DefaultPivotX
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var pivotY: Float = DefaultPivotY
+ var pivotY = DefaultPivotY
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var scaleX: Float = DefaultScaleX
+ var scaleX = DefaultScaleX
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var scaleY: Float = DefaultScaleY
+ var scaleY = DefaultScaleY
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var translationX: Float = DefaultTranslationX
+ var translationX = DefaultTranslationX
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
- var translationY: Float = DefaultTranslationY
+ var translationY = DefaultTranslationY
set(value) {
field = value
isMatrixDirty = true
invalidate()
}
+ val numChildren: Int
+ get() = children.size
+
private var isMatrixDirty = true
private fun updateMatrix() {
@@ -528,9 +525,6 @@
}
}
- val numChildren: Int
- get() = children.size
-
override fun toString(): String {
val sb = StringBuilder().append("VGroup: ").append(name)
children.fastForEach { node ->
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 9a7ddac..abd9b6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -376,12 +376,21 @@
@OptIn(ExperimentalComposeUiApi::class)
for ((key, change) in changes) {
- // Filter for changes that are associated with pointer ids that are relevant to this
- // node
- if (key in pointerIds) {
+ val keyValue = key.value
+
+ // Using for (key in pointerIds) causes key to be boxed and create allocations
+ var keyInPointerIds = false
+ for (i in 0..pointerIds.lastIndex) {
+ if (pointerIds[i].value == keyValue) {
+ keyInPointerIds = true
+ break
+ }
+ }
+
+ if (keyInPointerIds) {
// And translate their position relative to the parent coordinates, to give us a
// change local to the PointerInputFilter's coordinates
- val historical = mutableListOf<HistoricalChange>()
+ val historical = ArrayList<HistoricalChange>(change.historical.size)
change.historical.fastForEach {
historical.add(
HistoricalChange(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index a3d34a1..eab488c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -497,7 +497,7 @@
override fun resetPointerInputHandler() {
val localJob = pointerInputJob
if (localJob != null) {
- localJob.cancel(CancellationException())
+ localJob.cancel()
pointerInputJob = null
}
}
@@ -602,9 +602,6 @@
dispatchPointerEvent(cancelEvent, PointerEventPass.Final)
lastPointerEvent = null
-
- // Cancels existing coroutine (Job) handling events.
- resetPointerInputHandler()
}
override suspend fun <R> awaitPointerEventScope(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
index 9b4c8f9..e0ad52d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
@@ -317,6 +317,10 @@
}
}
+ // Intermediate layout pass is post-lookahead. Therefore return false here.
+ override val isLookingAhead: Boolean
+ get() = false
+
override val layoutDirection: LayoutDirection
get() = coordinator!!.layoutDirection
override val density: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
index 5c29e6c..657e31d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.layout
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -28,4 +29,17 @@
* to measure their children.
*/
val layoutDirection: LayoutDirection
+
+ /**
+ * This indicates whether the ongoing measurement is for lookahead pass.
+ * [IntrinsicMeasureScope] implementations, especially [MeasureScope] implementations should
+ * override this flag to reflect whether the measurement is intended for lookahead pass.
+ *
+ * @sample androidx.compose.ui.samples.animateContentSizeAfterLookaheadPass
+ */
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @get:ExperimentalComposeUiApi
+ @ExperimentalComposeUiApi
+ val isLookingAhead: Boolean
+ get() = false
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index 2b45ead..813a516 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -32,7 +32,6 @@
import androidx.compose.ui.node.ComposeUiNode
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -64,7 +63,8 @@
*/
@Suppress("ComposableLambdaParameterPosition")
@UiComposable
-@Composable inline fun Layout(
+@Composable
+inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
@@ -322,6 +322,6 @@
* call.
*/
internal class IntrinsicsMeasureScope(
- density: Density,
- override val layoutDirection: LayoutDirection
-) : MeasureScope, Density by density
+ intrinsicMeasureScope: IntrinsicMeasureScope,
+ override val layoutDirection: LayoutDirection,
+) : MeasureScope, IntrinsicMeasureScope by intrinsicMeasureScope
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 6ebfd55..d95d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -1000,6 +1000,9 @@
override var layoutDirection: LayoutDirection = LayoutDirection.Rtl
override var density: Float = 0f
override var fontScale: Float = 0f
+ override val isLookingAhead: Boolean
+ get() = root.layoutState == LayoutState.LookaheadLayingOut ||
+ root.layoutState == LayoutState.LookaheadMeasuring
override fun subcompose(slotId: Any?, content: @Composable () -> Unit) =
this@LayoutNodeSubcompositionsState.subcompose(slotId, content)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index 88b94ea..533ae5f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -59,6 +59,7 @@
import androidx.compose.ui.modifier.modifierLocalMapOf
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
@@ -352,8 +353,11 @@
}
}
- override val semanticsConfiguration: SemanticsConfiguration
- get() = (element as SemanticsModifier).semanticsConfiguration
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ val config = (element as SemanticsModifier).semanticsConfiguration
+ val toMergeInto = (this as SemanticsConfiguration)
+ toMergeInto.collapsePeer(config)
+ }
override fun onPointerEvent(
pointerEvent: PointerEvent,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index e42df1f..aa215e9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.simpleIdentityToString
+import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.generateSemanticsId
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -65,6 +66,8 @@
*/
private const val DebugChanges = false
+private val DefaultDensity = Density(1f)
+
/**
* An element in the layout hierarchy, built with compose UI.
*/
@@ -392,6 +395,37 @@
invalidateMeasurements()
}
+ private var _collapsedSemantics: SemanticsConfiguration? = null
+ internal fun invalidateSemantics() {
+ _collapsedSemantics = null
+ // TODO(lmr): this ends up scheduling work that diffs the entire tree, but we should
+ // eventually move to marking just this node as invalidated since we are invalidating
+ // on a per-node level. This should preserve current behavior for now.
+ requireOwner().onSemanticsChange()
+ }
+ internal val collapsedSemantics: SemanticsConfiguration?
+ get() {
+ if (!nodes.has(Nodes.Semantics) || _collapsedSemantics != null) {
+ return _collapsedSemantics
+ }
+
+ var config = SemanticsConfiguration()
+ requireOwner().snapshotObserver.observeSemanticsReads(this) {
+ nodes.tailToHead(Nodes.Semantics) {
+ if (it.shouldClearDescendantSemantics) {
+ config = SemanticsConfiguration()
+ config.isClearingSemantics = true
+ }
+ if (it.shouldMergeDescendantSemantics) {
+ config.isMergingSemanticsOfDescendants = true
+ }
+ with(config) { with(it) { applySemantics() } }
+ }
+ }
+ _collapsedSemantics = config
+ return config
+ }
+
/**
* Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached.
* [owner] must match its [parent].[owner].
@@ -419,7 +453,7 @@
this.owner = owner
this.depth = (parent?.depth ?: -1) + 1
if (nodes.has(Nodes.Semantics)) {
- owner.onSemanticsChange()
+ invalidateSemantics()
}
owner.onAttach(this)
@@ -469,7 +503,7 @@
onDetach?.invoke(owner)
if (nodes.has(Nodes.Semantics)) {
- owner.onSemanticsChange()
+ invalidateSemantics()
}
nodes.detach()
owner.onDetach(this)
@@ -595,7 +629,7 @@
/**
* The screen density to be used by this layout.
*/
- override var density: Density = Density(1f)
+ override var density: Density = DefaultDensity
set(value) {
if (field != value) {
field = value
@@ -1139,10 +1173,8 @@
fun invalidateSubtree(isRootOfInvalidation: Boolean = true) {
if (isRootOfInvalidation) {
parent?.invalidateLayer()
- // Invalidate semantics. We can do this once because there isn't a node-by-node
- // invalidation mechanism.
- requireOwner().onSemanticsChange()
}
+ invalidateSemantics()
requestRemeasure()
nodes.headToTail(Nodes.Layout) {
it.requireCoordinator(Nodes.Layout).layer?.invalidate()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index b34b523..ba59c04 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.node
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
@@ -76,6 +77,10 @@
alignmentLinesOwner.parentAlignmentLinesOwner?.alignmentLines?.onAlignmentsChanged()
}
}
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override val isLookingAhead: Boolean
+ get() = false
}
internal abstract class LookaheadDelegate(
@@ -91,6 +96,8 @@
get() = _measureResult ?: error(
"LookaheadDelegate has not been measured yet when measureResult is requested."
)
+ override val isLookingAhead: Boolean
+ get() = true
override val layoutDirection: LayoutDirection
get() = coordinator.layoutDirection
override val density: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 2b2847f..9043a07 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -743,7 +743,8 @@
private fun Modifier.fillVector(
result: MutableVector<Modifier.Element>
): MutableVector<Modifier.Element> {
- val stack = MutableVector<Modifier>(result.size).also { it.add(this) }
+ val capacity = result.size.coerceAtLeast(16)
+ val stack = MutableVector<Modifier>(capacity).also { it.add(this) }
while (stack.isNotEmpty()) {
when (val next = stack.removeAt(stack.size - 1)) {
is CombinedModifier -> {
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 b0abc9f..6519bf8 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
@@ -39,7 +39,6 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.semantics.collapsedSemantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
index 22391a49..689bc64 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
@@ -39,6 +39,12 @@
}
}
+ private val onCommitAffectingSemantics: (LayoutNode) -> Unit = { layoutNode ->
+ if (layoutNode.isValidOwnerScope) {
+ layoutNode.invalidateSemantics()
+ }
+ }
+
private val onCommitAffectingLayout: (LayoutNode) -> Unit = { layoutNode ->
if (layoutNode.isValidOwnerScope) {
layoutNode.requestRelayout()
@@ -108,6 +114,13 @@
}
}
+ internal fun observeSemanticsReads(
+ node: LayoutNode,
+ block: () -> Unit
+ ) {
+ observeReads(node, onCommitAffectingSemantics, block)
+ }
+
/**
* Observe snapshot reads for any target, allowing consumers to determine how to respond
* to state changes.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
index 8c80e15..1cc3317 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
@@ -21,6 +21,10 @@
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.getOrNull
/**
@@ -32,31 +36,64 @@
*/
interface SemanticsModifierNode : DelegatableNode {
/**
- * The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
- * such as (label -> "buttonName").
+ * Clears the semantics of all the descendant nodes and sets new semantics.
+ *
+ * In the merged semantics tree, this clears the semantic information provided
+ * by the node's descendants (but not those of the layout node itself, if any)
+ * In the unmerged tree, the semantics node is marked with
+ * "[SemanticsConfiguration.isClearingSemantics]", but nothing is actually cleared.
+ *
+ * Compose's default semantics provide baseline usability for screen-readers, but this can be
+ * used to provide a more polished screen-reader experience: for example, clearing the
+ * semantics of a group of tiny buttons, and setting equivalent actions on the card
+ * containing them.
*/
- val semanticsConfiguration: SemanticsConfiguration
+ @get:Suppress("GetterSetterNames")
+ val shouldClearDescendantSemantics: Boolean
+ get() = false
+
+ /**
+ * Whether the semantic information provided by this node and
+ * its descendants should be treated as one logical entity.
+ * Most commonly set on screen-reader-focusable items such as buttons or form fields.
+ * In the merged semantics tree, all descendant nodes (except those themselves marked
+ * [shouldMergeDescendantSemantics]) will disappear from the tree, and their properties
+ * will get merged into the parent's configuration (using a merging algorithm that varies based
+ * on the type of property -- for example, text properties will get concatenated, separated
+ * by commas). In the unmerged semantics tree, the node is simply marked with
+ * [SemanticsConfiguration.isMergingSemanticsOfDescendants].
+ */
+ @get:Suppress("GetterSetterNames")
+ val shouldMergeDescendantSemantics: Boolean
+ get() = false
+
+ /**
+ * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
+ *
+ * The [SemanticsPropertyReceiver] provides "key = value"-style setters for any
+ * [SemanticsPropertyKey]. Additionally, chaining multiple semantics modifiers is
+ * also a supported style.
+ *
+ * The resulting semantics produce two [SemanticsNode] trees:
+ *
+ * The "unmerged tree" rooted at [SemanticsOwner.unmergedRootSemanticsNode] has one
+ * [SemanticsNode] per layout node which has any [SemanticsModifierNode] on it. This
+ * [SemanticsNode] contains all the properties set in all the [SemanticsModifierNode]s on
+ * that node.
+ *
+ * The "merged tree" rooted at [SemanticsOwner.rootSemanticsNode] has equal-or-fewer nodes: it
+ * simplifies the structure based on [shouldMergeDescendantSemantics] and
+ * [shouldClearDescendantSemantics]. For most purposes (especially accessibility, or the
+ * testing of accessibility), the merged semantics tree should be used.
+ */
+ fun SemanticsPropertyReceiver.applySemantics()
}
-fun SemanticsModifierNode.invalidateSemantics() = requireOwner().onSemanticsChange()
-
-internal val SemanticsModifierNode.useMinimumTouchTarget: Boolean
- get() = semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
+fun SemanticsModifierNode.invalidateSemantics() = requireLayoutNode().invalidateSemantics()
internal val SemanticsConfiguration.useMinimumTouchTarget: Boolean
get() = getOrNull(SemanticsActions.OnClick) != null
-internal fun SemanticsModifierNode.touchBoundsInRoot(): Rect {
- if (!node.isAttached) {
- return Rect.Zero
- }
- if (!useMinimumTouchTarget) {
- return requireCoordinator(Nodes.Semantics).boundsInRoot()
- }
-
- return requireCoordinator(Nodes.Semantics).touchBoundsInRoot()
-}
-
internal fun Modifier.Node.touchBoundsInRoot(useMinimumTouchTarget: Boolean): Rect {
if (!node.isAttached) {
return Rect.Zero
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
index 0ca7a3dd..922af3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
@@ -59,7 +59,15 @@
}
override fun <T> set(key: SemanticsPropertyKey<T>, value: T) {
- props[key] = value
+ if (value is AccessibilityAction<*> && contains(key)) {
+ val prev = props[key] as AccessibilityAction<*>
+ props[key] = AccessibilityAction(
+ value.label ?: prev.label,
+ value.action ?: prev.action
+ )
+ } else {
+ props[key] = value
+ }
}
operator fun <T> contains(key: SemanticsPropertyKey<T>): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 723d099..688113e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -47,16 +47,10 @@
}
internal object EmptySemanticsElement :
- ModifierNodeElement<CoreSemanticsModifierNode>() {
+ ModifierNodeElement<EmptySemanticsModifier>() {
+ override fun create() = EmptySemanticsModifier()
- private val semanticsConfiguration = SemanticsConfiguration().apply {
- isMergingSemanticsOfDescendants = false
- isClearingSemantics = false
- }
-
- override fun create() = CoreSemanticsModifierNode(semanticsConfiguration)
-
- override fun update(node: CoreSemanticsModifierNode) {}
+ override fun update(node: EmptySemanticsModifier) {}
override fun InspectorInfo.inspectableProperties() {
// Nothing to inspect.
@@ -67,8 +61,22 @@
}
internal class CoreSemanticsModifierNode(
- override var semanticsConfiguration: SemanticsConfiguration
-) : Modifier.Node(), SemanticsModifierNode
+ var mergeDescendants: Boolean,
+ var isClearingSemantics: Boolean,
+ var properties: SemanticsPropertyReceiver.() -> Unit
+) : Modifier.Node(), SemanticsModifierNode {
+ override val shouldClearDescendantSemantics: Boolean
+ get() = isClearingSemantics
+ override val shouldMergeDescendantSemantics: Boolean
+ get() = mergeDescendants
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ properties()
+ }
+}
+
+internal class EmptySemanticsModifier : Modifier.Node(), SemanticsModifierNode {
+ override fun SemanticsPropertyReceiver.applySemantics() {}
+}
/**
* Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
@@ -110,30 +118,33 @@
// Implement SemanticsModifier to allow tooling to inspect the semantics configuration
internal data class AppendedSemanticsElement(
- override val semanticsConfiguration: SemanticsConfiguration
+ val mergeDescendants: Boolean,
+ val properties: (SemanticsPropertyReceiver.() -> Unit)
) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
- constructor(
- mergeDescendants: Boolean,
- properties: (SemanticsPropertyReceiver.() -> Unit)
- ) : this(
- SemanticsConfiguration().apply {
+ // This should only ever be called by layout inspector
+ override val semanticsConfiguration: SemanticsConfiguration
+ get() = SemanticsConfiguration().apply {
isMergingSemanticsOfDescendants = mergeDescendants
properties()
}
- )
override fun create(): CoreSemanticsModifierNode {
- return CoreSemanticsModifierNode(semanticsConfiguration)
+ return CoreSemanticsModifierNode(
+ mergeDescendants = mergeDescendants,
+ isClearingSemantics = false,
+ properties = properties
+ )
}
override fun update(node: CoreSemanticsModifierNode) {
- node.semanticsConfiguration = semanticsConfiguration
+ node.mergeDescendants = mergeDescendants
+ node.properties = properties
}
override fun InspectorInfo.inspectableProperties() {
name = "semantics"
- properties["mergeDescendants"] = semanticsConfiguration.isMergingSemanticsOfDescendants
+ properties["mergeDescendants"] = mergeDescendants
addSemanticsPropertiesFrom(semanticsConfiguration)
}
}
@@ -159,24 +170,27 @@
// Implement SemanticsModifier to allow tooling to inspect the semantics configuration
internal data class ClearAndSetSemanticsElement(
- override val semanticsConfiguration: SemanticsConfiguration
+ val properties: SemanticsPropertyReceiver.() -> Unit
) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
- init {
- semanticsConfiguration.isMergingSemanticsOfDescendants = false
- semanticsConfiguration.isClearingSemantics = true
- }
-
- constructor(properties: (SemanticsPropertyReceiver.() -> Unit)) : this(
- SemanticsConfiguration().apply(properties)
- )
+ // This should only ever be called by layout inspector
+ override val semanticsConfiguration: SemanticsConfiguration
+ get() = SemanticsConfiguration().apply {
+ isMergingSemanticsOfDescendants = false
+ isClearingSemantics = true
+ properties()
+ }
override fun create(): CoreSemanticsModifierNode {
- return CoreSemanticsModifierNode(semanticsConfiguration)
+ return CoreSemanticsModifierNode(
+ mergeDescendants = false,
+ isClearingSemantics = true,
+ properties = properties
+ )
}
override fun update(node: CoreSemanticsModifierNode) {
- node.semanticsConfiguration = semanticsConfiguration
+ node.properties = properties
}
override fun InspectorInfo.inspectableProperties() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 41cdc4f..bb78b7e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -402,7 +402,9 @@
}
val fakeNode = SemanticsNode(
outerSemanticsNode = object : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration = configuration
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ properties()
+ }
},
mergingEnabled = false,
layoutNode = LayoutNode(
@@ -427,25 +429,9 @@
}
}
-internal val LayoutNode.collapsedSemantics: SemanticsConfiguration?
- get() {
- var result: SemanticsConfiguration? = null
- nodes.tailToHead(Nodes.Semantics) {
- val current = result
- if (current == null || it.semanticsConfiguration.isClearingSemantics) {
- result = it.semanticsConfiguration
- } else {
- result = it.semanticsConfiguration.copy().also {
- it.collapsePeer(current)
- }
- }
- }
- return result
- }
-
internal val LayoutNode.outerMergingSemantics: SemanticsModifierNode?
get() = nodes.firstFromHead(Nodes.Semantics) {
- it.semanticsConfiguration.isMergingSemanticsOfDescendants
+ it.shouldMergeDescendantSemantics
}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
index a5d43a5..7a39405 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.Nodes
import androidx.compose.ui.util.fastForEach
/**
@@ -37,7 +38,16 @@
val unmergedRootSemanticsNode: SemanticsNode
get() {
- return SemanticsNode(rootNode, mergingEnabled = false)
+ return SemanticsNode(
+ outerSemanticsNode = rootNode.nodes.head(Nodes.Semantics)!!.node,
+ layoutNode = rootNode,
+ mergingEnabled = false,
+ // Forcing an empty SemanticsConfiguration here since the root node will always
+ // have an empty config, but if we don't pass this in explicitly here it will try
+ // to call `rootNode.collapsedSemantics` which will fail because the LayoutNode
+ // is not yet attached when this getter is first called.
+ unmergedConfig = SemanticsConfiguration()
+ )
}
}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 2fc9795..698d042 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -21,7 +21,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.unit.Constraints
import org.junit.Test
import org.junit.runner.RunWith
@@ -830,8 +830,7 @@
}
class SemanticsMod(val id: String = "") : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration
- get() = SemanticsConfiguration()
+ override fun SemanticsPropertyReceiver.applySemantics() { }
override fun toString(): String {
return "SemanticsMod($id)"
}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 586d600..df004df 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -61,6 +61,7 @@
import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -1382,12 +1383,11 @@
@Test
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
- val semanticsConfiguration = SemanticsConfiguration()
val semanticsNode1 = object : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+ override fun SemanticsPropertyReceiver.applySemantics() { }
}
val semanticsNode2 = object : SemanticsModifierNode, Modifier.Node() {
- override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+ override fun SemanticsPropertyReceiver.applySemantics() { }
}
data class TestSemanticsElement(
private val node: Modifier.Node
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 639aa60..51a3271 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -14,9 +14,7 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -24,77 +22,49 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+androidXMultiplatform {
+ android()
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:ui:ui-util"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:foundation:foundation-layout"))
-
- implementation(project(":constraintlayout:constraintlayout-core"))
-
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation(project(":compose:ui:ui-test"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:ui:ui-test-manifest"))
- androidTestImplementation(project(":activity:activity"))
-
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
-
- lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
-// implementation(libs.kotlinStdlibCommon)
-
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(project(":compose:ui:ui"))
- implementation("androidx.compose.ui:ui-unit:1.4.0-beta02")
- implementation("androidx.compose.ui:ui-util:1.4.0-beta02")
- implementation("androidx.compose.foundation:foundation:1.4.0-beta02")
- implementation("androidx.compose.foundation:foundation-layout:1.4.0-beta02")
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":constraintlayout:constraintlayout-core"))
-
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(commonMain)
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core-ktx:1.5.0")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ jvmTest {
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- }
-
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.kotlinTest)
implementation(libs.testRules)
implementation(libs.testRunner)
@@ -107,9 +77,27 @@
implementation(project(":compose:test-utils"))
}
}
+
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ }
+ }
}
}
+dependencies {
+ lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
+}
+
androidx {
name = "Android ConstraintLayout Compose Library"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/core/core-telecom/build.gradle b/core/core-telecom/build.gradle
index 595ce5c..2e308881 100644
--- a/core/core-telecom/build.gradle
+++ b/core/core-telecom/build.gradle
@@ -31,6 +31,7 @@
implementation(libs.kotlinCoroutinesCore)
implementation(libs.kotlinCoroutinesGuava)
// Test dependencies
+ androidTestImplementation(project(":internal-testutils-common"))
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
new file mode 100644
index 0000000..ee9f215
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.telecom
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build.VERSION_CODES
+import android.telecom.Connection
+import android.telecom.ConnectionRequest
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(VERSION_CODES.O)
+@SdkSuppress(minSdkVersion = VERSION_CODES.O /* api=26 */)
+class JetpackConnectionServiceTest {
+
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+ private val mCallsManager = CallsManager(mContext)
+ private val mConnectionService = mCallsManager.mConnectionService
+ private val mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+ private val workerExecutor = TestExecutor()
+ private val workerContext: CoroutineContext = workerExecutor.asCoroutineDispatcher()
+ private val callChannels = CallChannels()
+ private val TEST_CALL_ATTRIB_NAME = "Elon Musk"
+ private val TEST_CALL_ATTRIB_NUMBER = Uri.parse("tel:6506959001")
+
+ @After
+ fun onDestroy() {
+ callChannels.closeAllChannels()
+ JetpackConnectionService.mPendingConnectionRequests.clear()
+ }
+
+ /**
+ * Ensure an outgoing Connection object has its properties set before sending it off to the
+ * platform. The properties should reflect everything that is set in CallAttributes.
+ */
+ @SmallTest
+ @Test
+ fun testConnectionServicePropertiesAreSet_outgoingCall() {
+ // create the CallAttributes
+ val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_OUTGOING)
+ // simulate the connection being created
+ val connection = mConnectionService.createSelfManagedConnection(
+ createConnectionRequest(attributes),
+ CallAttributesCompat.DIRECTION_OUTGOING
+ )
+ // verify / assert connection properties
+ verifyConnectionPropertiesBasics(connection)
+ assertEquals(Connection.STATE_DIALING, connection!!.state)
+ }
+
+ /**
+ * Ensure an incoming Connection object has its properties set before sending it off to the
+ * platform. The properties should reflect everything that is set in CallAttributes.
+ */
+ @SmallTest
+ @Test
+ fun testConnectionServicePropertiesAreSet_incomingCall() {
+ // create the CallAttributes
+ val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_INCOMING)
+ // simulate the connection being created
+ val connection = mConnectionService.createSelfManagedConnection(
+ createConnectionRequest(attributes),
+ CallAttributesCompat.DIRECTION_INCOMING
+ )
+ // verify / assert connection properties
+ verifyConnectionPropertiesBasics(connection)
+ assertEquals(Connection.STATE_RINGING, connection!!.state)
+ }
+
+ private fun verifyConnectionPropertiesBasics(connection: Connection?) {
+ // assert it's not null
+ assertNotNull(connection)
+ // unwrap for testing
+ val unwrappedConnection = connection!!
+ // assert all the properties are the same
+ assertEquals(TEST_CALL_ATTRIB_NAME, unwrappedConnection.callerDisplayName)
+ assertEquals(TEST_CALL_ATTRIB_NUMBER, unwrappedConnection.address)
+ assertEquals(
+ Connection.CAPABILITY_HOLD,
+ unwrappedConnection.connectionCapabilities
+ and Connection.CAPABILITY_HOLD
+ )
+ assertEquals(
+ Connection.CAPABILITY_SUPPORT_HOLD,
+ unwrappedConnection.connectionCapabilities
+ and Connection.CAPABILITY_SUPPORT_HOLD
+ )
+ assertEquals(0, JetpackConnectionService.mPendingConnectionRequests.size)
+ }
+
+ private fun createCallAttributes(
+ callDirection: Int,
+ callType: Int? = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
+ ): CallAttributesCompat {
+
+ val attributes: CallAttributesCompat = if (callType != null) {
+ CallAttributesCompat(
+ TEST_CALL_ATTRIB_NAME,
+ TEST_CALL_ATTRIB_NUMBER,
+ callDirection, callType
+ )
+ } else {
+ CallAttributesCompat(
+ TEST_CALL_ATTRIB_NAME,
+ TEST_CALL_ATTRIB_NUMBER,
+ callDirection
+ )
+ }
+
+ attributes.mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+
+ return attributes
+ }
+
+ private fun createConnectionRequest(callAttributesCompat: CallAttributesCompat):
+ ConnectionRequest {
+ // wrap in PendingRequest
+ val pr = JetpackConnectionService.PendingConnectionRequest(
+ callAttributesCompat, callChannels, workerContext, null
+ )
+ // add to the list of pendingRequests
+ JetpackConnectionService.mPendingConnectionRequests.add(pr)
+ // create a ConnectionRequest
+ return ConnectionRequest(mHandle, TEST_CALL_ATTRIB_NUMBER, null)
+ }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/AndroidManifest.xml b/core/core-telecom/src/main/AndroidManifest.xml
index c904673..8ea6753 100644
--- a/core/core-telecom/src/main/AndroidManifest.xml
+++ b/core/core-telecom/src/main/AndroidManifest.xml
@@ -19,7 +19,6 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
- <uses-permission android:name="android.permission.CALL_PHONE" />
<application>
<service
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
index 86cb668..e3d3610 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
@@ -158,7 +158,7 @@
}
}
- private fun hasSupportsSetInactiveCapability(): Boolean {
+ internal fun hasSupportsSetInactiveCapability(): Boolean {
return Utils.hasCapability(SUPPORTS_SET_INACTIVE, callCapabilities)
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index 61e13a3..25e9287 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -60,7 +60,7 @@
private var mPhoneAccount: PhoneAccount? = null
private val mTelecomManager: TelecomManager =
mContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
- private val mConnectionService: JetpackConnectionService = JetpackConnectionService()
+ internal val mConnectionService: JetpackConnectionService = JetpackConnectionService()
// A single declared constant for a direct [Executor], since the coroutines primitives we invoke
// from the associated callbacks will perform their own dispatch as needed.
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
index 628c2c0..d219910 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
@@ -22,6 +22,7 @@
import android.telecom.ConnectionService
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
+import android.telecom.VideoProfile
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
@@ -47,7 +48,7 @@
val callAttributes: CallAttributesCompat,
val callChannel: CallChannels,
val coroutineContext: CoroutineContext,
- val completableDeferred: CompletableDeferred<CallSessionLegacy>
+ val completableDeferred: CompletableDeferred<CallSessionLegacy>?
)
companion object {
@@ -89,7 +90,7 @@
// create a job that times out if the connection cannot be created in x amount of time
CoroutineScope(pendingConnectionRequest.coroutineContext).launch {
delay(CONNECTION_CREATION_TIMEOUT)
- if (!pendingConnectionRequest.completableDeferred.isCompleted) {
+ if (!pendingConnectionRequest.completableDeferred!!.isCompleted) {
Log.i(
TAG, "The request to create a connection timed out. Cancelling the" +
"request to add the call to Telecom."
@@ -155,22 +156,55 @@
mPendingConnectionRequests.remove(pendingRequest)
}
- private fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
+ internal fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
Connection? {
- var jetpackConnection: CallSessionLegacy? = null
- val targetRequest: PendingConnectionRequest? =
- findTargetPendingConnectionRequest(request, direction)
+ val targetRequest: PendingConnectionRequest =
+ findTargetPendingConnectionRequest(request, direction) ?: return null
- if (targetRequest != null) {
- jetpackConnection = CallSessionLegacy(
- ParcelUuid.fromString(UUID.randomUUID().toString()),
- targetRequest.callChannel,
- targetRequest.coroutineContext
- )
- targetRequest.completableDeferred.complete(jetpackConnection)
- mPendingConnectionRequests.remove(targetRequest)
+ val jetpackConnection = CallSessionLegacy(
+ ParcelUuid.fromString(UUID.randomUUID().toString()),
+ targetRequest.callChannel,
+ targetRequest.coroutineContext
+ )
+
+ // set display name
+ jetpackConnection.setCallerDisplayName(
+ targetRequest.callAttributes.displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED
+ )
+
+ // set address
+ jetpackConnection.setAddress(
+ targetRequest.callAttributes.address,
+ TelecomManager.PRESENTATION_ALLOWED
+ )
+
+ // set the call state for the given direction
+ if (direction == CallAttributesCompat.DIRECTION_OUTGOING) {
+ jetpackConnection.setDialing()
+ } else {
+ jetpackConnection.setRinging()
}
+ // set the callType
+ if (targetRequest.callAttributes.callType
+ == CallAttributesCompat.CALL_TYPE_VIDEO_CALL
+ ) {
+ jetpackConnection.setVideoState(VideoProfile.STATE_BIDIRECTIONAL)
+ } else {
+ jetpackConnection.setVideoState(VideoProfile.STATE_AUDIO_ONLY)
+ }
+
+ // set the call capabilities
+ if (targetRequest.callAttributes.hasSupportsSetInactiveCapability()) {
+ jetpackConnection.setConnectionCapabilities(
+ Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD
+ )
+ }
+
+ targetRequest.completableDeferred?.complete(jetpackConnection)
+ mPendingConnectionRequests.remove(targetRequest)
+
return jetpackConnection
}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e79b24a..3ff9e96 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1089,6 +1089,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.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 eeb990f..ed6f417 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1089,6 +1089,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.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 a861a92..f1f4d49 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1206,6 +1206,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.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/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 9629356..508f281 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -66,6 +66,7 @@
import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.content.Context.WINDOW_SERVICE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -138,6 +139,7 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
+import android.view.Display;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -147,8 +149,10 @@
import androidx.annotation.OptIn;
import androidx.core.app.NotificationManagerCompat;
+import androidx.core.hardware.display.DisplayManagerCompat;
import androidx.core.os.BuildCompat;
import androidx.core.test.R;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -606,4 +610,58 @@
Manifest.permission.POST_NOTIFICATIONS));
}
}
+
+ @Test
+ public void testGetDisplayFromActivity() {
+ final Display actualDisplay = ContextCompat.getDisplay(mContext);
+ if (Build.VERSION.SDK_INT >= 30) {
+ assertEquals(mContext.getDisplay(), actualDisplay);
+ } else {
+ final WindowManager windowManager =
+ (WindowManager) mContext.getSystemService(WINDOW_SERVICE);
+ assertEquals(actualDisplay, windowManager.getDefaultDisplay());
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 17)
+ public void testGetDisplayFromDisplayContext() {
+ final DisplayManagerCompat displayManagerCompat = DisplayManagerCompat
+ .getInstance(mContext);
+ final Display defaultDisplay = displayManagerCompat.getDisplay(Display.DEFAULT_DISPLAY);
+ final Context displayContext = mContext.createDisplayContext(defaultDisplay);
+
+ assertEquals(ContextCompat.getDisplay(displayContext), defaultDisplay);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 30)
+ public void testGetDisplayFromWindowContext() {
+ final Context windowContext = mContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
+
+ assertEquals(ContextCompat.getDisplay(windowContext), windowContext.getDisplay());
+ }
+
+ @Test
+ public void testGetDisplayFromApplication() {
+ final Context applicationContext = ApplicationProvider.getApplicationContext();
+ final Context spyContext = spy(applicationContext);
+ final Display actualDisplay = ContextCompat.getDisplay(spyContext);
+
+ if (Build.VERSION.SDK_INT >= 30) {
+ verify(spyContext).getSystemService(eq(DisplayManager.class));
+
+ final Display defaultDisplay = DisplayManagerCompat.getInstance(spyContext)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+ assertEquals(defaultDisplay, actualDisplay);
+ } else {
+ final WindowManager windowManager =
+ (WindowManager) spyContext.getSystemService(WINDOW_SERVICE);
+ // Don't verify if the returned display is the same instance because Application is
+ // not a DisplayContext and the framework always create a fallback Display for
+ // the Context that not associated with a Display.
+ assertEquals(windowManager.getDefaultDisplay().getDisplayId(),
+ actualDisplay.getDisplayId());
+ }
+ }
}
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 cd4e613..172b452 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -70,6 +70,7 @@
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AppOpsManager;
@@ -131,6 +132,7 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
+import android.view.Display;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -140,6 +142,7 @@
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
+import androidx.annotation.DisplayContext;
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
@@ -753,6 +756,28 @@
}
/**
+ * Get the display this context is associated with.
+ * <p>
+ * Applications must use this method with {@link Activity} or a context associated with a
+ * {@link Display} via {@link Context#createDisplayContext(Display)} or
+ * {@link Context#createWindowContext(Display, int, Bundle)}, or the reported {@link Display}
+ * instance is not reliable. </p>
+ *
+ * @param context Context to obtain the associated display
+ * @return The display associated with the Context.
+ */
+ @NonNull
+ public static Display getDisplay(@NonNull @DisplayContext Context context) {
+ if (Build.VERSION.SDK_INT >= 30) {
+ return Api30Impl.getDisplayNoCrash(context);
+ } else {
+ final WindowManager windowManager =
+ (WindowManager) context.getSystemService(WINDOW_SERVICE);
+ return windowManager.getDefaultDisplay();
+ }
+ }
+
+ /**
* Return the handle to a system-level service by class.
*
* @param context Context to retrieve service from.
@@ -1113,6 +1138,19 @@
static String getAttributionTag(Context obj) {
return obj.getAttributionTag();
}
+
+ @DoNotInline
+ static Display getDisplayNoCrash(Context obj) {
+ try {
+ return obj.getDisplay();
+ } catch (UnsupportedOperationException e) {
+ // Provide a fallback display if the context is not associated with any display.
+ Log.w(TAG, "The context:" + obj + " is not associated with any display. Return a "
+ + "fallback display instead.");
+ return obj.getSystemService(DisplayManager.class)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+ }
+ }
}
@RequiresApi(33)
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 a43c2af..5ccc392 100644
--- a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
@@ -207,24 +207,23 @@
insetsController = mView.getWindowInsetsController();
}
if (insetsController != null) {
- if (SDK_INT <= 33) {
- final AtomicBoolean isImeInsetsControllable = new AtomicBoolean(false);
- final WindowInsetsController.OnControllableInsetsChangedListener listener =
- (windowInsetsController, typeMask) -> isImeInsetsControllable.set(
- (typeMask & WindowInsetsCompat.Type.IME) != 0);
- // Register the OnControllableInsetsChangedListener would synchronously
- // callback current controllable insets. Adding the listener here to check if
- // ime inset is controllable.
- insetsController.addOnControllableInsetsChangedListener(listener);
- if (!isImeInsetsControllable.get()) {
- 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
- // control the ime insets. Use the InputMethodManager instead.
- imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
- }
- insetsController.removeOnControllableInsetsChangedListener(listener);
+ final AtomicBoolean isImeInsetsControllable = new AtomicBoolean(false);
+ final WindowInsetsController.OnControllableInsetsChangedListener listener =
+ (windowInsetsController, typeMask) -> isImeInsetsControllable.set(
+ (typeMask & WindowInsetsCompat.Type.IME) != 0);
+ // Register the OnControllableInsetsChangedListener would synchronously
+ // callback current controllable insets. Adding the listener here to check if
+ // ime inset is controllable.
+ insetsController.addOnControllableInsetsChangedListener(listener);
+ if (!isImeInsetsControllable.get()) {
+ 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
+ // control the ime insets. Use the InputMethodManager instead.
+ // TODO(b/280532442): Fix this in the platform side.
+ imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
}
+ insetsController.removeOnControllableInsetsChangedListener(listener);
insetsController.hide(WindowInsets.Type.ime());
} else {
// Couldn't find an insets controller, fallback to old implementation
diff --git a/development/project-creator/compose-template/groupId/artifactId/build.gradle b/development/project-creator/compose-template/groupId/artifactId/build.gradle
index a47774d..e09ec31 100644
--- a/development/project-creator/compose-template/groupId/artifactId/build.gradle
+++ b/development/project-creator/compose-template/groupId/artifactId/build.gradle
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
+import androidx.build.KmpPlatformsKt
plugins {
id("AndroidXPlugin")
@@ -24,68 +24,75 @@
id("org.jetbrains.kotlin.android")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
-
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
- implementation(libs.kotlinStdlibCommon)
-
- api("androidx.annotation:annotation:1.1.0")
-
- testImplementation(libs.testRules)
- testImplementation(libs.testRunner)
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
}
+ }
+ androidMain.dependencies {
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
}
+ }
- test.dependencies {
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.junit)
implementation(libs.truth)
}
+ }
- androidAndroidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
}
}
}
diff --git a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
index e75969a..5130a65 100644
--- a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
+++ b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
@@ -30,13 +30,19 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.Spannable;
import android.text.SpannableString;
+import android.widget.EditText;
import android.widget.TextView;
import androidx.emoji.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
@@ -126,4 +132,31 @@
verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void initCallback_doesntCrashWhenNotAttached() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ EditText editText = new EditText(context);
+ EmojiInputFilter subject = new EmojiInputFilter(editText);
+ subject.getInitCallback().onInitialized();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ public void initCallback_sendsToNonMainHandler_beforeSetText() {
+ // this is just testing that onInitialized dispatches to editText.getHandler before setText
+ EditText mockEditText = mock(EditText.class);
+ HandlerThread thread = new HandlerThread("random thread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ thread.quitSafely();
+ when(mockEditText.getHandler()).thenReturn(handler);
+ EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
+ EmojiInputFilter.InitCallbackImpl initCallback =
+ (EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
+ initCallback.onInitialized();
+
+ handler.hasCallbacks(initCallback);
+ }
}
diff --git a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
index fe3da64..50b0169 100644
--- a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
+++ b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
@@ -27,13 +27,18 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.Spannable;
import android.text.SpannableString;
import android.widget.EditText;
import androidx.emoji.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
@@ -41,6 +46,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
public class EmojiTextWatcherTest {
private EmojiTextWatcher mTextWatcher;
@@ -120,4 +126,31 @@
verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
}
+
+ @Test
+ public void initCallback_doesntCrashWhenNotAttached() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ EditText editText = new EditText(context);
+ EmojiTextWatcher subject = new EmojiTextWatcher(editText);
+ subject.getInitCallback().onInitialized();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ public void initCallback_sendsToNonMainHandler_beforeSetText() {
+ // this is just testing that onInitialized dispatches to editText.getHandler before setText
+ EditText mockEditText = mock(EditText.class);
+ HandlerThread thread = new HandlerThread("random thread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ thread.quitSafely();
+ when(mockEditText.getHandler()).thenReturn(handler);
+ EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText);
+ EmojiTextWatcher.InitCallbackImpl initCallback =
+ (EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
+ initCallback.onInitialized();
+
+ handler.hasCallbacks(initCallback);
+ }
}
+
diff --git a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
index 27d1aa0..25f9478 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
@@ -15,8 +15,10 @@
*/
package androidx.emoji.widget;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.os.Handler;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
@@ -88,14 +90,16 @@
}
}
- private InitCallback getInitCallback() {
+ @RestrictTo(LIBRARY)
+ InitCallback getInitCallback() {
if (mInitCallback == null) {
mInitCallback = new InitCallbackImpl(mTextView);
}
return mInitCallback;
}
- private static class InitCallbackImpl extends InitCallback {
+ @RestrictTo(LIBRARY)
+ static class InitCallbackImpl extends InitCallback implements Runnable {
private final Reference<TextView> mViewRef;
InitCallbackImpl(TextView textView) {
@@ -106,7 +110,23 @@
public void onInitialized() {
super.onInitialized();
final TextView textView = mViewRef.get();
- if (textView != null && textView.isAttachedToWindow()) {
+ if (textView == null) {
+ return;
+ }
+ // we need to move to the actual thread this view is using as main
+ Handler handler = textView.getHandler();
+ if (handler != null) {
+ handler.post(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ final TextView textView = mViewRef.get();
+ if (textView == null) {
+ return;
+ }
+ if (textView.isAttachedToWindow()) {
final CharSequence result = EmojiCompat.get().process(textView.getText());
final int selectionStart = Selection.getSelectionStart(result);
diff --git a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
index 2a4572f..6e6fcf3 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
@@ -15,8 +15,10 @@
*/
package androidx.emoji.widget;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.os.Handler;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
@@ -99,14 +101,16 @@
// do nothing
}
- private InitCallback getInitCallback() {
+ @RestrictTo(LIBRARY)
+ InitCallback getInitCallback() {
if (mInitCallback == null) {
mInitCallback = new InitCallbackImpl(mEditText);
}
return mInitCallback;
}
- private static class InitCallbackImpl extends InitCallback {
+ @RestrictTo(LIBRARY)
+ static class InitCallbackImpl extends InitCallback implements Runnable {
private final Reference<EditText> mViewRef;
InitCallbackImpl(EditText editText) {
@@ -117,6 +121,19 @@
public void onInitialized() {
super.onInitialized();
final EditText editText = mViewRef.get();
+ if (editText == null) {
+ return;
+ }
+ Handler handler = editText.getHandler();
+ if (handler == null) {
+ return;
+ }
+ handler.post(this);
+ }
+
+ @Override
+ public void run() {
+ final EditText editText = mViewRef.get();
if (editText != null && editText.isAttachedToWindow()) {
final Editable text = editText.getEditableText();
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
index 3349e6d..a21adf7 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
@@ -30,9 +30,13 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.InputFilter;
import android.text.Spannable;
import android.text.SpannableString;
+import android.widget.EditText;
import android.widget.TextView;
import androidx.emoji2.text.EmojiCompat;
@@ -40,6 +44,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
@@ -191,7 +196,7 @@
when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
// trigger initialized
- captor.getValue().onInitialized();
+ ((Runnable) captor.getValue()).run();
verify(mEmojiCompat).process(eq(testString));
}
@@ -223,7 +228,7 @@
when(mEmojiCompat.process(eq(testString))).thenReturn(testString);
when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
// trigger initialized
- captor.getValue().onInitialized();
+ ((Runnable) captor.getValue()).run();
// validate interactions don't do anything except check for update
verify(mTextView).getFilters();
@@ -233,4 +238,31 @@
// if you add a safe interaction please update test
verifyNoMoreInteractions(mTextView);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void initCallback_doesntCrashWhenNotAttached() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ EditText editText = new EditText(context);
+ EmojiInputFilter subject = new EmojiInputFilter(editText);
+ subject.getInitCallback().onInitialized();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ public void initCallback_sendsToNonMainHandler_beforeSetText() {
+ // this is just testing that onInitialized dispatches to editText.getHandler before setText
+ EditText mockEditText = mock(EditText.class);
+ HandlerThread thread = new HandlerThread("random thread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ thread.quitSafely();
+ when(mockEditText.getHandler()).thenReturn(handler);
+ EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
+ EmojiInputFilter.InitCallbackImpl initCallback =
+ (EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
+ initCallback.onInitialized();
+
+ handler.hasCallbacks(initCallback);
+ }
}
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
index 8f149da..5c3c5de 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
@@ -27,6 +27,9 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@@ -37,6 +40,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
@@ -149,4 +153,30 @@
mTextWatcher.onTextChanged(expected, 0, 0, 1);
assertTrue(TextUtils.equals(expected, "abc"));
}
+
+ @Test
+ public void initCallback_doesntCrashWhenNotAttached() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ EditText editText = new EditText(context);
+ EmojiTextWatcher subject = new EmojiTextWatcher(editText, false);
+ subject.getInitCallback().onInitialized();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ public void initCallback_sendsToNonMainHandler_beforeSetText() {
+ // this is just testing that onInitialized dispatches to editText.getHandler before setText
+ EditText mockEditText = mock(EditText.class);
+ HandlerThread thread = new HandlerThread("random thread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ thread.quitSafely();
+ when(mockEditText.getHandler()).thenReturn(handler);
+ EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText, false);
+ EmojiTextWatcher.InitCallbackImpl initCallback =
+ (EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
+ initCallback.onInitialized();
+
+ handler.hasCallbacks(initCallback);
+ }
}
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
index 530fc52..9c9b485 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
@@ -15,6 +15,9 @@
*/
package androidx.emoji2.viewsintegration;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.Handler;
import android.text.InputFilter;
import android.text.Selection;
import android.text.Spannable;
@@ -39,7 +42,7 @@
* effects.
*
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(LIBRARY)
@RequiresApi(19)
final class EmojiInputFilter implements android.text.InputFilter {
private final TextView mTextView;
@@ -88,15 +91,17 @@
}
}
- private InitCallback getInitCallback() {
+ @RestrictTo(LIBRARY)
+ InitCallback getInitCallback() {
if (mInitCallback == null) {
mInitCallback = new InitCallbackImpl(mTextView, this);
}
return mInitCallback;
}
+ @RestrictTo(LIBRARY)
@RequiresApi(19)
- private static class InitCallbackImpl extends InitCallback {
+ static class InitCallbackImpl extends InitCallback implements Runnable {
private final Reference<TextView> mViewRef;
private final Reference<EmojiInputFilter> mEmojiInputFilterReference;
@@ -109,6 +114,19 @@
@Override
public void onInitialized() {
super.onInitialized();
+ final TextView textView = mViewRef.get();
+ if (textView == null) {
+ return;
+ }
+ // we need to move to the actual thread this view is using as main
+ Handler handler = textView.getHandler();
+ if (handler != null) {
+ handler.post(this);
+ }
+ }
+
+ @Override
+ public void run() {
@Nullable final TextView textView = mViewRef.get();
@Nullable final InputFilter myInputFilter = mEmojiInputFilterReference.get();
if (!isInputFilterCurrentlyRegisteredOnTextView(textView, myInputFilter)) return;
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
index 55c37da..691f995 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
@@ -15,6 +15,9 @@
*/
package androidx.emoji2.viewsintegration;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.Handler;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
@@ -34,7 +37,7 @@
* TextWatcher used for an EditText.
*
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(LIBRARY)
@RequiresApi(19)
final class EmojiTextWatcher implements android.text.TextWatcher {
private final EditText mEditText;
@@ -107,7 +110,11 @@
// do nothing
}
- private InitCallback getInitCallback() {
+ /**
+ * @return
+ */
+ @RestrictTo(LIBRARY)
+ InitCallback getInitCallback() {
if (mInitCallback == null) {
mInitCallback = new InitCallbackImpl(mEditText);
}
@@ -130,8 +137,9 @@
}
}
+ @RestrictTo(LIBRARY)
@RequiresApi(19)
- private static class InitCallbackImpl extends InitCallback {
+ static class InitCallbackImpl extends InitCallback implements Runnable {
private final Reference<EditText> mViewRef;
InitCallbackImpl(EditText editText) {
@@ -142,6 +150,19 @@
public void onInitialized() {
super.onInitialized();
final EditText editText = mViewRef.get();
+ if (editText == null) {
+ return;
+ }
+ Handler handler = editText.getHandler();
+ if (handler == null) {
+ return;
+ }
+ handler.post(this);
+ }
+
+ @Override
+ public void run() {
+ final EditText editText = mViewRef.get();
processTextOnEnablingEvent(editText, EmojiCompat.LOAD_STATE_SUCCEEDED);
}
}
diff --git a/graphics/graphics-path/build.gradle b/graphics/graphics-path/build.gradle
index 44d4e0c..392c246 100644
--- a/graphics/graphics-path/build.gradle
+++ b/graphics/graphics-path/build.gradle
@@ -26,7 +26,7 @@
api(libs.kotlinStdlib)
implementation('androidx.appcompat:appcompat:1.6.1')
- implementation('androidx.core:core:1.5.0-beta01')
+ implementation(project(':core:core'))
androidTestImplementation("androidx.annotation:annotation:1.4.0")
androidTestImplementation("androidx.core:core-ktx:1.8.0")
diff --git a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
index 8a7e238..495d89f 100644
--- a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
+++ b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
@@ -17,9 +17,12 @@
package androidx.javascriptengine;
import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.webkit.WebView;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.core.content.pm.PackageInfoCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -53,6 +56,18 @@
Assume.assumeTrue(JavaScriptSandbox.isSupported());
}
+ // Get the current WebView provider version. In a versionCode of AAAABBBCD, AAAA is the build
+ // number and BBB is the patch number. C and D may usually be ignored.
+ //
+ // Strongly prefer using feature flags over version checks if possible.
+ public long getWebViewVersion() {
+ PackageInfo systemWebViewPackage = WebView.getCurrentWebViewPackage();
+ if (systemWebViewPackage == null) {
+ Assert.fail("No current WebView provider");
+ }
+ return PackageInfoCompat.getLongVersionCode(systemWebViewPackage);
+ }
+
@Test
@MediumTest
public void testSimpleJsEvaluation() throws Throwable {
@@ -571,6 +586,14 @@
@Test
@LargeTest
public void testHeapSizeEnforced() throws Throwable {
+ // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
+ // instead crash the whole sandbox process. This change is not tracked in a feature flag.
+ // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
+ // of interest for this test.
+ // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
+ Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
+ getWebViewVersion() >= 5438_000_00L);
+
final long maxHeapSize = REASONABLE_HEAP_SIZE;
// We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
// that we're allocating an array of elements - not bytes. Filling will ensure that the
@@ -587,6 +610,7 @@
try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
+
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
@@ -657,6 +681,14 @@
@Test
@LargeTest
public void testIsolateCreationAfterCrash() throws Throwable {
+ // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
+ // instead crash the whole sandbox process. This change is not tracked in a feature flag.
+ // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
+ // of interest for this test.
+ // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
+ Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
+ getWebViewVersion() >= 5438_000_00L);
+
final long maxHeapSize = REASONABLE_HEAP_SIZE;
// We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
// that we're allocating an array of elements - not bytes. Filling will ensure that the
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
index 00afeb8..a4432f0 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
@@ -162,13 +162,23 @@
@Override
public void reportResult(String result) {
Objects.requireNonNull(result);
- handleEvaluationResult(mCompleter, result);
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ handleEvaluationResult(mCompleter, result);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
}
@Override
public void reportError(@ExecutionErrorTypes int type, String error) {
Objects.requireNonNull(error);
- handleEvaluationError(mCompleter, type, error);
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ handleEvaluationError(mCompleter, type, error);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
}
}
diff --git a/lifecycle/lifecycle-runtime-compose/api/current.txt b/lifecycle/lifecycle-runtime-compose/api/current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/current.txt
@@ -10,11 +10,31 @@
public final class LifecycleEffectKt {
method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+ method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+ method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
}
public final class LifecycleExtKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
}
+ public interface LifecyclePauseEffectResult {
+ method public void runPauseEffect();
+ }
+
+ public final class LifecycleResumePauseEffectScope {
+ ctor public LifecycleResumePauseEffectScope();
+ method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+ }
+
+ public final class LifecycleStartStopEffectScope {
+ ctor public LifecycleStartStopEffectScope();
+ method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+ }
+
+ public interface LifecycleStopEffectResult {
+ method public void runStopEffect();
+ }
+
}
diff --git a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
@@ -10,11 +10,31 @@
public final class LifecycleEffectKt {
method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+ method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+ method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
}
public final class LifecycleExtKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
}
+ public interface LifecyclePauseEffectResult {
+ method public void runPauseEffect();
+ }
+
+ public final class LifecycleResumePauseEffectScope {
+ ctor public LifecycleResumePauseEffectScope();
+ method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+ }
+
+ public final class LifecycleStartStopEffectScope {
+ ctor public LifecycleStartStopEffectScope();
+ method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+ }
+
+ public interface LifecycleStopEffectResult {
+ method public void runStopEffect();
+ }
+
}
diff --git a/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt b/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
@@ -10,11 +10,31 @@
public final class LifecycleEffectKt {
method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+ method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+ method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
}
public final class LifecycleExtKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
}
+ public interface LifecyclePauseEffectResult {
+ method public void runPauseEffect();
+ }
+
+ public final class LifecycleResumePauseEffectScope {
+ ctor public LifecycleResumePauseEffectScope();
+ method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+ }
+
+ public final class LifecycleStartStopEffectScope {
+ ctor public LifecycleStartStopEffectScope();
+ method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+ }
+
+ public interface LifecycleStopEffectResult {
+ method public void runStopEffect();
+ }
+
}
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt b/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
index 3d7e9fe..482c851 100644
--- a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
@@ -110,4 +110,78 @@
.isEqualTo(1)
}
}
+
+ @Test
+ fun lifecycleStartEffectTest() {
+ lifecycleOwner = TestLifecycleOwner(
+ Lifecycle.State.INITIALIZED,
+ dispatcher
+ )
+ var startCount = 0
+ var stopCount = 0
+
+ composeTestRule.waitForIdle()
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
+ LifecycleStartEffect {
+ startCount++
+
+ onStop {
+ stopCount++
+ }
+ }
+ }
+ }
+
+ composeTestRule.runOnIdle {
+ assertWithMessage("Lifecycle should not be started (or stopped)")
+ .that(startCount)
+ .isEqualTo(0)
+
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
+ assertWithMessage("Lifecycle should have been started")
+ .that(startCount)
+ .isEqualTo(1)
+
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ assertWithMessage("Lifecycle should have been stopped")
+ .that(stopCount)
+ .isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun lifecycleResumeEffectTest() {
+ var resumeCount = 0
+ var pauseCount = 0
+
+ composeTestRule.waitForIdle()
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
+ LifecycleResumeEffect {
+ resumeCount++
+
+ onPause {
+ pauseCount++
+ }
+ }
+ }
+ }
+
+ composeTestRule.runOnIdle {
+ assertWithMessage("Lifecycle should not be resumed (or paused)")
+ .that(resumeCount)
+ .isEqualTo(0)
+
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ assertWithMessage("Lifecycle should have been resumed")
+ .that(resumeCount)
+ .isEqualTo(1)
+
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ assertWithMessage("Lifecycle should have been paused")
+ .that(pauseCount)
+ .isEqualTo(1)
+ }
+ }
}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
index c9e47ae..245b1a5 100644
--- a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
@@ -75,4 +75,176 @@
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Schedule a pair of effects to run when the [Lifecycle] receives either a
+ * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP]. The ON_START effect will
+ * be the body of the [effects] block and the ON_STOP effect will be within the
+ * (onStop clause)[LifecycleStartStopEffectScope.onStop]:
+ *
+ * LifecycleStartEffect(lifecycleOwner) {
+ * // add ON_START effect work here
+ *
+ * onStop {
+ * // add ON_STOP effect work here
+ * }
+ * }
+ *
+ * A [LifecycleStartEffect] **must** include an [onStop][LifecycleStartStopEffectScope.onStop]
+ * clause as the final statement in its [effects] block. If your operation does not require
+ * an effect for both ON_START and ON_STOP, a [LifecycleEventEffect] should be used instead.
+ *
+ * This function uses a [LifecycleEventObserver] to listen for when [LifecycleStartEffect]
+ * enters the composition and the effects will be launched when receiving a
+ * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively.
+ *
+ * This function should **not** be used to launch tasks in response to callback
+ * events by way of storing callback data as a [Lifecycle.State] in a [MutableState].
+ * Instead, see [currentStateAsState] to obtain a [State<Lifecycle.State>][State]
+ * that may be used to launch jobs in response to state changes.
+ *
+ * @param lifecycleOwner The lifecycle owner to attach an observer
+ * @param effects The effects to be launched when we receive the respective event callbacks
+ */
+@Composable
+fun LifecycleStartEffect(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ effects: LifecycleStartStopEffectScope.() -> LifecycleStopEffectResult
+) {
+ val lifecycleStartStopEffectScope = LifecycleStartStopEffectScope()
+ // Safely update the current `onStart` lambda when a new one is provided
+ val currentEffects by rememberUpdatedState(effects)
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_START ->
+ lifecycleStartStopEffectScope.currentEffects()
+
+ Lifecycle.Event.ON_STOP ->
+ lifecycleStartStopEffectScope.currentEffects().runStopEffect()
+
+ else -> {}
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+}
+
+/**
+ * Interface used for [LifecycleStartEffect] to run the effect within the onStop
+ * clause when an (ON_STOP)[Lifecycle.Event.ON_STOP] event is received.
+ */
+interface LifecycleStopEffectResult {
+ fun runStopEffect()
+}
+
+/**
+ * Receiver scope for [LifecycleStartEffect] that offers the [onStop] clause to
+ * couple the ON_START effect. This should be the last statement in any call to
+ * [LifecycleStartEffect].
+ */
+class LifecycleStartStopEffectScope {
+ /**
+ * Provide the [onStopEffect] to the [LifecycleStartEffect] to run when the observer
+ * receives an (ON_STOP)[Lifecycle.Event.ON_STOP] event.
+ */
+ inline fun onStop(
+ crossinline onStopEffect: () -> Unit
+ ): LifecycleStopEffectResult = object : LifecycleStopEffectResult {
+ override fun runStopEffect() {
+ onStopEffect()
+ }
+ }
+}
+
+/**
+ * Schedule a pair of effects to run when the [Lifecycle] receives either a
+ * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE]. The ON_RESUME effect
+ * will be the body of the [effects] block and the ON_PAUSE effect will be within the
+ * (onPause clause)[LifecycleResumePauseEffectScope.onPause]:
+ *
+ * LifecycleResumeEffect(lifecycleOwner) {
+ * // add ON_RESUME effect work here
+ *
+ * onPause {
+ * // add ON_PAUSE effect work here
+ * }
+ * }
+ *
+ * A [LifecycleResumeEffect] **must** include an [onPause][LifecycleResumePauseEffectScope.onPause]
+ * clause as the final statement in its [effects] block. If your operation does not require
+ * an effect for both ON_RESUME and ON_PAUSE, a [LifecycleEventEffect] should be used instead.
+ *
+ * This function uses a [LifecycleEventObserver] to listen for when [LifecycleResumeEffect]
+ * enters the composition and the effects will be launched when receiving a
+ * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively.
+ *
+ * This function should **not** be used to launch tasks in response to callback
+ * events by way of storing callback data as a [Lifecycle.State] in a [MutableState].
+ * Instead, see [currentStateAsState] to obtain a [State<Lifecycle.State>][State]
+ * that may be used to launch jobs in response to state changes.
+ *
+ * @param lifecycleOwner The lifecycle owner to attach an observer
+ * @param effects The effects to be launched when we receive the respective event callbacks
+ */
+@Composable
+fun LifecycleResumeEffect(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseEffectResult
+) {
+ val lifecycleResumePauseEffectScope = LifecycleResumePauseEffectScope()
+ // Safely update the current `onResume` lambda when a new one is provided
+ val currentEffects by rememberUpdatedState(effects)
+ DisposableEffect(lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME ->
+ lifecycleResumePauseEffectScope.currentEffects()
+
+ Lifecycle.Event.ON_PAUSE ->
+ lifecycleResumePauseEffectScope.currentEffects().runPauseEffect()
+
+ else -> {}
+ }
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+}
+
+/**
+ * Interface used for [LifecycleResumeEffect] to run the effect within the onPause
+ * clause when an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event is received.
+ */
+interface LifecyclePauseEffectResult {
+ fun runPauseEffect()
+}
+
+/**
+ * Receiver scope for [LifecycleResumeEffect] that offers the [onPause] clause to
+ * couple the ON_RESUME effect. This should be the last statement in any call to
+ * [LifecycleResumeEffect].
+ */
+class LifecycleResumePauseEffectScope {
+ /**
+ * Provide the [onPauseEffect] to the [LifecycleResumeEffect] to run when the observer
+ * receives an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event.
+ */
+ inline fun onPause(
+ crossinline onPauseEffect: () -> Unit
+ ): LifecyclePauseEffectResult = object : LifecyclePauseEffectResult {
+ override fun runPauseEffect() {
+ onPauseEffect()
+ }
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt b/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
index a365b00..16e56b7 100644
--- a/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
@@ -44,19 +44,32 @@
override fun getApplicableFiles() = Scope.OTHER_SCOPE
override fun beforeCheckEachProject(context: Context) {
- LanguageParserDefinitions.INSTANCE.apply {
+ // Neither LanguageParserDefinitions nor CoreFileTypeRegistry are thread-safe, so this is a
+ // best-effort to avoid a race condition during our own access across multiple lint worker
+ // threads.
+ synchronized(intellijCoreLock) {
+ val aidlFileType = AidlFileType.INSTANCE
+
// When we run from CLI, the IntelliJ parser (which does not support lexing AIDL) will
// already be set. Only the first parser will be used, so we need to remove that parser
// before we add our own.
- allForLanguage(AidlFileType.INSTANCE.language).forEach { parser ->
- removeExplicitExtension(AidlFileType.INSTANCE.language, parser)
+ val languageParserDefinitions = LanguageParserDefinitions.INSTANCE
+ languageParserDefinitions.apply {
+ allForLanguage(aidlFileType.language).forEach { parser ->
+ removeExplicitExtension(aidlFileType.language, parser)
+ }
+ addExplicitExtension(aidlFileType.language, AidlParserDefinition())
}
- addExplicitExtension(AidlFileType.INSTANCE.language, AidlParserDefinition())
+
+ // Register our parser for the AIDL file type. Files may be registered more than once to
+ // overwrite the associated extension, but only the first call to `registerFileType`
+ // will associate the file with the name returned by `FileType.getName()`.
+ val coreFileTypeRegistry = CoreFileTypeRegistry.getInstance() as CoreFileTypeRegistry
+ coreFileTypeRegistry.registerFileType(
+ aidlFileType,
+ aidlFileType.defaultExtension
+ )
}
- (CoreFileTypeRegistry.getInstance() as CoreFileTypeRegistry).registerFileType(
- AidlFileType.INSTANCE,
- AidlFileType.INSTANCE.defaultExtension
- )
}
override fun run(context: Context) {
@@ -125,4 +138,10 @@
containingFile.text,
textRange.startOffset,
textRange.endOffset
-)
\ No newline at end of file
+)
+
+/**
+ * Lock object used to synchronize access to IntelliJ registries which are not thread-safe,
+ * including [LanguageParserDefinitions] and [CoreFileTypeRegistry].
+ */
+private val intellijCoreLock = Any()
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
index 083d17d..e820bfe 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
@@ -18,6 +18,7 @@
import android.graphics.Color
import android.os.Bundle
+import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -27,6 +28,7 @@
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
+import androidx.transition.Slide
/**
* Fragment used to show how to navigate to another destination
@@ -38,6 +40,8 @@
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
+ enterTransition = Slide(Gravity.RIGHT)
+ exitTransition = Slide(Gravity.LEFT)
return inflater.inflate(R.layout.main_fragment, container, false)
}
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
index cea98bd..4db4d1d 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
@@ -23,31 +23,19 @@
android:name=".MainFragment"
android:label="@string/home">
<argument android:name="myarg" android:defaultValue="Home" />
- <action android:id="@+id/next" app:destination="@+id/first_screen"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
</fragment>
<fragment android:id="@+id/first_screen"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/first">
<argument android:name="myarg" android:defaultValue="one" />
- <action android:id="@+id/next" app:destination="@+id/next_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/next_fragment"/>
</fragment>
<fragment android:id="@+id/next_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/second">
<argument android:name="myarg" android:defaultValue="two" />
- <action android:id="@+id/next" app:destination="@+id/first_screen"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
</fragment>
</navigation>
<dialog
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
index f7eb4da..63261cbf 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
@@ -20,51 +20,31 @@
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/first">
<argument android:name="myarg" android:defaultValue="one" />
- <action android:id="@+id/next" app:destination="@+id/second_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/second_fragment"/>
</fragment>
<fragment android:id="@+id/second_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/second">
<argument android:name="myarg" android:defaultValue="two" />
- <action android:id="@+id/next" app:destination="@+id/third_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/third_fragment"/>
</fragment>
<fragment android:id="@+id/third_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/third">
<argument android:name="myarg" android:defaultValue="three" />
- <action android:id="@+id/next" app:destination="@+id/fourth_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/fourth_fragment"/>
</fragment>
<fragment android:id="@+id/fourth_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/fourth">
<argument android:name="myarg" android:defaultValue="four" />
- <action android:id="@+id/next" app:destination="@+id/fifth_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/fifth_fragment"/>
</fragment>
<fragment android:id="@+id/fifth_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/fifth">
<argument android:name="myarg" android:defaultValue="five" />
- <action android:id="@+id/next" app:destination="@+id/first_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_fragment"/>
</fragment>
<dialog
android:id="@+id/learn_more"
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -46,7 +46,7 @@
method @AnyThread public void onInvalidated();
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 07e6471..a5d3d01 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -49,7 +49,7 @@
@kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -415,6 +415,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -46,7 +46,7 @@
method @AnyThread public void onInvalidated();
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
index 93ddb2d..3f74d54 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
@@ -32,7 +32,7 @@
*/
public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
private val pagingSourceFactory: () -> PagingSource<Key, Value>
-) : () -> PagingSource<Key, Value> {
+) : PagingSourceFactory<Key, Value> {
@VisibleForTesting
internal val pagingSources = CopyOnWriteArrayList<PagingSource<Key, Value>>()
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
new file mode 100644
index 0000000..4ed3c7f
--- /dev/null
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.paging
+
+/**
+ * Interface for a factory that generates [PagingSource].
+ *
+ * The factory extending this interface can be used to instantiate a [Pager] as the
+ * pagingSourceFactory.
+ */
+public fun interface PagingSourceFactory<Key : Any, Value : Any> : () -> PagingSource<Key, Value> {
+ /**
+ * Returns a new PagingSource instance.
+ *
+ * This function can be invoked by calling pagingSourceFactory() or pagingSourceFactory::invoke.
+ */
+ public override operator fun invoke(): PagingSource<Key, Value>
+}
\ No newline at end of file
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<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 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
index e44216e..3cf9165 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
@@ -19,6 +19,7 @@
import androidx.paging.InvalidatingPagingSourceFactory
import androidx.paging.PagingSource
import androidx.paging.Pager
+import androidx.paging.PagingSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@@ -41,7 +42,7 @@
*/
public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
coroutineScope: CoroutineScope
-): () -> PagingSource<Int, Value> {
+): PagingSourceFactory<Int, Value> {
var data: List<Value>? = null
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 42aaa0b5..312d95b 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -20,6 +20,7 @@
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSourceFactory
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
@@ -2355,9 +2356,9 @@
}
private class WrappedPagingSourceFactory(
- private val factory: () -> PagingSource<Int, Int>,
+ private val factory: PagingSourceFactory<Int, Int>,
private val loadDelay: Long,
-) : () -> PagingSource<Int, Int> {
+) : PagingSourceFactory<Int, Int> {
override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
}
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 6a2f2c1..eb28936 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
@@ -17,8 +17,8 @@
package androidx.paging.testing
import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSourceFactory
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -47,7 +47,7 @@
@Test
fun emptyFlow() {
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flowOf<List<Int>>().asPagingSourceFactory(testScope)
val pagingSource = factory()
val pager = TestPager(pagingSource, CONFIG)
@@ -64,7 +64,7 @@
List(20) { it }
)
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flow.asPagingSourceFactory(testScope)
val pagingSource = factory()
val pager = TestPager(pagingSource, CONFIG)
@@ -85,7 +85,7 @@
emit(List(15) { it + 30 }) // second gen
}
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flow.asPagingSourceFactory(testScope)
advanceTimeBy(1000)
@@ -117,7 +117,7 @@
val mutableFlow = MutableSharedFlow<List<Int>>()
val collectionScope = this.backgroundScope
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(collectionScope)
mutableFlow.emit(List(10) { it })
@@ -146,10 +146,10 @@
fun multipleFactories_fromSameFlow() = testScope.runTest {
val mutableFlow = MutableSharedFlow<List<Int>>()
- val factory1: () -> PagingSource<Int, Int> =
+ val factory1: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
- val factory2: () -> PagingSource<Int, Int> =
+ val factory2: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
mutableFlow.emit(List(10) { it })
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 470c9af..213eeb9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -22,10 +22,10 @@
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.KSTypeReference
-import com.google.devtools.ksp.symbol.Modifier
/**
* Root package comes as <root> instead of "" so we work around it here.
@@ -47,10 +47,12 @@
}
internal fun KSTypeReference.isTypeParameterReference(): Boolean {
- return this.resolve().declaration is KSTypeParameter
+ return this.resolve().isTypeParameter()
}
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+internal fun KSType.isTypeParameter(): Boolean {
+ return declaration is KSTypeParameter
+}
internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
XNullability.NULLABLE -> makeNullable()
@@ -59,14 +61,15 @@
}
private fun KSAnnotated.hasAnnotation(qName: String) =
- annotations.any { it.hasQualifiedName(qName) }
+ annotations.any { it.hasQualifiedNameOrAlias(qName) }
-private fun KSAnnotation.hasQualifiedName(qName: String): Boolean {
- return annotationType.resolve().hasQualifiedName(qName)
+private fun KSAnnotation.hasQualifiedNameOrAlias(qName: String): Boolean {
+ return annotationType.resolve().hasQualifiedNameOrAlias(qName)
}
-private fun KSType.hasQualifiedName(qName: String): Boolean {
- return declaration.qualifiedName?.asString() == qName
+private fun KSType.hasQualifiedNameOrAlias(qName: String): Boolean {
+ return declaration.qualifiedName?.asString() == qName ||
+ (declaration as? KSTypeAlias)?.type?.resolve()?.hasQualifiedNameOrAlias(qName) ?: false
}
internal fun KSAnnotated.hasJvmWildcardAnnotation() =
@@ -75,20 +78,55 @@
internal fun KSAnnotated.hasSuppressJvmWildcardAnnotation() =
hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
-private fun KSType.hasAnnotation(qName: String) = annotations.any { it.hasQualifiedName(qName) }
-
-internal fun KSType.hasJvmWildcardAnnotation() =
- hasAnnotation(JvmWildcard::class.java.canonicalName!!)
+// TODO(bcorso): There's a bug in KSP where, after using KSType#asMemberOf() or KSType#replace(),
+// the annotations are removed from the resulting type. However, it turns out that the annotation
+// information is still available in the underlying KotlinType, so we use reflection to get them.
+// See https://github.com/google/ksp/issues/1376.
+private fun KSType.hasAnnotation(qName: String): Boolean {
+ fun String.toFqName(): Any {
+ return Class.forName("org.jetbrains.kotlin.name.FqName")
+ .getConstructor(String::class.java)
+ .newInstance(this)
+ }
+ fun hasAnnotationViaReflection(qName: String): Boolean {
+ val ksType = if (
+ // Note: Technically, we could just make KSTypeWrapper internal and cast to get the
+ // delegate, but since we need to use reflection anyway, just get it via reflection.
+ this.javaClass.canonicalName == "androidx.room.compiler.processing.ksp.KSTypeWrapper") {
+ this.javaClass.methods.find { it.name == "getDelegate" }?.invoke(this)
+ } else {
+ this
+ }
+ val kotlinType =
+ ksType?.javaClass?.methods?.find { it.name == "getKotlinType" }?.invoke(ksType)
+ val kotlinAnnotations =
+ kotlinType?.javaClass
+ ?.methods
+ ?.find { it.name == "getAnnotations" }
+ ?.invoke(kotlinType)
+ return kotlinAnnotations?.javaClass
+ ?.methods
+ ?.find { it.name == "hasAnnotation" }
+ ?.invoke(kotlinAnnotations, qName.toFqName()) == true
+ }
+ return if (annotations.toList().isEmpty()) {
+ // If there are no annotations but KSType#toString() shows annotations, check the underlying
+ // KotlinType for annotations using reflection.
+ toString().startsWith("[") && hasAnnotationViaReflection(qName)
+ } else {
+ annotations.any { it.annotationType.resolve().hasQualifiedNameOrAlias(qName) }
+ }
+}
internal fun KSType.hasSuppressJvmWildcardAnnotation() =
hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
internal fun KSNode.hasSuppressWildcardsAnnotationInHierarchy(): Boolean {
- (this as? KSAnnotated)?.let {
- if (hasSuppressJvmWildcardAnnotation()) {
- return true
- }
- }
- val parent = parent ?: return false
- return parent.hasSuppressWildcardsAnnotationInHierarchy()
- }
\ No newline at end of file
+ (this as? KSAnnotated)?.let {
+ if (hasSuppressJvmWildcardAnnotation()) {
+ return true
+ }
+ }
+ val parent = parent ?: return false
+ return parent.hasSuppressWildcardsAnnotationInHierarchy()
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index bdade61..d797d50 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Variance
@@ -59,21 +60,57 @@
return type
}
- val resolvedType = if (hasTypeVariables(scope.declarationType())) {
+ // First, replace any type aliases in the type with their actual types
+ return type.replaceTypeAliases()
+ // Next, resolve wildcards based on the scope of the type
+ .resolveWildcards(scope)
+ // Next, apply any additional variance changes based on the @JvmSuppressWildcards or
+ // @JvmWildcard annotations on the resolved type.
+ .applyJvmWildcardAnnotations()
+ // Finally, unwrap any delegate types. (Note: as part of resolving wildcards, we wrap
+ // types/type arguments in delegates to avoid loosing annotation information. However,
+ // those delegates may cause issues later if KSP tries to cast the type/argument to a
+ // particular implementation, so we unwrap them here.
+ .removeWrappers()
+ }
+
+ private fun KSType.replaceTypeAliases(typeStack: ReferenceStack = ReferenceStack()): KSType {
+ if (isError || typeStack.queue.contains(this)) {
+ return this
+ }
+ if (declaration is KSTypeAlias) {
+ return (declaration as KSTypeAlias).type.resolve().replaceTypeAliases(typeStack)
+ }
+ return typeStack.withReference(this) {
+ createWrapper(arguments.map { it.replaceTypeAliases(typeStack) })
+ }
+ }
+
+ private fun KSTypeArgument.replaceTypeAliases(typeStack: ReferenceStack): KSTypeArgument {
+ val type = type?.resolve()
+ if (
+ type == null ||
+ type.isError ||
+ variance == Variance.STAR ||
+ typeStack.queue.contains(type)
+ ) {
+ return this
+ }
+ return createWrapper(type.replaceTypeAliases(typeStack), variance)
+ }
+
+ private fun KSType.resolveWildcards(scope: KSTypeVarianceResolverScope): KSType {
+ return if (hasTypeVariables(scope.declarationType())) {
// If the associated declared type contains type variables that were resolved, e.g.
// using "asMemberOf", then it has special rules about how to resolve the types.
getJavaWildcardWithTypeVariables(
- type = type,
+ type = this,
declarationType = getJavaWildcard(scope.declarationType(), scope),
scope = scope,
)
} else {
- getJavaWildcard(type, scope)
+ getJavaWildcard(this, scope)
}
-
- // As a final pass, we apply variance from any @JvmSuppressWildcards or @JvmWildcard
- // annotations on the resolved type.
- return applyJvmWildcardAnnotations(resolvedType)
}
private fun hasTypeVariables(
@@ -99,15 +136,6 @@
if (type.isError || typeStack.queue.contains(type)) {
return type
}
- if (type.declaration is KSTypeAlias) {
- return getJavaWildcard(
- type = (type.declaration as KSTypeAlias).type.resolve(),
- scope = scope,
- typeStack = typeStack,
- typeArgStack = typeArgStack,
- typeParamStack = typeParamStack,
- )
- }
return typeStack.withReference(type) {
val resolvedTypeArgs =
type.arguments.indices.map { i ->
@@ -120,7 +148,7 @@
typeParamStack = typeParamStack,
)
}
- type.replace(resolvedTypeArgs)
+ type.createWrapper(resolvedTypeArgs)
}
}
@@ -158,10 +186,10 @@
if (typeParamStack.indices.none { i ->
(typeParamStack[i].variance == Variance.CONTRAVARIANT ||
typeArgStack[i].variance == Variance.CONTRAVARIANT) &&
- // The declaration and use site variance is ignored when using @JvmWildcard
- // explicitly on a type.
- !typeArgStack[i].hasJvmWildcardAnnotation()
- }) {
+ // The declaration and use site variance is ignored when using @JvmWildcard
+ // explicitly on a type.
+ !typeArgStack[i].hasJvmWildcardAnnotation()
+ }) {
return false
}
} else {
@@ -217,7 +245,7 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
private fun getJavaWildcardWithTypeVariables(
@@ -252,7 +280,7 @@
)
}
}
- type.replace(resolvedTypeArgs)
+ type.createWrapper(resolvedTypeArgs)
}
}
@@ -293,7 +321,7 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
private fun getJavaWildcardWithTypeVariablesForOuterType(
@@ -322,26 +350,27 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
- private fun applyJvmWildcardAnnotations(
- type: KSType,
+ private fun KSType.applyJvmWildcardAnnotations(
typeStack: ReferenceStack = ReferenceStack(),
+ typeArgStack: List<KSTypeArgument> = emptyList(),
): KSType {
- if (type.isError || typeStack.queue.contains(type)) {
- return type
+ if (isError || typeStack.queue.contains(this)) {
+ return this
}
- return typeStack.withReference(type) {
+ return typeStack.withReference(this) {
val resolvedTypeArgs =
- type.arguments.indices.map { i ->
+ arguments.indices.map { i ->
applyJvmWildcardAnnotations(
- typeArg = type.arguments[i],
- typeParameter = type.declaration.typeParameters[i],
+ typeArg = arguments[i],
+ typeParameter = declaration.typeParameters[i],
+ typeArgStack = typeArgStack,
typeStack = typeStack,
)
}
- type.replace(resolvedTypeArgs)
+ createWrapper(resolvedTypeArgs)
}
}
@@ -349,6 +378,7 @@
typeArg: KSTypeArgument,
typeParameter: KSTypeParameter,
typeStack: ReferenceStack,
+ typeArgStack: List<KSTypeArgument>,
): KSTypeArgument {
val type = typeArg.type?.resolve()
if (
@@ -359,28 +389,107 @@
) {
return typeArg
}
- val resolvedType = applyJvmWildcardAnnotations(
- type = type,
- typeStack = typeStack,
- )
+ val resolvedType = type.applyJvmWildcardAnnotations(typeStack, typeArgStack + typeArg)
val resolvedVariance = when {
typeParameter.variance == Variance.INVARIANT &&
typeArg.variance != Variance.INVARIANT -> typeArg.variance
typeArg.hasJvmWildcardAnnotation() -> typeParameter.variance
- typeStack.queue.any { it.hasSuppressJvmWildcardAnnotation() } ||
+ // We only need to check the first type in the stack for @JvmSuppressWildcards.
+ // Any other @JvmSuppressWildcards usages will be placed on the type arguments rather
+ // than the types, so no need to check the rest of the types.
+ typeStack.queue.first().hasSuppressJvmWildcardAnnotation() ||
typeArg.hasSuppressWildcardsAnnotationInHierarchy() ||
+ typeArgStack.any { it.hasSuppressJvmWildcardAnnotation() } ||
typeParameter.hasSuppressWildcardsAnnotationInHierarchy() -> Variance.INVARIANT
else -> typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
- private fun KSType.isTypeParameter(): Boolean {
- return createTypeReference().isTypeParameterReference()
+ private fun KSTypeArgument.createWrapper(
+ newType: KSType,
+ newVariance: Variance
+ ): KSTypeArgument {
+ return KSTypeArgumentWrapper(
+ delegate = (this as? KSTypeArgumentWrapper)?.delegate ?: this,
+ type = newType.createTypeReference(),
+ variance = newVariance
+ )
}
- private fun createTypeArgument(type: KSType, variance: Variance): KSTypeArgument {
- return resolver.getTypeArgument(type.createTypeReference(), variance)
+ private fun KSType.createWrapper(newArguments: List<KSTypeArgument>): KSType {
+ return KSTypeWrapper(
+ delegate = (this as? KSTypeWrapper)?.delegate ?: this,
+ arguments = newArguments
+ )
+ }
+
+ private fun KSType.removeWrappers(typeStack: ReferenceStack = ReferenceStack()): KSType {
+ if (isError || typeStack.queue.contains(this)) {
+ return this
+ }
+ return typeStack.withReference(this) {
+ val delegateType = (this as? KSTypeWrapper)?.delegate ?: this
+ delegateType.replace(arguments.map { it.removeWrappers(typeStack) })
+ }
+ }
+
+ private fun KSTypeArgument.removeWrappers(
+ typeStack: ReferenceStack = ReferenceStack()
+ ): KSTypeArgument {
+ val type = type?.resolve()
+ if (
+ type == null ||
+ type.isError ||
+ variance == Variance.STAR ||
+ typeStack.queue.contains(type)
+ ) {
+ return this
+ }
+ return resolver.getTypeArgument(
+ type.removeWrappers(typeStack).createTypeReference(),
+ variance
+ )
+ }
+}
+
+/**
+ * A wrapper for creating a new [KSType] that allows arguments of type [KSTypeArgumentWrapper].
+ *
+ * Note: This wrapper acts similar to [KSType#replace(KSTypeArgument)]. However, we can't call
+ * [KSType#replace(KSTypeArgument)] directly when using [KSTypeArgumentWrapper] or we'll get an
+ * [IllegalStateException] since KSP tries to cast to its own implementation of [KSTypeArgument].
+ */
+private class KSTypeWrapper(
+ val delegate: KSType,
+ override val arguments: List<KSTypeArgument>
+) : KSType by delegate {
+ override fun toString() = if (arguments.isNotEmpty()) {
+ "${delegate.toString().substringBefore("<")}<${arguments.joinToString(",")}>"
+ } else {
+ delegate.toString()
+ }
+}
+
+/**
+ * A wrapper for creating a new [KSTypeArgument] that delegates to the original argument for
+ * annotations.
+ *
+ * Note: This wrapper acts similar to [Resolver#getTypeArgument(KSTypeReference, Variance)].
+ * However, we can't call [Resolver#getTypeArgument(KSTypeReference, Variance)] directly because
+ * we'll lose information about annotations (e.g. `@JvmSuppressWildcards`) that were on the original
+ * type argument.
+ */
+private class KSTypeArgumentWrapper(
+ val delegate: KSTypeArgument,
+ override val type: KSTypeReference,
+ override val variance: Variance,
+) : KSTypeArgument by delegate {
+ override fun toString() = when (variance) {
+ Variance.INVARIANT -> "${type.resolve()}"
+ Variance.CONTRAVARIANT -> "in ${type.resolve()}"
+ Variance.COVARIANT -> "out ${type.resolve()}"
+ Variance.STAR -> "*"
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
index f39ab11..19f546a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
@@ -27,7 +27,7 @@
* Provides KSType resolution scope for a type.
*/
internal sealed class KSTypeVarianceResolverScope(
- private val annotated: KSAnnotated,
+ val annotated: KSAnnotated,
private val container: KSDeclaration?,
private val asMemberOf: KspType?
) {
@@ -36,8 +36,15 @@
* parameter is in kotlin or the containing class, which inherited the method, is in kotlin.
*/
val needsWildcardResolution: Boolean by lazy {
+ fun nodeForSuppressionCheck(): KSAnnotated? = when (this) {
+ // For property setter and getter methods skip to the enclosing class to check for
+ // suppression annotations to match KAPT.
+ is PropertySetterParameterType,
+ is PropertyGetterMethodReturnType -> annotated.parent?.parent as? KSAnnotated
+ else -> annotated
+ }
(annotated.isInKotlinCode() || container?.isInKotlinCode() == true) &&
- !annotated.hasSuppressWildcardsAnnotationInHierarchy()
+ nodeForSuppressionCheck()?.hasSuppressWildcardsAnnotationInHierarchy() != true
}
private fun KSAnnotated.isInKotlinCode(): Boolean {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 2bd5ed4..be3f4bf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -159,6 +159,11 @@
class MyGenericIn<in T>
class MyGenericOut<out T>
class MyGenericMultipleParameters<T1: MyGeneric<*>, T2: MyGeneric<T1>>
+ interface MyInterface
+ typealias MyInterfaceAlias = MyInterface
+ typealias MyGenericAlias = MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>
+ typealias JSW = JvmSuppressWildcards
+ typealias JW = JvmWildcard
typealias MyLambdaTypeAlias = (@JvmWildcard MyType) -> @JvmWildcard MyType
enum class MyEnum {
VAL1,
@@ -341,6 +346,14 @@
sealedListChild: List<GrandParentSealed.Parent2.Child1>,
jvmWildcard: List<@JvmWildcard String>,
suppressJvmWildcard: List<@JvmSuppressWildcards Number>,
+ suppressJvmWildcardsGeneric1: @JvmSuppressWildcards List<MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric2: List<@JvmSuppressWildcards MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric3: List<MyGenericOut<@JvmSuppressWildcards MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric4: List<MyGenericOut<MyGenericIn<@JvmSuppressWildcards MyGeneric<MyType>>>>,
+ interfaceAlias: List<MyInterfaceAlias>,
+ genericAlias: List<MyGenericAlias>,
+ jvmWildcardTypeAlias: List<@JW String>,
+ suppressJvmWildcardTypeAlias: List<@JSW Number>,
) {
var propWithFinalType: String = ""
var propWithOpenType: Number = 3
@@ -353,6 +366,16 @@
var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
@JvmSuppressWildcards
var propWithOpenTypeButSuppressAnnotation: Number = 3
+ var genericVar: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ @JvmSuppressWildcards var suppressJvmWildcardsGenericVar1: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar2: @JvmSuppressWildcards List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar3: List<@JvmSuppressWildcards MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar4: List<MyGenericIn<@JvmSuppressWildcards MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar5: List<MyGenericIn<MyGenericOut<@JvmSuppressWildcards MyGenericOut<MyType>>>> = TODO()
+ var interfaceAlias: List<MyInterfaceAlias> = TODO()
+ var genericAlias: List<MyGenericAlias> = TODO()
+ var jvmWildcardTypeAlias: List<@JW String> = TODO()
+ var suppressJvmWildcardTypeAlias: List<@JSW Number> = TODO()
fun list(list: List<*>): List<*> { TODO() }
fun listTypeArg(list: List<R>): List<R> { TODO() }
fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
@@ -393,6 +416,43 @@
fun suspendExplicitJvmSuppressWildcard_OnType2(
list: @JvmSuppressWildcards List<Number>
): @JvmSuppressWildcards List<Number> { TODO() }
+ fun interfaceAlias(
+ param: List<MyInterfaceAlias>
+ ): List<MyInterfaceAlias> = TODO()
+ fun explicitJvmSuppressWildcardsOnAlias(
+ param: List<@JvmSuppressWildcards MyInterfaceAlias>,
+ ): List<@JvmSuppressWildcards MyInterfaceAlias> = TODO()
+ fun genericAlias(param: List<MyGenericAlias>): List<MyGenericAlias> = TODO()
+ fun explicitJvmSuppressWildcardsOnGenericAlias(
+ param: List<@JvmSuppressWildcards MyGenericAlias>,
+ ): List<@JvmSuppressWildcards MyGenericAlias> = TODO()
+ fun explicitOutOnInvariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW MyGeneric<out MyGeneric<MyType>>
+ ): @JSW MyGeneric<out MyGeneric<MyType>> { TODO() }
+ fun explicitOutOnInvariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW MyGeneric<MyGeneric<out MyType>>
+ ): @JSW MyGeneric<MyGeneric<out MyType>> { TODO() }
+ fun explicitOutOnVariant_onType1(
+ list: List<out List<Number>>
+ ): List<out List<Number>> { TODO() }
+ fun explicitOutOnVariant_onType2(
+ list: List<List<out Number>>
+ ): List<List<out Number>> { TODO() }
+ fun explicitOutOnVariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW List<out List<Number>>
+ ): @JSW List<out List<Number>> { TODO() }
+ fun explicitOutOnVariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW List<List<out Number>>
+ ): @JSW List<List<out Number>> { TODO() }
+ fun explicitJvmWildcardTypeAlias(
+ list: List<@JW String>
+ ): List<@JW String> { TODO() }
+ fun explicitJvmSuppressWildcardTypeAlias_OnType(
+ list: List<@JSW Number>
+ ): List<@JSW Number> { TODO() }
+ fun explicitJvmSuppressWildcardTypeAlias_OnType2(
+ list: @JSW List<Number>
+ ): @JSW List<Number> { TODO() }
}
""".trimIndent()
), listOf(className)
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index 54717f6..aa246e1 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -59,7 +59,6 @@
public class UiDeviceTest extends BaseTest {
private static final long TIMEOUT_MS = 5_000;
- private static final int GESTURE_MARGIN = 50;
private static final String PACKAGE_NAME = "androidx.test.uiautomator.testapp";
// Defined in 'AndroidManifest.xml'.
private static final String APP_NAME = "UiAutomator Test App";
@@ -300,7 +299,6 @@
assertEquals("I've been clicked!", button.getText());
}
- @Ignore // b/266617096
@Test
public void testSwipe() {
launchTestActivity(SwipeTestActivity.class);
@@ -309,7 +307,7 @@
int width = mDevice.getDisplayWidth();
int height = mDevice.getDisplayHeight();
- mDevice.swipe(GESTURE_MARGIN, height / 2, width - GESTURE_MARGIN, height / 2, 10);
+ mDevice.swipe(width / 10, height / 2, 9 * width / 10, height / 2, 10);
assertTrue(swipeRegion.wait(Until.textEquals("swipe_right"), TIMEOUT_MS));
}
@@ -330,7 +328,6 @@
assertTrue(dragDestination.wait(Until.textEquals("drag_received"), TIMEOUT_MS));
}
- @Ignore // b/266617096
@Test
public void testSwipe_withPointArray() {
launchTestActivity(SwipeTestActivity.class);
@@ -340,9 +337,9 @@
int width = mDevice.getDisplayWidth();
int height = mDevice.getDisplayHeight();
- Point point1 = new Point(GESTURE_MARGIN, height / 2);
+ Point point1 = new Point(width / 10, height / 2);
Point point2 = new Point(width / 2, height / 2);
- Point point3 = new Point(width - GESTURE_MARGIN, height / 2);
+ Point point3 = new Point(9 * width / 10, height / 2);
mDevice.swipe(new Point[]{point1, point2, point3}, 10);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index c91c791..bb9f70b 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -551,7 +551,6 @@
+ "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
}
- @Ignore // b/266617335
@Test
public void testSwipe() {
launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index 36aaeb6..f74a13b 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -24,12 +24,10 @@
import android.graphics.Point;
import android.graphics.Rect;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
-import org.junit.Ignore;
import org.junit.Test;
public class UiObjectTest extends BaseTest {
@@ -119,7 +117,6 @@
assertTrue(expectedDragDest.waitForExists(TIMEOUT_MS));
}
- @Ignore // b/266617747
@Test
public void testSwipeUp() throws Exception {
launchTestActivity(SwipeTestActivity.class);
@@ -141,7 +138,6 @@
assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
}
- @Ignore // b/266617747
@Test
public void testSwipeDown() throws Exception {
launchTestActivity(SwipeTestActivity.class);
@@ -161,7 +157,6 @@
assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
}
- @FlakyTest(bugId = 242761733)
@Test
public void testSwipeLeft() throws Exception {
launchTestActivity(SwipeTestActivity.class);
@@ -181,7 +176,6 @@
assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
}
- @Ignore // b/266617747
@Test
public void testSwipeRight() throws Exception {
launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
index 765aab1..3384042 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
@@ -273,7 +273,6 @@
assertUiObjectNotFound(noNode::flingBackward);
}
- @Ignore // b/266965027
@Test
public void testScrollBackward_vertical() throws Exception {
launchTestActivity(SwipeTestActivity.class);
@@ -285,7 +284,6 @@
assertEquals("swipe_down", scrollRegion.getText());
}
- @Ignore // b/266965027
@Test
public void testScrollBackward_horizontal() throws Exception {
launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
index fda840d..9a1fe18 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
@@ -22,6 +22,7 @@
import android.view.MotionEvent;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SwipeTestActivity extends Activity {
@@ -31,23 +32,20 @@
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
setContentView(R.layout.swipe_test_activity);
TextView swipeRegion = findViewById(R.id.swipe_region);
mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
- // Swipe is using the same logic as fling, except that their directions are
- // opposite under the same finger movement.
- boolean horizontal = Math.abs(vX) > Math.abs(vY);
+ public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
+ boolean horizontal = Math.abs(distanceX) > Math.abs(distanceY);
if (horizontal) {
- swipeRegion.setText(vX < 0 ? "swipe_left" : "swipe_right");
+ swipeRegion.setText(distanceX > 0 ? "swipe_left" : "swipe_right");
} else {
- swipeRegion.setText(vY < 0 ? "swipe_up" : "swipe_down");
+ swipeRegion.setText(distanceY > 0 ? "swipe_up" : "swipe_down");
}
-
return true;
}
});
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 63a0a49..b818481 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -16,6 +16,8 @@
package androidx.test.uiautomator;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
@@ -106,8 +108,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull String className) {
- checkNotNull(className, "className cannot be null");
-
+ requireNonNull(className, "className cannot be null");
// If className starts with a period, assume the package is 'android.widget'
if (className.charAt(0) == '.') {
return clazz("android.widget", className.substring(1));
@@ -126,9 +127,8 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull String packageName, @NonNull String className) {
- checkNotNull(packageName, "packageName cannot be null");
- checkNotNull(className, "className cannot be null");
-
+ requireNonNull(packageName, "packageName cannot be null");
+ requireNonNull(className, "className cannot be null");
return clazz(Pattern.compile(Pattern.quote(
String.format("%s.%s", packageName, className))));
}
@@ -141,8 +141,7 @@
* @return A reference to this {@link BySelector}
*/
public @NonNull BySelector clazz(@NonNull Class clazz) {
- checkNotNull(clazz, "clazz cannot be null");
-
+ requireNonNull(clazz, "clazz cannot be null");
return clazz(Pattern.compile(Pattern.quote(clazz.getName())));
}
@@ -155,8 +154,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector clazz(@NonNull Pattern className) {
- checkNotNull(className, "className cannot be null");
-
+ requireNonNull(className, "className cannot be null");
if (mClazz != null) {
throw new IllegalStateException("Class selector is already defined");
}
@@ -173,8 +171,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector desc(@NonNull String contentDescription) {
- checkNotNull(contentDescription, "contentDescription cannot be null");
-
+ requireNonNull(contentDescription, "contentDescription cannot be null");
return desc(Pattern.compile(Pattern.quote(contentDescription)));
}
@@ -187,8 +184,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descContains(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return desc(RegexHelper.getPatternContains(substring));
}
@@ -201,8 +197,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descStartsWith(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return desc(RegexHelper.getPatternStartsWith(substring));
}
@@ -215,8 +210,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector descEndsWith(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return desc(RegexHelper.getPatternEndsWith(substring));
}
@@ -229,8 +223,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector desc(@NonNull Pattern contentDescription) {
- checkNotNull(contentDescription, "contentDescription cannot be null");
-
+ requireNonNull(contentDescription, "contentDescription cannot be null");
if (mDesc != null) {
throw new IllegalStateException("Description selector is already defined");
}
@@ -247,8 +240,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector pkg(@NonNull String applicationPackage) {
- checkNotNull(applicationPackage, "applicationPackage cannot be null");
-
+ requireNonNull(applicationPackage, "applicationPackage cannot be null");
return pkg(Pattern.compile(Pattern.quote(applicationPackage)));
}
@@ -261,8 +253,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector pkg(@NonNull Pattern applicationPackage) {
- checkNotNull(applicationPackage, "applicationPackage cannot be null");
-
+ requireNonNull(applicationPackage, "applicationPackage cannot be null");
if (mPkg != null) {
throw new IllegalStateException("Package selector is already defined");
}
@@ -279,8 +270,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull String resourceName) {
- checkNotNull(resourceName, "resourceName cannot be null");
-
+ requireNonNull(resourceName, "resourceName cannot be null");
return res(Pattern.compile(Pattern.quote(resourceName)));
}
@@ -294,9 +284,8 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull String resourcePackage, @NonNull String resourceId) {
- checkNotNull(resourcePackage, "resourcePackage cannot be null");
- checkNotNull(resourceId, "resourceId cannot be null");
-
+ requireNonNull(resourcePackage, "resourcePackage cannot be null");
+ requireNonNull(resourceId, "resourceId cannot be null");
return res(Pattern.compile(Pattern.quote(
String.format("%s:id/%s", resourcePackage, resourceId))));
}
@@ -310,8 +299,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector res(@NonNull Pattern resourceName) {
- checkNotNull(resourceName, "resourceName cannot be null");
-
+ requireNonNull(resourceName, "resourceName cannot be null");
if (mRes != null) {
throw new IllegalStateException("Resource name selector is already defined");
}
@@ -328,8 +316,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector text(@NonNull String textValue) {
- checkNotNull(textValue, "textValue cannot be null");
-
+ requireNonNull(textValue, "textValue cannot be null");
return text(Pattern.compile(Pattern.quote(textValue)));
}
@@ -342,8 +329,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textContains(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return text(RegexHelper.getPatternContains(substring));
}
@@ -356,8 +342,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textStartsWith(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return text(RegexHelper.getPatternStartsWith(substring));
}
@@ -370,8 +355,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector textEndsWith(@NonNull String substring) {
- checkNotNull(substring, "substring cannot be null");
-
+ requireNonNull(substring, "substring cannot be null");
return text(RegexHelper.getPatternEndsWith(substring));
}
@@ -383,8 +367,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector text(@NonNull Pattern textValue) {
- checkNotNull(textValue, "textValue cannot be null");
-
+ requireNonNull(textValue, "textValue cannot be null");
if (mText != null) {
throw new IllegalStateException("Text selector is already defined");
}
@@ -581,7 +564,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector hasParent(@NonNull BySelector parentSelector) {
- checkNotNull(parentSelector, "parentSelector cannot be null");
+ requireNonNull(parentSelector, "parentSelector cannot be null");
return hasAncestor(parentSelector, 1);
}
@@ -594,7 +577,7 @@
* @return A reference to this {@link BySelector}.
*/
public @NonNull BySelector hasAncestor(@NonNull BySelector ancestorSelector) {
- checkNotNull(ancestorSelector, "ancestorSelector cannot be null");
+ requireNonNull(ancestorSelector, "ancestorSelector cannot be null");
if (mAncestorSelector != null) {
throw new IllegalStateException("Parent/ancestor selector is already defined");
}
@@ -631,7 +614,7 @@
* @throws IllegalArgumentException if the selector has a parent/ancestor selector
*/
public @NonNull BySelector hasChild(@NonNull BySelector childSelector) {
- checkNotNull(childSelector, "childSelector cannot be null");
+ requireNonNull(childSelector, "childSelector cannot be null");
return hasDescendant(childSelector, 1);
}
@@ -646,7 +629,7 @@
* @throws IllegalArgumentException if the selector has a parent/ancestor selector
*/
public @NonNull BySelector hasDescendant(@NonNull BySelector descendantSelector) {
- checkNotNull(descendantSelector, "descendantSelector cannot be null");
+ requireNonNull(descendantSelector, "descendantSelector cannot be null");
if (descendantSelector.mAncestorSelector != null) {
// Search root is ambiguous with nested parent selectors.
throw new IllegalArgumentException(
@@ -735,11 +718,4 @@
builder.append("]");
return builder.toString();
}
-
- private static <T> T checkNotNull(T value, @NonNull String message) {
- if (value == null) {
- throw new NullPointerException(message);
- }
- return value;
- }
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
index 5cb978a..0eda6d6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
@@ -16,6 +16,8 @@
package androidx.test.uiautomator;
+import static java.util.Objects.requireNonNull;
+
import android.util.SparseArray;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -109,7 +111,7 @@
*/
@NonNull
public UiSelector text(@NonNull String text) {
- checkNotNull(text, "text cannot be null");
+ requireNonNull(text, "text cannot be null");
return buildSelector(SELECTOR_TEXT, text);
}
@@ -125,7 +127,7 @@
*/
@NonNull
public UiSelector textMatches(@NonNull String regex) {
- checkNotNull(regex, "regex cannot be null");
+ requireNonNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
}
@@ -140,7 +142,7 @@
*/
@NonNull
public UiSelector textStartsWith(@NonNull String text) {
- checkNotNull(text, "text cannot be null");
+ requireNonNull(text, "text cannot be null");
return buildSelector(SELECTOR_START_TEXT, text);
}
@@ -155,7 +157,7 @@
*/
@NonNull
public UiSelector textContains(@NonNull String text) {
- checkNotNull(text, "text cannot be null");
+ requireNonNull(text, "text cannot be null");
return buildSelector(SELECTOR_CONTAINS_TEXT, text);
}
@@ -168,7 +170,7 @@
*/
@NonNull
public UiSelector className(@NonNull String className) {
- checkNotNull(className, "className cannot be null");
+ requireNonNull(className, "className cannot be null");
return buildSelector(SELECTOR_CLASS, className);
}
@@ -181,7 +183,7 @@
*/
@NonNull
public UiSelector classNameMatches(@NonNull String regex) {
- checkNotNull(regex, "regex cannot be null");
+ requireNonNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
}
@@ -194,7 +196,7 @@
*/
@NonNull
public <T> UiSelector className(@NonNull Class<T> type) {
- checkNotNull(type, "type cannot be null");
+ requireNonNull(type, "type cannot be null");
return buildSelector(SELECTOR_CLASS, type.getName());
}
@@ -216,7 +218,7 @@
*/
@NonNull
public UiSelector description(@NonNull String desc) {
- checkNotNull(desc, "desc cannot be null");
+ requireNonNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_DESCRIPTION, desc);
}
@@ -236,7 +238,7 @@
*/
@NonNull
public UiSelector descriptionMatches(@NonNull String regex) {
- checkNotNull(regex, "regex cannot be null");
+ requireNonNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
}
@@ -258,7 +260,7 @@
*/
@NonNull
public UiSelector descriptionStartsWith(@NonNull String desc) {
- checkNotNull(desc, "desc cannot be null");
+ requireNonNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_START_DESCRIPTION, desc);
}
@@ -280,7 +282,7 @@
*/
@NonNull
public UiSelector descriptionContains(@NonNull String desc) {
- checkNotNull(desc, "desc cannot be null");
+ requireNonNull(desc, "desc cannot be null");
return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
}
@@ -292,7 +294,7 @@
*/
@NonNull
public UiSelector resourceId(@NonNull String id) {
- checkNotNull(id, "id cannot be null");
+ requireNonNull(id, "id cannot be null");
return buildSelector(SELECTOR_RESOURCE_ID, id);
}
@@ -305,7 +307,7 @@
*/
@NonNull
public UiSelector resourceIdMatches(@NonNull String regex) {
- checkNotNull(regex, "regex cannot be null");
+ requireNonNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
}
@@ -537,7 +539,7 @@
*/
@NonNull
public UiSelector childSelector(@NonNull UiSelector selector) {
- checkNotNull(selector, "selector cannot be null");
+ requireNonNull(selector, "selector cannot be null");
return buildSelector(SELECTOR_CHILD, selector);
}
@@ -561,7 +563,7 @@
*/
@NonNull
public UiSelector fromParent(@NonNull UiSelector selector) {
- checkNotNull(selector, "selector cannot be null");
+ requireNonNull(selector, "selector cannot be null");
return buildSelector(SELECTOR_PARENT, selector);
}
@@ -574,7 +576,7 @@
*/
@NonNull
public UiSelector packageName(@NonNull String name) {
- checkNotNull(name, "name cannot be null");
+ requireNonNull(name, "name cannot be null");
return buildSelector(SELECTOR_PACKAGE_NAME, name);
}
@@ -587,7 +589,7 @@
*/
@NonNull
public UiSelector packageNameMatches(@NonNull String regex) {
- checkNotNull(regex, "regex cannot be null");
+ requireNonNull(regex, "regex cannot be null");
return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
}
@@ -1022,11 +1024,4 @@
builder.append("]");
return builder.toString();
}
-
- private static <T> T checkNotNull(T value, @NonNull String message) {
- if (value == null) {
- throw new NullPointerException(message);
- }
- return value;
- }
}
diff --git a/testutils/testutils-fonts/build.gradle b/testutils/testutils-fonts/build.gradle
index dffd039..911cf9b 100644
--- a/testutils/testutils-fonts/build.gradle
+++ b/testutils/testutils-fonts/build.gradle
@@ -14,12 +14,20 @@
* limitations under the License.
*/
+
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("kotlin-android")
+}
+
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
}
dependencies {
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index f541247..1a5e97a 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.navigation
+import android.util.Log
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@@ -171,9 +172,23 @@
// no WearNavigator.Destinations were added to the navigation backstack (be sure to build
// the NavGraph using androidx.wear.compose.navigation.composable) or because the last entry
// was popped prior to navigating (instead, use navigate with popUpTo).
- val current = if (backStack.isNotEmpty()) backStack.last() else throw IllegalArgumentException(
- "The WearNavigator backstack is empty, there is no navigation destination to display."
- )
+ // If the activity is using FLAG_ACTIVITY_NEW_TASK then it also needs to set
+ // FLAG_ACTIVITY_CLEAR_TASK, otherwise the activity will be created twice,
+ // the first of these with an empty backstack.
+ val current = backStack.lastOrNull()
+
+ if (current == null) {
+ val warningText =
+ "Current backstack entry is empty. Please ensure: \n" +
+ "1. The current WearNavigator navigation backstack is not empty (e.g. by using " +
+ "androidx.wear.compose.navigation.composable to build your nav graph). \n" +
+ "2. The last entry is not popped prior to navigation " +
+ "(instead, use navigate with popUpTo). \n" +
+ "3. If the activity uses FLAG_ACTIVITY_NEW_TASK you should also set " +
+ "FLAG_ACTIVITY_CLEAR_TASK to maintain the backstack consistency."
+
+ Log.w(TAG, warningText)
+ }
val swipeState = state.swipeToDismissBoxState
LaunchedEffect(swipeState.currentValue) {
@@ -200,7 +215,7 @@
modifier = Modifier,
hasBackground = previous != null,
backgroundKey = previous?.id ?: SwipeToDismissKeys.Background,
- contentKey = current.id,
+ contentKey = current?.id ?: SwipeToDismissKeys.Content,
content = { isBackground ->
BoxedStackEntryContent(if (isBackground) previous else current, stateHolder, modifier)
}
@@ -279,3 +294,5 @@
}
}
}
+
+private const val TAG = "SwipeDismissableNavHost"
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -56,6 +56,7 @@
public class StateStore {
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -56,6 +56,7 @@
public class StateStore {
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index 62aa8aa..6dd8857 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -58,6 +58,7 @@
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @UiThread public void setStateEntryValuesProto(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 3ed31ac..bbae3dc 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -16,6 +16,11 @@
package androidx.wear.protolayout.expression.pipeline;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.UiThread;
+
import java.util.List;
/**
@@ -76,6 +81,15 @@
@Override
public void close() {
+ if (Looper.getMainLooper().isCurrentThread()) {
+ closeInternal();
+ } else {
+ new Handler(Looper.getMainLooper()).post(this::closeInternal);
+ }
+ }
+
+ @UiThread
+ private void closeInternal() {
mNodes.stream()
.filter(n -> n instanceof DynamicDataSourceNode)
.forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 068ce4b..3deccea 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -250,8 +250,8 @@
/**
* Gets the quota manager used for limiting the total number of dynamic types in the
- * pipeline, or {@code null} if there are no restriction on the number of dynamic types.
- * If present, the quota manager is used to prevent unreasonably expensive expressions.
+ * pipeline, or {@code null} if there are no restriction on the number of dynamic types. If
+ * present, the quota manager is used to prevent unreasonably expensive expressions.
*/
@Nullable
public QuotaManager getDynamicTypesQuotaManager() {
@@ -303,8 +303,8 @@
MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
TimeGateway timeGateway = config.getTimeGateway();
if (timeGateway == null) {
- timeGateway = new TimeGatewayImpl(uiHandler);
- ((TimeGatewayImpl) timeGateway).enableUpdates();
+ timeGateway = new TimeGatewayImpl(uiHandler);
+ ((TimeGatewayImpl) timeGateway).enableUpdates();
}
this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, timeGateway);
if (config.getSensorGateway() != null) {
@@ -331,7 +331,7 @@
if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
throw new EvaluationException(
"Dynamic type expression limit reached. Try making the dynamic type expression"
- + " shorter or reduce the number of dynamic type expressions.");
+ + " shorter or reduce the number of dynamic type expressions.");
}
return boundDynamicType;
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
index c2ce371..f722bbb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
@@ -18,6 +18,8 @@
import static java.util.stream.Collectors.toMap;
+import android.annotation.SuppressLint;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -41,13 +43,27 @@
* must only be used from the UI thread.
*/
public class StateStore {
+ /**
+ * Maximum number for state entries allowed for this {@link StateStore}.
+ *
+ * <p>The ProtoLayout state model is not designed to handle large volumes of layout provided
+ * state. So we limit the number of state entries to keep the on-the-wire size and state
+ * store update times manageable.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_STATE_ENTRY_COUNT = 100;
@NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
@NonNull
private final Map<String, Set<DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>>>
mRegisteredCallbacks = new ArrayMap<>();
- /** Creates a {@link StateStore}. */
+ /**
+ * Creates a {@link StateStore}.
+ *
+ * @throws IllegalStateException if number of initialState entries is greater than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}.
+ */
@NonNull
public static StateStore create(
@NonNull Map<String, StateEntryBuilders.StateEntryValue> initialState) {
@@ -56,6 +72,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public StateStore(@NonNull Map<String, StateEntryValue> initialState) {
+ if (initialState.size() > MAX_STATE_ENTRY_COUNT) {
+ throw stateTooLargeException(initialState.size());
+ }
mCurrentState.putAll(initialState);
}
@@ -63,6 +82,10 @@
* Sets the given state, replacing the current state.
*
* <p>Informs registered listeners of changed values, invalidates removed values.
+ *
+ * @throws IllegalStateException if number of state entries is greater than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+ * will stay in place.
*/
@UiThread
public void setStateEntryValues(
@@ -74,10 +97,18 @@
* Sets the given state, replacing the current state.
*
* <p>Informs registered listeners of changed values, invalidates removed values.
+ *
+ * @throws IllegalStateException if number of state entries is larger than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+ * will stay in place.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@UiThread
public void setStateEntryValuesProto(@NonNull Map<String, StateEntryValue> newState) {
+ if (newState.size() > MAX_STATE_ENTRY_COUNT) {
+ throw stateTooLargeException(newState.size());
+ }
+
// Figure out which nodes have actually changed.
Set<String> removedKeys = getRemovedKeys(newState);
Map<String, StateEntryValue> changedEntries = getChangedEntries(newState);
@@ -85,10 +116,9 @@
Stream.concat(removedKeys.stream(), changedEntries.keySet().stream())
.forEach(
key -> {
- for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
- callback :
- mRegisteredCallbacks.getOrDefault(
- key, Collections.emptySet())) {
+ for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> callback :
+ mRegisteredCallbacks.getOrDefault(
+ key, Collections.emptySet())) {
callback.onPreUpdate();
}
});
@@ -168,4 +198,12 @@
}
return result;
}
+
+ static IllegalStateException stateTooLargeException(int stateSize) {
+ return new IllegalStateException(
+ String.format(
+ "Too many state entries: %d. The maximum number of allowed state entries "
+ + "is %d.",
+ stateSize, MAX_STATE_ENTRY_COUNT));
+ }
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
index f9f699d..18bae95 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -138,6 +138,7 @@
}
@Override
+ @UiThread
public void close() {
setUpdatesEnabled(false);
registeredCallbacks.clear();
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
index d3a2bb4..ae1630d 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
@@ -16,6 +16,7 @@
package androidx.wear.protolayout.expression.pipeline;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -38,6 +39,9 @@
import org.mockito.InOrder;
import org.mockito.Mockito;
+import java.util.HashMap;
+import java.util.Map;
+
@RunWith(AndroidJUnit4.class)
public class StateStoreTest {
@Rule public Expect mExpect = Expect.create();
@@ -48,6 +52,8 @@
"foo", buildStateEntry("bar"),
"baz", buildStateEntry("foobar")));
+ public StateStoreTest() {}
+
@Test
public void setBuilderApi() {
mStateStoreUnderTest.setStateEntryValues(
@@ -58,6 +64,25 @@
}
@Test
+ public void initState_largeNumberOfEntries_throws() {
+ Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+ for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+ state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+ }
+ assertThrows(IllegalStateException.class, () -> StateStore.create(state));
+ }
+
+ @Test
+ public void newState_largeNumberOfEntries_throws() {
+ Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+ for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+ state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+ }
+ assertThrows(
+ IllegalStateException.class, () -> mStateStoreUnderTest.setStateEntryValues(state));
+ }
+
+ @Test
public void canReadInitialState() {
mExpect.that(mStateStoreUnderTest.getStateEntryValuesProto("foo"))
.isEqualTo(buildStateEntry("bar"));
@@ -88,8 +113,7 @@
@Test
public void setStateFiresListeners() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
mStateStoreUnderTest.registerCallback("foo", cb);
mStateStoreUnderTest.setStateEntryValuesProto(
@@ -101,8 +125,7 @@
@Test
public void setStateFiresOnPreStateUpdateFirst() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
InOrder inOrder = Mockito.inOrder(cb);
@@ -166,8 +189,7 @@
@SuppressWarnings("unchecked")
@Test
public void canUnregisterListeners() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
mStateStoreUnderTest.registerCallback("foo", cb);
mStateStoreUnderTest.setStateEntryValuesProto(
@@ -183,8 +205,7 @@
}
@SuppressWarnings("unchecked")
- private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
- buildStateUpdateCallbackMock() {
+ private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> buildStateUpdateCallbackMock() {
// This needs an unchecked cast because of the generic; this method just centralizes the
// warning suppression.
return mock(DynamicTypeValueReceiverWithPreUpdate.class);
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/action.proto b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
index 72e7e95..589a31c 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/action.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
@@ -54,11 +54,11 @@
// A launch action to send an intent to an Android activity.
message AndroidActivity {
- // The package name to send the intent to, for example, "com.google.weather".
+ // The package name to send the intent to, for example, "com.example.weather".
string package_name = 1;
// The fully qualified class name (including the package) to send the intent
- // to, for example, "com.google.weather.WeatherOverviewActivity".
+ // to, for example, "com.example.weather.WeatherOverviewActivity".
string class_name = 2;
// The extras to be included in the intent.
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index 9b7d7ac..d89a564 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -168,7 +168,8 @@
}
@Test
- public void buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+ public void
+ buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
List<Float> results = new ArrayList<>();
float endValue = 10.0f;
DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -183,7 +184,7 @@
@Test
public void
- buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+ buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
List<Float> results = new ArrayList<>();
float endValue = 10.0f;
DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -217,7 +218,7 @@
@Test
public void
- buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndVal() {
+ buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndValue() {
List<Integer> results = new ArrayList<>();
DynamicColor dynamicColor = animatableFixedColor(0, 1);
ColorProp colorProp = ColorProp.newBuilder().setDynamicValue(dynamicColor).build();
@@ -1719,8 +1720,7 @@
Repeatable.newBuilder()
.setRepeatMode(
RepeatMode
- .REPEAT_MODE_REVERSE
- )
+ .REPEAT_MODE_REVERSE)
.setIterations(iterations)
.setForwardRepeatOverride(
alternateParameters)
@@ -1813,13 +1813,12 @@
ProtoLayoutDynamicDataPipeline pipeline =
enableAnimations
? new ProtoLayoutDynamicDataPipeline(
- /* sensorGateway= */ null,
+ /* sensorGateway= */ null,
mStateStore,
new FixedQuotaManagerImpl(MAX_VALUE),
new FixedQuotaManagerImpl(MAX_VALUE))
: new ProtoLayoutDynamicDataPipeline(
- /* sensorGateway= */ null,
- mStateStore);
+ /* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1842,7 +1841,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1860,7 +1859,7 @@
AddToListCallback<Integer> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1878,7 +1877,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1896,7 +1895,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1914,7 +1913,7 @@
AddToListCallback<Integer> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 9e263bf..9a529c2 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -4205,8 +4205,9 @@
private static FadeInTransition.Builder fadeIn(int delay) {
return FadeInTransition.newBuilder()
.setAnimationSpec(
- AnimationSpec.newBuilder().setAnimationParameters(
- AnimationParameters.newBuilder().setDelayMillis(delay)));
+ AnimationSpec.newBuilder()
+ .setAnimationParameters(
+ AnimationParameters.newBuilder().setDelayMillis(delay)));
}
private LayoutElement textFadeInSlideIn(String text) {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
index 8889ec0..0fb8101 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
@@ -149,9 +149,17 @@
return this;
}
+ private static final int MAX_STATE_SIZE = 30;
+
/** Builds an instance from accumulated values. */
@NonNull
public State build() {
+ if (mImpl.getIdToValueMap().size() > MAX_STATE_SIZE) {
+ throw new IllegalStateException(
+ String.format(
+ "State size is too large: %d. Maximum " + "allowed state size is %d.",
+ mImpl.getIdToValueMap().size(), MAX_STATE_SIZE));
+ }
return new State(mImpl.build(), mFingerprint);
}
}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 184aa81..3fb2267 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -202,15 +202,18 @@
public View inflate(@NonNull ViewGroup parent) {
String errorMessage =
"This method only works with the deprecated constructors that accept Layout and"
- + " Resources.";
+ + " Resources.";
try {
// Waiting for the result from future for backwards compatibility.
return inflateLayout(
- checkNotNull(mLayout, errorMessage),
- checkNotNull(mResources, errorMessage),
- parent).get(10, TimeUnit.SECONDS);
- } catch (ExecutionException | InterruptedException | CancellationException |
- TimeoutException e) {
+ checkNotNull(mLayout, errorMessage),
+ checkNotNull(mResources, errorMessage),
+ parent)
+ .get(10, TimeUnit.SECONDS);
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException
+ | TimeoutException e) {
// Wrap checked exceptions to avoid changing the method signature.
throw new RuntimeException("Rendering tile has not successfully finished.", e);
}
@@ -219,13 +222,12 @@
/**
* Inflates a Tile into {@code parent}.
*
- * @param layout The portion of the Tile to render.
+ * @param layout The portion of the Tile to render.
* @param resources The resources for the Tile.
- * @param parent The view to attach the tile into.
+ * @param parent The view to attach the tile into.
* @return The future with the first child that was inflated. This may be null if the Layout is
- * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
- * contains an
- * unsupported inner type.
+ * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
+ * contains an unsupported inner type.
*/
@NonNull
public ListenableFuture<View> inflateAsync(
@@ -241,7 +243,6 @@
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup parent) {
ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
- return FluentFuture.from(result)
- .transform(ignored -> parent.getChildAt(0), mUiExecutor);
+ return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
}
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 8887171d..949c589 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -1836,13 +1836,13 @@
*
* Returns this Builder to allow chaining.
*/
- fun setListEntryCollection(timelineEntries: Collection<ComplicationData>?) = apply {
- if (timelineEntries == null) {
+ fun setListEntryCollection(listEntries: Collection<ComplicationData>?) = apply {
+ if (listEntries == null) {
fields.remove(EXP_FIELD_LIST_ENTRIES)
} else {
fields.putParcelableArray(
EXP_FIELD_LIST_ENTRIES,
- timelineEntries
+ listEntries
.map { data ->
data.fields.putInt(EXP_FIELD_LIST_ENTRY_TYPE, data.type)
data.fields
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 94db677..ddf43de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -18,7 +18,7 @@
import android.icu.util.ULocale
import android.support.wearable.complications.ComplicationData as WireComplicationData
-import android.support.wearable.complications.ComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
import android.support.wearable.complications.ComplicationText as WireComplicationText
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
@@ -41,17 +41,19 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
/**
* Evaluates a [WireComplicationData] with
* [androidx.wear.protolayout.expression.DynamicBuilders.DynamicType] within its fields.
- *
- * Due to [WireComplicationData]'s shallow copy strategy the input is modified in-place.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
@@ -65,27 +67,101 @@
*
* The expression is evaluated _separately_ on each flow collection.
*/
- fun evaluate(unevaluatedData: WireComplicationData) =
- flow<WireComplicationData> {
- val state: MutableStateFlow<State> = unevaluatedData.buildState()
- state.value.use {
- val evaluatedData: Flow<WireComplicationData> =
- state
- .mapNotNull {
- when {
- // Emitting INVALID_DATA if there's an invalid receiver.
- it.invalidReceivers.isNotEmpty() -> INVALID_DATA
- // Emitting the data if all pending receivers are done and all
- // pre-updates are satisfied.
- it.pendingReceivers.isEmpty() -> it.data
- // Skipping states that are not ready for be emitted.
- else -> null
- }
- }
- .distinctUntilChanged()
- emitAll(evaluatedData)
+ fun evaluate(unevaluatedData: WireComplicationData): Flow<WireComplicationData> =
+ evaluateTopLevelFields(unevaluatedData)
+ // Combining with fields that are made of WireComplicationData.
+ .combineWithDataList(unevaluatedData.timelineEntries) { entries ->
+ // Timeline entries are set on the built WireComplicationData.
+ WireComplicationData.Builder(
+ this@combineWithDataList.build().apply { setTimelineEntryCollection(entries) }
+ )
}
+ .combineWithDataList(unevaluatedData.listEntries) { setListEntryCollection(it) }
+ // Must be last, as it overwrites INVALID_DATA.
+ .combineWithEvaluatedPlaceholder(unevaluatedData.placeholder)
+ .distinctUntilChanged()
+
+ /** Evaluates "local" fields, excluding fields of type WireComplicationData. */
+ private fun evaluateTopLevelFields(
+ unevaluatedData: WireComplicationData
+ ): Flow<WireComplicationData> = flow {
+ val state: MutableStateFlow<State> = unevaluatedData.buildState()
+ state.value.use {
+ val evaluatedData: Flow<WireComplicationData> =
+ state.mapNotNull {
+ when {
+ // Emitting INVALID_DATA if there's an invalid receiver.
+ it.invalidReceivers.isNotEmpty() -> INVALID_DATA
+ // Emitting the data if all pending receivers are done and all
+ // pre-updates are satisfied.
+ it.pendingReceivers.isEmpty() -> it.data
+ // Skipping states that are not ready for be emitted.
+ else -> null
+ }
+ }
+ emitAll(evaluatedData)
}
+ }
+
+ /**
+ * Combines the receiver with the evaluated version of the provided list.
+ *
+ * If the receiver [Flow] emits [INVALID_DATA] or the input list is null or empty, this does not
+ * mutate the flow and does not wait for the entries to finish evaluating.
+ *
+ * If even one [WireComplicationData] within the provided list is evaluated to [INVALID_DATA],
+ * the output [Flow] becomes [INVALID_DATA] (the receiver [Flow] is ignored).
+ */
+ private fun Flow<WireComplicationData>.combineWithDataList(
+ unevaluatedEntries: List<WireComplicationData>?,
+ setter:
+ WireComplicationData.Builder.(
+ List<WireComplicationData>
+ ) -> WireComplicationData.Builder,
+ ): Flow<WireComplicationData> {
+ if (unevaluatedEntries.isNullOrEmpty()) return this
+ val evaluatedEntriesFlow: Flow<List<WireComplicationData>> =
+ combine(unevaluatedEntries.map { evaluate(it) })
+
+ return this.combine(evaluatedEntriesFlow).map {
+ (data: WireComplicationData, evaluatedEntries: List<WireComplicationData>?) ->
+ // Not mutating if invalid.
+ if (data === INVALID_DATA) return@map data
+ // An entry is invalid, emitting invalid.
+ if (evaluatedEntries.any { it === INVALID_DATA }) return@map INVALID_DATA
+ // All is well, mutating the input.
+ return@map WireComplicationData.Builder(data).setter(evaluatedEntries).build()
+ }
+ }
+
+ /**
+ * Same as [combineWithDataList], but sets the evaluated placeholder ONLY when the receiver
+ * [Flow] emits [TYPE_NO_DATA], or [keepExpression] is true, otherwise clears it and does not
+ * wait for the placeholder to finish evaluating.
+ *
+ * If the placeholder is not required (per the above paragraph), this doesn't wait for it.
+ */
+ private fun Flow<WireComplicationData>.combineWithEvaluatedPlaceholder(
+ unevaluatedPlaceholder: WireComplicationData?
+ ): Flow<WireComplicationData> {
+ if (unevaluatedPlaceholder == null) return this
+ val evaluatedPlaceholderFlow: Flow<WireComplicationData> = evaluate(unevaluatedPlaceholder)
+
+ return this.combine(evaluatedPlaceholderFlow).map {
+ (data: WireComplicationData, evaluatedPlaceholder: WireComplicationData?) ->
+ if (!keepExpression && data.type != TYPE_NO_DATA) {
+ // Clearing the placeholder when data is not TYPE_NO_DATA (it was meant as an
+ // expression fallback).
+ return@map WireComplicationData.Builder(data).setPlaceholder(null).build()
+ }
+ // Placeholder required but invalid, emitting invalid.
+ if (evaluatedPlaceholder === INVALID_DATA) return@map INVALID_DATA
+ // All is well, mutating the input.
+ return@map WireComplicationData.Builder(data)
+ .setPlaceholder(evaluatedPlaceholder)
+ .build()
+ }
+ }
private suspend fun WireComplicationData.buildState() =
MutableStateFlow(State(this)).apply {
@@ -177,7 +253,7 @@
* [ComplicationEvaluationResultReceiver] that are evaluating it.
*/
private inner class State(
- val data: ComplicationData,
+ val data: WireComplicationData,
val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
@@ -317,3 +393,35 @@
runnable.run()
}
}
+
+/** Replacement of [kotlinx.coroutines.flow.combine], which doesn't seem to work. */
+internal fun <T> combine(flows: List<Flow<T>>): Flow<List<T>> = flow {
+ data class ValueExists(val value: T?, val exists: Boolean)
+ val latest = MutableStateFlow(List(flows.size) { ValueExists(null, false) })
+ @Suppress("UNCHECKED_CAST") // Flow<List<T?>> -> Flow<List<T>> safe after filtering exists.
+ emitAll(
+ flows
+ .mapIndexed { i, flow -> flow.map { i to it } } // List<Flow<Int, T>> (indexed flows)
+ .merge() // Flow<Int, T>
+ .map { (i, value) ->
+ // Updating latest and returning the current latest.
+ latest.updateAndGet {
+ val newLatest = it.toMutableList()
+ newLatest[i] = ValueExists(value, true)
+ newLatest
+ }
+ } // Flow<List<ValueExists>>
+ // Filtering emissions until we have all values.
+ .filter { values -> values.all { it.exists } }
+ // Flow<List<T>> + defensive copy.
+ .map { values -> values.map { it.value } } as Flow<List<T>>
+ )
+}
+
+/**
+ * Another replacement of [kotlinx.coroutines.flow.combine] which is similar to
+ * `combine(List<Flow<T>>)` but allows different types for each flow.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun <T1, T2> Flow<T1>.combine(other: Flow<T2>): Flow<Pair<T1, T2>> =
+ combine(listOf(this as Flow<*>, other as Flow<*>)).map { (a, b) -> (a as T1) to (b as T2) }
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index c913c25..3a22917 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -17,6 +17,8 @@
package androidx.wear.watchface.complications.data
import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_SHORT_TEXT
import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.util.Log
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
@@ -58,10 +60,7 @@
@Test
fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
- val data =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
- .setRangedValue(10f)
- .build()
+ val data = WireComplicationData.Builder(TYPE_NO_DATA).setRangedValue(10f).build()
val evaluator = ComplicationDataExpressionEvaluator()
@@ -81,7 +80,7 @@
) {
SET_IMMEDIATELY_WHEN_ALL_DATA_AVAILABLE(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(WireComplicationText(DynamicString.constant("Long Text")))
.setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
@@ -90,23 +89,29 @@
.setContentDescription(
WireComplicationText(DynamicString.constant("Description"))
)
- .build(),
+ .setPlaceholder(constantData("Placeholder"))
+ .setListEntryCollection(listOf(constantData("List")))
+ .build()
+ .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) },
states = listOf(),
evaluated =
listOf(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setLongText(WireComplicationText("Long Text"))
.setLongTitle(WireComplicationText("Long Title"))
.setShortText(WireComplicationText("Short Text"))
.setShortTitle(WireComplicationText("Short Title"))
.setContentDescription(WireComplicationText("Description"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
),
),
SET_ONLY_AFTER_ALL_FIELDS_EVALUATED(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
.setLongText(WireComplicationText(DynamicString.fromState("long_text")))
.setLongTitle(WireComplicationText(DynamicString.fromState("long_title")))
@@ -115,7 +120,10 @@
.setContentDescription(
WireComplicationText(DynamicString.fromState("description"))
)
- .build(),
+ .setPlaceholder(stateData("placeholder"))
+ .setListEntryCollection(listOf(stateData("list")))
+ .build()
+ .also { it.setTimelineEntryCollection(listOf(stateData("timeline"))) },
states =
aggregate(
// Each map piles on top of the previous ones.
@@ -124,25 +132,38 @@
mapOf("long_title" to StateEntryValue.fromString("Long Title")),
mapOf("short_text" to StateEntryValue.fromString("Short Text")),
mapOf("short_title" to StateEntryValue.fromString("Short Title")),
- // Only the last one will trigger an evaluated data.
mapOf("description" to StateEntryValue.fromString("Description")),
+ mapOf("placeholder" to StateEntryValue.fromString("Placeholder")),
+ mapOf("list" to StateEntryValue.fromString("List")),
+ mapOf("timeline" to StateEntryValue.fromString("Timeline")),
+ // Only the last one will trigger an evaluated data.
),
evaluated =
listOf(
- INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ // Before any state is available.
+ INVALID_DATA,
+ // INVALID_DATA with placeholder, after it's available (and others aren't).
+ WireComplicationData.Builder(INVALID_DATA)
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build(),
+ // Evaluated data with after everything is available.
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setLongText(WireComplicationText("Long Text"))
.setLongTitle(WireComplicationText("Long Title"))
.setShortText(WireComplicationText("Short Text"))
.setShortTitle(WireComplicationText("Short Title"))
.setContentDescription(WireComplicationText("Description"))
+ // Not trimmed for TYPE_NO_DATA.
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
),
),
SET_TO_EVALUATED_IF_ALL_FIELDS_VALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("valid")))
.build(),
@@ -153,7 +174,7 @@
evaluated =
listOf(
INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText("Valid"))
.setShortText(WireComplicationText("Valid"))
.build(),
@@ -161,7 +182,7 @@
),
SET_TO_NO_DATA_IF_FIRST_STATE_IS_INVALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("invalid")))
.build(),
@@ -177,7 +198,7 @@
),
SET_TO_NO_DATA_IF_LAST_STATE_IS_INVALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("invalid")))
.build(),
@@ -192,13 +213,43 @@
evaluated =
listOf(
INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText("Valid"))
.setShortText(WireComplicationText("Valid"))
.build(),
INVALID_DATA, // After it was invalidated.
),
),
+ SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_IF_NOT_NO_DATA(
+ expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build(),
+ states = listOf(),
+ evaluated =
+ listOf(
+ // No placeholder.
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .build(),
+ )
+ ),
+ SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_EVEN_IF_PLACEHOLDER_INVALID_IF_NOT_NO_DATA(
+ expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(stateData("placeholder"))
+ .build(),
+ states = listOf(), // placeholder state not set.
+ evaluated =
+ listOf(
+ // No placeholder.
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .build(),
+ )
+ ),
}
@Test
@@ -231,7 +282,7 @@
@Test
fun evaluate_cancelled_cleansUp() = runBlocking {
val expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(
// Uses TimeGateway, which needs cleaning up.
DynamicInstant.withSecondsPrecision(Instant.EPOCH)
@@ -262,19 +313,22 @@
@Test
fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
val expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(WireComplicationText(DynamicString.constant("Long Text")))
.setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
.setShortText(WireComplicationText(DynamicString.constant("Short Text")))
.setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
.setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+ .setPlaceholder(constantData("Placeholder"))
+ .setListEntryCollection(listOf(constantData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) }
val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
assertThat(evaluator.evaluate(expressed).firstOrNull())
.isEqualTo(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(
@@ -292,6 +346,29 @@
.setContentDescription(
WireComplicationText("Description", DynamicString.constant("Description"))
)
+ .setPlaceholder(evaluatedWithConstantData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedWithConstantData("List")))
+ .build()
+ .also {
+ it.setTimelineEntryCollection(listOf(evaluatedWithConstantData("Timeline")))
+ },
+ )
+ }
+
+ @Test
+ fun evaluate_keepExpressionNotNoData_doesNotTrimPlaceholder() = runBlocking {
+ val expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build()
+ val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
+
+ assertThat(evaluator.evaluate(expressed).firstOrNull())
+ .isEqualTo(
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
.build()
)
}
@@ -300,5 +377,25 @@
/** Converts `[{a: A}, {b: B}, {c: C}]` to `[{a: A}, {a: A, b: B}, {a: A, b: B, c: C}]`. */
fun <K, V> aggregate(vararg maps: Map<K, V>): List<Map<K, V>> =
maps.fold(listOf()) { acc, map -> acc + ((acc.lastOrNull() ?: mapOf()) + map) }
+
+ fun constantData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(DynamicString.constant(value)))
+ .build()
+
+ fun stateData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(DynamicString.fromState(value)))
+ .build()
+
+ fun evaluatedData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(value))
+ .build()
+
+ fun evaluatedWithConstantData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(value, DynamicString.constant(value)))
+ .build()
}
}
diff --git a/window/window/proguard-rules.pro b/window/window/proguard-rules.pro
index 609e1cc1..b8cf236 100644
--- a/window/window/proguard-rules.pro
+++ b/window/window/proguard-rules.pro
@@ -22,4 +22,6 @@
androidx.window.layout.adapter.sidecar.DistinctElementSidecarCallback {
public *** onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState);
public *** onWindowLayoutChanged(android.os.IBinder, androidx.window.sidecar.SidecarWindowLayoutInfo);
-}
\ No newline at end of file
+}
+# Required for window area API reflection guard
+-keep interface androidx.window.area.reflectionguard.* {*;}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
new file mode 100644
index 0000000..b385670
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -0,0 +1,257 @@
+/*
+ * 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.window.area.reflectionguard
+
+import android.app.Activity
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.View
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.ExtensionWindowAreaStatus
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.extensions.core.util.function.Consumer
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+
+/**
+ * Unit test for [WindowAreaComponentValidator]
+ */
+class WindowAreaComponentValidatorTest {
+
+ /**
+ * Test that validator returns true if the component fully implements [WindowAreaComponent]
+ */
+ @Test
+ fun isWindowAreaComponentValid_fullImplementation() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentFullImplementation::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentFullImplementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
+ * implementation.
+ */
+ @Test
+ fun isWindowAreaComponentValid_apiLevel2() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV2Implementation::class.java, 2))
+ assertFalse(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ IncompleteWindowAreaComponentApiV2Implementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns correct results for API Level 3 [WindowAreaComponent]
+ * implementation.
+ */
+ @Test
+ fun isWindowAreaComponentValid_apiLevel3() {
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV3Implementation::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ WindowAreaComponentApiV3Implementation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the component implementation is incomplete
+ */
+ @Test
+ fun isWindowAreaComponentValid_falseIfIncompleteImplementation() {
+ assertFalse(
+ WindowAreaComponentValidator.isWindowAreaComponentValid(
+ IncompleteWindowAreaComponentApiV2Implementation::class.java, 2))
+ }
+
+ /**
+ * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid
+ */
+ @Test
+ fun isExtensionWindowAreaStatusValid_trueIfValid() {
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ ValidExtensionWindowAreaStatus::class.java, 2))
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ ValidExtensionWindowAreaStatus::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete
+ */
+ @Test
+ fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ IncompleteExtensionWindowAreaStatus::class.java, 2))
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+ IncompleteExtensionWindowAreaStatus::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns true if the [ExtensionWindowAreaPresentation] is valid
+ */
+ @Test
+ fun isExtensionWindowAreaPresentationValid_trueIfValid() {
+ assertTrue(
+ WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+ ValidExtensionWindowAreaPresentation::class.java, 3))
+ }
+
+ /**
+ * Test that validator returns false if the [ExtensionWindowAreaPresentation] is incomplete
+ */
+ @Test
+ fun isExtensionWindowAreaPresentationValid_falseIfIncomplete() {
+ assertFalse(
+ WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+ IncompleteExtensionWindowAreaPresentation::class.java, 3))
+ }
+
+ private class WindowAreaComponentFullImplementation : WindowAreaComponent {
+ override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun endRearDisplaySession() {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class WindowAreaComponentApiV2Implementation : WindowAreaComponentApi2Requirements {
+ override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun endRearDisplaySession() {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class WindowAreaComponentApiV3Implementation : WindowAreaComponentApi3Requirements {
+ override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun endRearDisplaySession() {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun addRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun removeRearDisplayPresentationStatusListener(
+ consumer: Consumer<ExtensionWindowAreaStatus>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun startRearDisplayPresentationSession(
+ activity: Activity,
+ consumer: Consumer<Int>
+ ) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun endRearDisplayPresentationSession() {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteWindowAreaComponentApiV2Implementation {
+ @Suppress("UNUSED_PARAMETER")
+ fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class ValidExtensionWindowAreaPresentation : ExtensionWindowAreaPresentation {
+ override fun getPresentationContext(): Context {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun setPresentationView(view: View) {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteExtensionWindowAreaPresentation {
+ fun getPresentationContext(): Context {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class ValidExtensionWindowAreaStatus : ExtensionWindowAreaStatus {
+ override fun getWindowAreaStatus(): Int {
+ throw NotImplementedError("Not implemented")
+ }
+
+ override fun getWindowAreaDisplayMetrics(): DisplayMetrics {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+
+ private class IncompleteExtensionWindowAreaStatus {
+ fun getWindowAreaStatus(): Int {
+ throw NotImplementedError("Not implemented")
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
new file mode 100644
index 0000000..9153250
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
@@ -0,0 +1,37 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+
+/**
+ * API requirements for [ExtensionWindowAreaPresentation]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaPresentationRequirements {
+ /** @see ExtensionWindowAreaPresentation#getPresentationContext */
+ @NonNull
+ Context getPresentationContext();
+
+ /** @see ExtensionWindowAreaPresentation#setPresentationView */
+ void setPresentationView(@NonNull View view);
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
new file mode 100644
index 0000000..14ba999
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
@@ -0,0 +1,36 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+
+/**
+ * API requirements for [ExtensionWindowAreaStatus]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaStatusRequirements {
+ /** @see ExtensionWindowAreaStatus#getWindowAreaStatus */
+ int getWindowAreaStatus();
+
+ /** @see ExtensionWindowAreaStatus#getWindowAreaDisplayMetrics */
+ @NonNull
+ DisplayMetrics getWindowAreaDisplayMetrics();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
new file mode 100644
index 0000000..0ab78c0
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
@@ -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.
+ */
+
+package androidx.window.area.reflectionguard;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * This file defines the Vendor API Level 2 Requirements for WindowAreaComponent. This is used
+ * in the client library to perform reflection guard to ensure that the OEM extension implementation
+ * is complete.
+ *
+ * @see WindowAreaComponent
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface WindowAreaComponentApi2Requirements {
+
+ /** @see WindowAreaComponent#addRearDisplayStatusListener */
+ void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#removeRearDisplayStatusListener */
+ void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#startRearDisplaySession */
+ void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#endRearDisplaySession */
+ void endRearDisplaySession();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
new file mode 100644
index 0000000..aad8216
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -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.window.area.reflectionguard;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
+
+
+/**
+ * This file defines the Vendor API Level 3 Requirements for WindowAreaComponent. This is used
+ * in the client library to perform reflection guard to ensure that the OEM extension implementation
+ * is complete.
+ *
+ * @see WindowAreaComponent
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface WindowAreaComponentApi3Requirements extends WindowAreaComponentApi2Requirements {
+
+ /** @see WindowAreaComponent#addRearDisplayPresentationStatusListener */
+ void addRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+ /** @see WindowAreaComponent#removeRearDisplayPresentationStatusListener */
+ void removeRearDisplayPresentationStatusListener(
+ @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+ /** @see WindowAreaComponent#startRearDisplayPresentationSession */
+ void startRearDisplayPresentationSession(@NonNull Activity activity,
+ @NonNull Consumer<Integer> consumer);
+
+ /** @see WindowAreaComponent#endRearDisplayPresentationSession */
+ void endRearDisplayPresentationSession();
+
+ /** @see WindowAreaComponent#getRearDisplayPresentation */
+ @Nullable
+ ExtensionWindowAreaPresentation getRearDisplayPresentation();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
new file mode 100644
index 0000000..d48d2ab
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.window.area.reflectionguard
+
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.reflection.ReflectionUtils.validateImplementation
+
+/**
+ * Utility class to validate [WindowAreaComponent] implementation.
+ */
+internal object WindowAreaComponentValidator {
+
+ internal fun isWindowAreaComponentValid(windowAreaComponent: Class<*>, apiLevel: Int): Boolean {
+ return when {
+ apiLevel <= 1 -> false
+ apiLevel == 2 -> validateImplementation(
+ windowAreaComponent, WindowAreaComponentApi2Requirements::class.java
+ )
+ else -> validateImplementation(
+ windowAreaComponent, WindowAreaComponentApi3Requirements::class.java
+ )
+ }
+ }
+
+ internal fun isExtensionWindowAreaStatusValid(
+ extensionWindowAreaStatus: Class<*>,
+ apiLevel: Int
+ ): Boolean {
+ return when {
+ apiLevel <= 1 -> false
+ else -> validateImplementation(
+ extensionWindowAreaStatus, ExtensionWindowAreaStatusRequirements::class.java
+ )
+ }
+ }
+
+ internal fun isExtensionWindowAreaPresentationValid(
+ extensionWindowAreaPresentation: Class<*>,
+ apiLevel: Int
+ ): Boolean {
+ return when {
+ apiLevel <= 2 -> false
+ else -> validateImplementation(
+ extensionWindowAreaPresentation, ExtensionWindowAreaPresentation::class.java
+ )
+ }
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
index 326486e..ed8b7ee 100644
--- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
+++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
@@ -80,4 +80,16 @@
internal fun Method.doesReturn(clazz: Class<*>): Boolean {
return returnType.equals(clazz)
}
-}
\ No newline at end of file
+
+ internal fun validateImplementation(
+ implementation: Class<*>,
+ requirements: Class<*>,
+ ): Boolean {
+ return requirements.methods.all {
+ validateReflection("${implementation.name}#${it.name} is not valid") {
+ val implementedMethod = implementation.getMethod(it.name, *it.parameterTypes)
+ implementedMethod.isPublic && implementedMethod.doesReturn(it.returnType)
+ }
+ }
+ }
+}
diff --git a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
index 639d5b1..0660ca2 100644
--- a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
+++ b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
@@ -23,6 +23,7 @@
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.SystemClock
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.background.gcm.GcmTaskConverter.EXECUTION_WINDOW_SIZE_IN_SECONDS
import com.google.android.gms.gcm.Task
@@ -45,7 +46,7 @@
@Before
fun setUp() {
- mTaskConverter = spy(GcmTaskConverter())
+ mTaskConverter = spy(GcmTaskConverter(SystemClock()))
}
@Test
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
index f8ffaba..71f595e 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
@@ -19,6 +19,7 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.work.Clock;
import androidx.work.Logger;
import androidx.work.impl.Scheduler;
import androidx.work.impl.model.WorkSpec;
@@ -38,14 +39,14 @@
private final GcmNetworkManager mNetworkManager;
private final GcmTaskConverter mTaskConverter;
- public GcmScheduler(@NonNull Context context) {
+ public GcmScheduler(@NonNull Context context, @NonNull Clock clock) {
boolean isPlayServicesAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
if (!isPlayServicesAvailable) {
throw new IllegalStateException("Google Play Services not available");
}
mNetworkManager = GcmNetworkManager.getInstance(context);
- mTaskConverter = new GcmTaskConverter();
+ mTaskConverter = new GcmTaskConverter(clock);
}
@Override
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
index 6dec44e..fb6e1ae 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import androidx.work.Clock;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.impl.model.WorkSpec;
@@ -40,7 +41,6 @@
* Converts a {@link androidx.work.impl.model.WorkSpec} to a {@link Task}.
*/
public class GcmTaskConverter {
-
/**
* This is referring to the size of the execution window in seconds. {@link GcmNetworkManager}
* requires that we specify a window of time relative to {@code now} where a {@link Task}
@@ -53,6 +53,11 @@
public static final long EXECUTION_WINDOW_SIZE_IN_SECONDS = 5L;
static final String EXTRA_WORK_GENERATION = "androidx.work.impl.background.gcm.GENERATION";
+ private final Clock mClock;
+
+ public GcmTaskConverter(@NonNull Clock clock) {
+ mClock = clock;
+ }
OneoffTask convert(@NonNull WorkSpec workSpec) {
Bundle extras = new Bundle();
@@ -81,7 +86,7 @@
*/
@VisibleForTesting
public long now() {
- return System.currentTimeMillis();
+ return mClock.currentTimeMillis();
}
private static Task.Builder applyConstraints(
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
index ffce534..5cba6e9 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
@@ -20,6 +20,7 @@
import androidx.test.filters.MediumTest
import androidx.work.OneTimeWorkRequest
import androidx.work.RunnableScheduler
+import androidx.work.SystemClock
import androidx.work.worker.TestWorker
import org.junit.Before
import org.junit.Test
@@ -40,7 +41,7 @@
fun setUp() {
mScheduler = mock(GreedyScheduler::class.java)
mRunnableScheduler = mock(RunnableScheduler::class.java)
- mDelayedWorkTracker = DelayedWorkTracker(mScheduler, mRunnableScheduler)
+ mDelayedWorkTracker = DelayedWorkTracker(mScheduler, mRunnableScheduler, SystemClock())
}
@Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 2b97041..d000c32 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -44,6 +44,7 @@
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
+import androidx.work.SystemClock;
import androidx.work.WorkManagerTest;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkSpec;
@@ -70,7 +71,7 @@
@Before
public void setUp() {
mConverter = new SystemJobInfoConverter(
- ApplicationProvider.getApplicationContext());
+ ApplicationProvider.getApplicationContext(), new SystemClock());
}
@Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
index 305ccf2..164b411 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -126,7 +126,7 @@
workDatabase,
configuration,
mJobScheduler,
- new SystemJobInfoConverter(context)));
+ new SystemJobInfoConverter(context, configuration.getClock())));
doNothing().when(mSystemJobScheduler).scheduleInternal(any(WorkSpec.class), anyInt());
}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
index 60dd8fe..27d7f35 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
+++ b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
@@ -224,7 +224,7 @@
* Sets an initial delay for the [WorkRequest].
*
* @param duration The length of the delay
- * @return The current [Builder] *
+ * @return The current [Builder]
* @throws IllegalArgumentException if the given initial delay will push the execution time
* past `Long.MAX_VALUE` and cause an overflow
*/
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
index 6a1ed29..b0786fe 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
@@ -26,6 +26,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.work.Clock;
import androidx.work.Configuration;
import androidx.work.Logger;
import androidx.work.impl.background.systemalarm.SystemAlarmScheduler;
@@ -100,13 +101,13 @@
List<WorkSpec> contentUriWorkSpecs = null;
if (Build.VERSION.SDK_INT >= CONTENT_URI_TRIGGER_API_LEVEL) {
contentUriWorkSpecs = workSpecDao.getEligibleWorkForSchedulingWithContentUris();
- markScheduled(workSpecDao, contentUriWorkSpecs);
+ markScheduled(workSpecDao, configuration.getClock(), contentUriWorkSpecs);
}
// Enqueued workSpecs when scheduling limits are applicable.
eligibleWorkSpecsForLimitedSlots = workSpecDao.getEligibleWorkForScheduling(
configuration.getMaxSchedulerLimit());
- markScheduled(workSpecDao, eligibleWorkSpecsForLimitedSlots);
+ markScheduled(workSpecDao, configuration.getClock(), eligibleWorkSpecsForLimitedSlots);
if (contentUriWorkSpecs != null) {
eligibleWorkSpecsForLimitedSlots.addAll(contentUriWorkSpecs);
}
@@ -157,7 +158,7 @@
setComponentEnabled(context, SystemJobService.class, true);
Logger.get().debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
} else {
- scheduler = tryCreateGcmBasedScheduler(context);
+ scheduler = tryCreateGcmBasedScheduler(context, configuration.getClock());
if (scheduler == null) {
scheduler = new SystemAlarmScheduler(context);
setComponentEnabled(context, SystemAlarmService.class, true);
@@ -168,11 +169,12 @@
}
@Nullable
- private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context) {
+ private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context, Clock clock) {
try {
Class<?> klass = Class.forName(GCM_SCHEDULER);
Scheduler scheduler =
- (Scheduler) klass.getConstructor(Context.class).newInstance(context);
+ (Scheduler) klass.getConstructor(Context.class, Clock.class)
+ .newInstance(context, clock);
Logger.get().debug(TAG, "Created " + GCM_SCHEDULER);
return scheduler;
} catch (Throwable throwable) {
@@ -184,9 +186,9 @@
private Schedulers() {
}
- private static void markScheduled(WorkSpecDao dao, List<WorkSpec> workSpecs) {
+ private static void markScheduled(WorkSpecDao dao, Clock clock, List<WorkSpec> workSpecs) {
if (workSpecs.size() > 0) {
- long now = System.currentTimeMillis();
+ long now = clock.currentTimeMillis();
// Mark all the WorkSpecs as scheduled.
// Calls to Scheduler#schedule() could potentially result in more schedules
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
index 01e484a..3ae8711 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.work.Clock;
import androidx.work.Logger;
import androidx.work.RunnableScheduler;
import androidx.work.impl.model.WorkSpec;
@@ -43,14 +44,17 @@
final GreedyScheduler mGreedyScheduler;
private final RunnableScheduler mRunnableScheduler;
+ private final Clock mClock;
private final Map<String, Runnable> mRunnables;
public DelayedWorkTracker(
@NonNull GreedyScheduler scheduler,
- @NonNull RunnableScheduler runnableScheduler) {
+ @NonNull RunnableScheduler runnableScheduler,
+ @NonNull Clock clock) {
mGreedyScheduler = scheduler;
mRunnableScheduler = runnableScheduler;
+ mClock = clock;
mRunnables = new HashMap<>();
}
@@ -77,7 +81,7 @@
};
mRunnables.put(workSpec.id, runnable);
- long now = System.currentTimeMillis();
+ long now = mClock.currentTimeMillis();
long delay = nextRunTime - now;
mRunnableScheduler.scheduleWithDelay(delay, runnable);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
index b7e8767..bb48033 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
@@ -93,7 +93,8 @@
) {
mContext = context;
mWorkConstraintsTracker = new WorkConstraintsTrackerImpl(trackers, this);
- mDelayedWorkTracker = new DelayedWorkTracker(this, configuration.getRunnableScheduler());
+ mDelayedWorkTracker = new DelayedWorkTracker(
+ this, configuration.getRunnableScheduler(), configuration.getClock());
mConfiguration = configuration;
mProcessor = processor;
mWorkLauncher = workLauncher;
@@ -152,7 +153,7 @@
}
long throttled = throttleIfNeeded(workSpec);
long nextRunTime = max(workSpec.calculateNextRunTime(), throttled);
- long now = System.currentTimeMillis();
+ long now = mConfiguration.getClock().currentTimeMillis();
if (workSpec.state == WorkInfo.State.ENQUEUED) {
if (now < nextRunTime) {
// Future work
@@ -290,7 +291,7 @@
AttemptData firstRunAttempt = mFirstRunAttempts.get(id);
if (firstRunAttempt == null) {
firstRunAttempt = new AttemptData(workSpec.runAttemptCount,
- System.currentTimeMillis());
+ mConfiguration.getClock().currentTimeMillis());
mFirstRunAttempts.put(id, firstRunAttempt);
}
return firstRunAttempt.mTimeStamp
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
index ade9938..1e73418 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
@@ -24,6 +24,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
+import androidx.work.Clock;
import androidx.work.Logger;
import androidx.work.impl.ExecutionListener;
import androidx.work.impl.StartStopToken;
@@ -128,10 +129,13 @@
private final Context mContext;
private final Map<WorkGenerationalId, DelayMetCommandHandler> mPendingDelayMet;
private final Object mLock;
+ private final Clock mClock;
private final StartStopTokens mStartStopTokens;
- CommandHandler(@NonNull Context context, @NonNull StartStopTokens startStopTokens) {
+ CommandHandler(@NonNull Context context, Clock clock,
+ @NonNull StartStopTokens startStopTokens) {
mContext = context;
+ mClock = clock;
mStartStopTokens = startStopTokens;
mPendingDelayMet = new HashMap<>();
mLock = new Object();
@@ -332,7 +336,7 @@
// Constraints changed command handler is synchronous. No cleanup
// is necessary.
ConstraintsCommandHandler changedCommandHandler =
- new ConstraintsCommandHandler(mContext, startId, dispatcher);
+ new ConstraintsCommandHandler(mContext, mClock, startId, dispatcher);
changedCommandHandler.handleConstraintsChanged();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
index ad3361b..4bd3fbf 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
+import androidx.work.Clock;
import androidx.work.Logger;
import androidx.work.impl.constraints.WorkConstraintsTrackerImpl;
import androidx.work.impl.constraints.trackers.Trackers;
@@ -35,7 +36,6 @@
/**
* This is a command handler which handles the constraints changed event.
* Typically this happens for WorkSpec's for which we have pending alarms.
- *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ConstraintsCommandHandler {
@@ -43,16 +43,18 @@
private static final String TAG = Logger.tagWithPrefix("ConstraintsCmdHandler");
private final Context mContext;
+ private final Clock mClock;
private final int mStartId;
private final SystemAlarmDispatcher mDispatcher;
private final WorkConstraintsTrackerImpl mWorkConstraintsTracker;
ConstraintsCommandHandler(
@NonNull Context context,
+ Clock clock,
int startId,
@NonNull SystemAlarmDispatcher dispatcher) {
-
mContext = context;
+ mClock = clock;
mStartId = startId;
mDispatcher = dispatcher;
Trackers trackers = mDispatcher.getWorkManager().getTrackers();
@@ -74,7 +76,7 @@
List<WorkSpec> eligibleWorkSpecs = new ArrayList<>(candidates.size());
// Filter candidates should have already been scheduled.
- long now = System.currentTimeMillis();
+ long now = mClock.currentTimeMillis();
for (WorkSpec workSpec : candidates) {
String workSpecId = workSpec.id;
long triggerAt = workSpec.calculateNextRunTime();
@@ -87,7 +89,8 @@
for (WorkSpec workSpec : eligibleWorkSpecs) {
String workSpecId = workSpec.id;
Intent intent = CommandHandler.createDelayMetIntent(mContext, generationalId(workSpec));
- Logger.get().debug(TAG, "Creating a delay_met command for workSpec with id (" + workSpecId + ")");
+ Logger.get().debug(TAG,
+ "Creating a delay_met command for workSpec with id (" + workSpecId + ")");
mDispatcher.getTaskExecutor().getMainThreadExecutor().execute(
new SystemAlarmDispatcher.AddRunnable(mDispatcher, intent, mStartId));
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
index c92310b..d6a5ef4 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
@@ -90,8 +90,9 @@
) {
mContext = context.getApplicationContext();
mStartStopTokens = new StartStopTokens();
- mCommandHandler = new CommandHandler(mContext, mStartStopTokens);
mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(context);
+ mCommandHandler = new CommandHandler(
+ mContext, mWorkManager.getConfiguration().getClock(), mStartStopTokens);
mWorkTimer = new WorkTimer(mWorkManager.getConfiguration().getRunnableScheduler());
mProcessor = processor != null ? processor : mWorkManager.getProcessor();
mTaskExecutor = mWorkManager.getWorkTaskExecutor();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 61a24d5..f292f3f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -29,6 +29,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.work.BackoffPolicy;
+import androidx.work.Clock;
import androidx.work.Constraints;
import androidx.work.Logger;
import androidx.work.NetworkType;
@@ -50,8 +51,10 @@
static final String EXTRA_WORK_SPEC_GENERATION = "EXTRA_WORK_SPEC_GENERATION";
private final ComponentName mWorkServiceComponent;
+ private final Clock mClock;
- SystemJobInfoConverter(@NonNull Context context) {
+ SystemJobInfoConverter(@NonNull Context context, Clock clock) {
+ mClock = clock;
Context appContext = context.getApplicationContext();
mWorkServiceComponent = new ComponentName(appContext, SystemJobService.class);
}
@@ -86,7 +89,7 @@
}
long nextRunTime = workSpec.calculateNextRunTime();
- long now = System.currentTimeMillis();
+ long now = mClock.currentTimeMillis();
long offset = Math.max(nextRunTime - now, 0);
if (Build.VERSION.SDK_INT <= 28) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index af76cf1..fc679669 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -78,7 +78,7 @@
workDatabase,
configuration,
(JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE),
- new SystemJobInfoConverter(context)
+ new SystemJobInfoConverter(context, configuration.getClock())
);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
index 03158ae..71e3ec8 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
@@ -222,7 +222,9 @@
}
// Update the last cancelled time in Preference.
new PreferenceUtils(workManagerImpl.getWorkDatabase())
- .setLastCancelAllTimeMillis(System.currentTimeMillis());
+ .setLastCancelAllTimeMillis(
+ workManagerImpl.getConfiguration().getClock()
+ .currentTimeMillis());
workDatabase.setTransactionSuccessful();
} finally {
workDatabase.endTransaction();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt b/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
index 2781cdf..24221a9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
@@ -38,7 +38,8 @@
val workNameDao = database.workNameDao()
val workTagDao = database.workTagDao()
val systemIdInfoDao = database.systemIdInfoDao()
- val startAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)
+ val startAt =
+ workManager.configuration.clock.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)
val completed = workSpecDao.getRecentlyCompletedWork(startAt)
val running = workSpecDao.getRunningWork()
val enqueued = workSpecDao.getAllEligibleWorkSpecsForScheduling(