Merge "Check in Thing.kt temporarily under appactions/interaction/.. code location, based on the needs of other schemeOrg types in app interaction core library." into androidx-main
diff --git a/appactions/interaction/interaction-capabilities-core/build.gradle b/appactions/interaction/interaction-capabilities-core/build.gradle
index 2fccf45..6c5b2e1 100644
--- a/appactions/interaction/interaction-capabilities-core/build.gradle
+++ b/appactions/interaction/interaction-capabilities-core/build.gradle
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import androidx.build.LibraryType
import androidx.build.Publish
@@ -24,15 +23,15 @@
}
dependencies {
- api(libs.kotlinStdlib)
- api(libs.autoValueAnnotations)
- api(libs.kotlinStdlib)
+ api(project(path: ":appactions:interaction:interaction-proto", configuration: "shadowJar"))
+
annotationProcessor(libs.autoValue)
- implementation(libs.protobufLite)
+
+ api(libs.autoValueAnnotations)
implementation(libs.guavaListenableFuture)
implementation(libs.kotlinCoroutinesGuava)
+ implementation(libs.kotlinStdlib)
implementation("androidx.concurrent:concurrent-futures:1.1.0")
- implementation(project(":appactions:interaction:interaction-proto"))
testAnnotationProcessor(libs.autoValue)
testImplementation(libs.junit)
@@ -68,7 +67,6 @@
androidx {
name = "androidx.appactions.interaction:interaction-capabilities-core"
type = LibraryType.PUBLISHED_LIBRARY
- publish = Publish.NONE
inceptionYear = "2023"
description = "App Interaction library core capabilities API and implementation."
failOnDeprecationWarnings = false
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
index 70da0c5..21f4c9d 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/CapabilityBuilderBase.kt
@@ -49,7 +49,7 @@
private var id: String? = null
private var property: PropertyT? = null
private var actionExecutor: ActionExecutor<ArgumentT, OutputT>? = null
- private var sessionBuilder: SessionBuilder<SessionT>? = null
+ private var sessionFactory: SessionFactory<SessionT>? = null
/**
* The SessionBridge object, which is used to normalize Session instances to TaskHandler.
* see SessionBridge documentation for more information.
@@ -85,22 +85,22 @@
/**
* Sets the ActionExecutor for this capability.
*
- * setSessionBuilder and setActionExecutor are mutually exclusive, so calling one will nullify the other.
+ * setSessionFactory and setActionExecutor are mutually exclusive, so calling one will nullify the other.
*/
fun setActionExecutor(actionExecutor: ActionExecutor<ArgumentT, OutputT>) = asBuilder().apply {
this.actionExecutor = actionExecutor
}
/**
- * Sets the SessionBuilder instance which is used to create Session instaces for this
+ * Sets the SessionFactory instance which is used to create Session instaces for this
* capability.
*
- * setSessionBuilder and setActionExecutor are mutually exclusive, so calling one will nullify the other.
+ * setSessionFactory and setActionExecutor are mutually exclusive, so calling one will nullify the other.
*/
- protected open fun setSessionBuilder(
- sessionBuilder: SessionBuilder<SessionT>,
+ protected open fun setSessionFactory(
+ sessionFactory: SessionFactory<SessionT>,
): BuilderT = asBuilder().apply {
- this.sessionBuilder = sessionBuilder
+ this.sessionFactory = sessionFactory
}
/** Builds and returns this ActionCapability. */
@@ -119,8 +119,8 @@
actionSpec,
checkedProperty,
requireNotNull(
- sessionBuilder,
- { "either setActionExecutor or setSessionBuilder must be called before build" },
+ sessionFactory,
+ { "either setActionExecutor or setSessionFactory must be called before build" },
),
sessionBridge!!,
sessionUpdaterSupplier!!,
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionBuilder.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionFactory.kt
similarity index 94%
rename from appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionBuilder.kt
rename to appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionFactory.kt
index 7e8ec2c..11c32cf 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionBuilder.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/SessionFactory.kt
@@ -19,9 +19,9 @@
/**
* Interface to be implemented for creating SessionT instances.
*/
-fun interface SessionBuilder<SessionT> {
+fun interface SessionFactory<SessionT> {
/**
- * Implement this method to create session for handling assistant requests.\
+ * Implement this method to create session for handling assistant requests.
*
* @param hostProperties only applicable while used with AppInteractionService. Contains the
* dimensions of the UI area. Null when used without AppInteractionService.
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java
deleted file mode 100644
index c38b30c..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.impl;
-
-import static androidx.appactions.interaction.capabilities.core.impl.utils.ImmutableCollectors.toImmutableList;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
-import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentParam;
-import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
-
-import com.google.auto.value.AutoValue;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/** Represents Fulfillment request sent from assistant, including arguments. */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings("AutoValueImmutableFields")
-@AutoValue
-public abstract class ArgumentsWrapper {
-
- /**
- * Creates an instance of ArgumentsWrapper based on the Fulfillment send from Assistant.
- *
- * @param fulfillment for a single BII sent from Assistant.
- */
- @NonNull
- public static ArgumentsWrapper create(@NonNull Fulfillment fulfillment) {
- return new AutoValue_ArgumentsWrapper(
- Collections.unmodifiableMap(convertToArgumentMap(fulfillment)),
- createRequestMetadata(fulfillment));
- }
-
- private static Optional<RequestMetadata> createRequestMetadata(Fulfillment fulfillment) {
- if (fulfillment.getType() == Fulfillment.Type.UNKNOWN_TYPE
- || fulfillment.getType() == Fulfillment.Type.UNRECOGNIZED) {
- return Optional.empty();
- }
- return Optional.of(
- RequestMetadata.newBuilder().setRequestType(fulfillment.getType()).build());
- }
-
- private static Map<String, List<FulfillmentValue>> convertToArgumentMap(
- Fulfillment fulfillment) {
- Map<String, List<FulfillmentValue>> result = new LinkedHashMap<>();
- for (FulfillmentParam fp : fulfillment.getParamsList()) {
- // Normalize deprecated param value list into new FulfillmentValue.
- if (!fp.getValuesList().isEmpty()) {
- result.put(
- fp.getName(),
- fp.getValuesList().stream()
- .map(
- paramValue ->
- FulfillmentValue.newBuilder()
- .setValue(paramValue)
- .build())
- .collect(toImmutableList()));
- } else {
- result.put(fp.getName(), fp.getFulfillmentValuesList());
- }
- }
- return result;
- }
-
- /**
- * A map of BII parameter names to a task param value, where each {@code FulfillmentValue} can
- * have a value and {@code DisambigData} sent from Assistant.
- */
- @NonNull
- public abstract Map<String, List<FulfillmentValue>> paramValues();
-
- /**
- * Metadata from the FulfillmentRequest on the current Assistant turn. This field should be
- * Optional.empty for one-shot capabilities.
- */
- @NonNull
- public abstract Optional<RequestMetadata> requestMetadata();
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt
new file mode 100644
index 0000000..94714e3
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ArgumentsWrapper.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.appactions.interaction.capabilities.core.impl
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue
+
+/**
+ * Represents Fulfillment request sent from assistant, including arguments.
+ *
+ * @param paramValues A map of BII parameter names to a task param value, where each
+ * `FulfillmentValue` can have a value and `DisambigData` sent from Assistant.
+ * @param requestMetadata Metadata from the FulfillmentRequest on the current Assistant turn. This
+ * field should be null for one-shot capabilities.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+data class ArgumentsWrapper
+internal constructor(
+ val paramValues: Map<String, List<FulfillmentValue>>,
+ val requestMetadata: RequestMetadata?,
+) {
+ companion object {
+ /**
+ * Creates an instance of ArgumentsWrapper based on the Fulfillment send from Assistant.
+ *
+ * @param fulfillment for a single BII sent from Assistant.
+ */
+ @JvmStatic
+ fun create(fulfillment: Fulfillment): ArgumentsWrapper {
+ return ArgumentsWrapper(
+ convertToArgumentMap(fulfillment),
+ createRequestMetadata(fulfillment),
+ )
+ }
+
+ private fun createRequestMetadata(fulfillment: Fulfillment): RequestMetadata? {
+ return if (
+ fulfillment.type == Fulfillment.Type.UNKNOWN_TYPE ||
+ fulfillment.type == Fulfillment.Type.UNRECOGNIZED
+ ) {
+ null
+ } else RequestMetadata.newBuilder().setRequestType(fulfillment.type).build()
+ }
+
+ @Suppress(
+ "DEPRECATION"
+ ) // Convert the deprecated "fp.valuesList" property to the new format.
+ private fun convertToArgumentMap(
+ fulfillment: Fulfillment
+ ): Map<String, List<FulfillmentValue>> {
+ val result = mutableMapOf<String, List<FulfillmentValue>>()
+ for (fp in fulfillment.paramsList) {
+ // Normalize deprecated param value list into new FulfillmentValue.
+ if (fp.valuesList.isNotEmpty()) {
+ result[fp.name] =
+ fp.valuesList.map { FulfillmentValue.newBuilder().setValue(it).build() }
+ } else {
+ result[fp.name] = fp.fulfillmentValuesList
+ }
+ }
+ return result.toMap()
+ }
+ }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
index 8154279..9509c68 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
@@ -1,4 +1,3 @@
-
/*
* Copyright 2023 The Android Open Source Project
*
@@ -38,7 +37,7 @@
internal class SingleTurnCapabilitySession<
ArgumentT,
OutputT,
- >(
+>(
val actionSpec: ActionSpec<*, ArgumentT, OutputT>,
val actionExecutor: ActionExecutor<ArgumentT, OutputT>,
) : ActionCapabilitySession {
@@ -64,13 +63,12 @@
@NonNull argumentsWrapper: ArgumentsWrapper,
@NonNull callback: CallbackInternal,
) {
- val paramValuesMap: Map<String, List<ParamValue>> = argumentsWrapper.paramValues().entries
- .associate {
- entry: Map.Entry<String, List<FulfillmentValue>> ->
+ val paramValuesMap: Map<String, List<ParamValue>> =
+ argumentsWrapper.paramValues.entries.associate {
+ entry: Map.Entry<String, List<FulfillmentValue>> ->
Pair(
entry.key,
- entry.value.mapNotNull {
- fulfillmentValue: FulfillmentValue ->
+ entry.value.mapNotNull { fulfillmentValue: FulfillmentValue ->
fulfillmentValue.getValue()
},
)
@@ -96,8 +94,7 @@
executionResult: ExecutionResult<OutputT>,
): FulfillmentResponse {
val fulfillmentResponseBuilder =
- FulfillmentResponse.newBuilder()
- .setStartDictation(executionResult.startDictation)
+ FulfillmentResponse.newBuilder().setStartDictation(executionResult.startDictation)
executionResult.output?.let { it ->
fulfillmentResponseBuilder.setExecutionOutput(
actionSpec.convertOutputToProto(it),
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java
index 048f5bb..04a8afd 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/FieldBinding.java
@@ -17,9 +17,9 @@
package androidx.appactions.interaction.capabilities.core.impl.converters;
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
+import androidx.appactions.interaction.protobuf.Value;
import com.google.auto.value.AutoValue;
-import com.google.protobuf.Value;
import java.util.Optional;
import java.util.function.Function;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
index 14c48d5..3b610ad 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConverters.java
@@ -40,10 +40,9 @@
import androidx.appactions.interaction.proto.Entity;
import androidx.appactions.interaction.proto.FulfillmentResponse;
import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.protobuf.ListValue;
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.ListValue;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import java.time.Duration;
import java.time.LocalDate;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java
index 6781668..11181a6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpec.java
@@ -18,8 +18,7 @@
import androidx.annotation.NonNull;
import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
-
-import com.google.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Struct;
/**
* TypeSpec is used to convert between java objects in capabilities/values and Struct proto.
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
index 38bab1f..46aa2d1 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecBuilder.java
@@ -21,10 +21,9 @@
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
import androidx.appactions.interaction.capabilities.core.values.Thing;
-
-import com.google.protobuf.ListValue;
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.ListValue;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import java.time.Duration;
import java.time.OffsetDateTime;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
index 13d36ed..25ac2fc 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImpl.java
@@ -19,9 +19,8 @@
import androidx.annotation.NonNull;
import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
-
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import java.util.Collections;
import java.util.List;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java
deleted file mode 100644
index c277143..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/InvalidTaskException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task;
-
-import androidx.annotation.NonNull;
-
-/**
- * Thrown when the task is not configured correctly for a particular capability instance. One
- * example is when a capability requires user confirmation support, but the capability instance does
- * not set a value for the {@code OnReadyToConfirmListener}.
- */
-public final class InvalidTaskException extends RuntimeException {
-
- public InvalidTaskException(@NonNull String message) {
- super(message);
- }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java
deleted file mode 100644
index eb6c3bd9..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/OnReadyToConfirmListener.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task;
-
-import androidx.annotation.NonNull;
-import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-/**
- * Listener for when a task reaches a valid argument state, and can confirm fulfillment.
- *
- * @param <ArgumentT>
- * @param <ConfirmationT>
- */
-public interface OnReadyToConfirmListener<ArgumentT, ConfirmationT> {
-
- /** Called when a task is ready to confirm, with the final Argument instance. */
- @NonNull
- ListenableFuture<ConfirmationOutput<ConfirmationT>> onReadyToConfirm(ArgumentT finalArgument);
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.kt
index 2dc76ad..9ede873 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImpl.kt
@@ -18,7 +18,7 @@
import androidx.appactions.interaction.capabilities.core.ActionCapability
import androidx.appactions.interaction.capabilities.core.BaseSession
import androidx.appactions.interaction.capabilities.core.HostProperties
-import androidx.appactions.interaction.capabilities.core.SessionBuilder
+import androidx.appactions.interaction.capabilities.core.SessionFactory
import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilitySession
import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
import androidx.appactions.interaction.proto.AppActionsContext.AppAction
@@ -29,7 +29,7 @@
* @param id a unique id for this capability, can be null
* @param supportsMultiTurnTask whether this is a single-turn capability or a multi-turn capability
* @param actionSpec the ActionSpec for this capability
- * @param sessionBuilder the SessionBuilder provided by the library user
+ * @param sessionFactory the SessionFactory provided by the library user
* @param sessionBridge a SessionBridge object that converts SessionT into TaskHandler instance
* @param sessionUpdaterSupplier a Supplier of SessionUpdaterT instances
*/
@@ -44,7 +44,7 @@
override val id: String?,
val actionSpec: ActionSpec<PropertyT, ArgumentT, OutputT>,
val property: PropertyT,
- val sessionBuilder: SessionBuilder<SessionT>,
+ val sessionFactory: SessionFactory<SessionT>,
val sessionBridge: SessionBridge<SessionT, ConfirmationT>,
val sessionUpdaterSupplier: Supplier<SessionUpdaterT>,
) : ActionCapability {
@@ -59,7 +59,7 @@
}
override fun createSession(hostProperties: HostProperties): ActionCapabilitySession {
- val externalSession = sessionBuilder.createSession(
+ val externalSession = sessionFactory.createSession(
hostProperties,
)
return TaskCapabilitySession(
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java
index 41fe7f6..1fca454 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtils.java
@@ -29,8 +29,7 @@
import androidx.appactions.interaction.proto.Entity;
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentValue;
import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Struct;
import java.util.Arrays;
import java.util.Collections;
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
index 28853a2..0dd26ab 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.java
@@ -188,11 +188,11 @@
ArgumentsWrapper argumentsWrapper = assistantUpdateRequest.argumentsWrapper();
CallbackInternal callback = assistantUpdateRequest.callbackInternal();
- if (!argumentsWrapper.requestMetadata().isPresent()) {
+ if (argumentsWrapper.getRequestMetadata() == null) {
callback.onError(ErrorStatusInternal.INVALID_REQUEST_TYPE);
return Futures.immediateVoidFuture();
}
- Fulfillment.Type requestType = argumentsWrapper.requestMetadata().get().requestType();
+ Fulfillment.Type requestType = argumentsWrapper.getRequestMetadata().requestType();
switch (requestType) {
case UNRECOGNIZED:
case UNKNOWN_TYPE:
@@ -345,7 +345,7 @@
ListenableFuture<Void> argResolutionFuture =
Futures.transformAsync(
onInitFuture,
- unused -> processFulfillmentValues(argumentsWrapper.paramValues()),
+ unused -> processFulfillmentValues(argumentsWrapper.getParamValues()),
mExecutor,
"processFulfillmentValues");
@@ -426,7 +426,7 @@
private void clearMissingArgs(ArgumentsWrapper assistantArgs) {
Set<String> argsCleared =
mCurrentValuesMap.keySet().stream()
- .filter(argName -> !assistantArgs.paramValues().containsKey(argName))
+ .filter(argName -> !assistantArgs.getParamValues().containsKey(argName))
.collect(toImmutableSet());
for (String arg : argsCleared) {
mCurrentValuesMap.remove(arg);
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java
index 2721638..0b838cb 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeConvertersTest.java
@@ -40,10 +40,9 @@
import androidx.appactions.interaction.capabilities.core.values.properties.Recipient;
import androidx.appactions.interaction.proto.Entity;
import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.protobuf.ListValue;
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.ListValue;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
index 52d6c2a..6a09135 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/converters/TypeSpecImplTest.java
@@ -22,9 +22,8 @@
import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
import androidx.appactions.interaction.capabilities.core.testing.spec.TestEntity;
-
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import org.junit.Test;
import org.junit.runner.RunWith;
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 771283d..a899c7c 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
@@ -21,7 +21,7 @@
import androidx.appactions.interaction.capabilities.core.ExecutionResult
import androidx.appactions.interaction.capabilities.core.HostProperties
import androidx.appactions.interaction.capabilities.core.InitArg
-import androidx.appactions.interaction.capabilities.core.SessionBuilder
+import androidx.appactions.interaction.capabilities.core.SessionFactory
import androidx.appactions.interaction.capabilities.core.impl.ActionCapabilitySession
import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal
import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
@@ -86,7 +86,7 @@
class TaskCapabilityImplTest {
val capability: ActionCapability = createCapability<EmptyTaskUpdater>(
SINGLE_REQUIRED_FIELD_PROPERTY,
- sessionBuilder = SessionBuilder {
+ sessionFactory = SessionFactory {
object : Session {
override fun onFinishAsync(argument: Argument) =
Futures.immediateFuture(ExecutionResult.getDefaultInstance<Output>())
@@ -123,7 +123,7 @@
val externalSession = object : Session {}
val capability = createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
- SessionBuilder { externalSession },
+ SessionFactory { externalSession },
SessionBridge { TaskHandler.Builder<Confirmation>().build() },
::EmptyTaskUpdater,
)
@@ -137,7 +137,7 @@
val onSuccessInvocationCount = AtomicInteger(0)
val capability: ActionCapability = createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
- sessionBuilder = SessionBuilder {
+ sessionFactory = SessionFactory {
object : Session {
override fun onInit(initArg: InitArg) {
onSuccessInvocationCount.incrementAndGet()
@@ -215,7 +215,7 @@
fun fulfillmentType_unknown_errorReported() {
val capability: ActionCapability = createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
- sessionBuilder = SessionBuilder {
+ sessionFactory = SessionFactory {
object : Session {
override fun onFinishAsync(argument: Argument) =
Futures.immediateFuture(
@@ -258,7 +258,7 @@
.setSlotA(EntityProperty.Builder().setRequired(true).build())
.setSlotB(EntityProperty.Builder().setRequired(true).build())
.build()
- val sessionBuilder = SessionBuilder<CapabilityTwoEntityValues.Session> {
+ val sessionFactory = SessionFactory<CapabilityTwoEntityValues.Session> {
object : CapabilityTwoEntityValues.Session {
override suspend fun onFinish(
argument: CapabilityTwoEntityValues.Argument,
@@ -280,7 +280,7 @@
"fakeId",
CapabilityTwoEntityValues.ACTION_SPEC,
property,
- sessionBuilder,
+ sessionFactory,
sessionBridge,
::EmptyTaskUpdater,
)
@@ -335,7 +335,7 @@
.setSlotA(EntityProperty.Builder().setRequired(true).build())
.setSlotB(EntityProperty.Builder().setRequired(false).build())
.build()
- val sessionBuilder = SessionBuilder<CapabilityTwoEntityValues.Session> {
+ val sessionFactory = SessionFactory<CapabilityTwoEntityValues.Session> {
object : CapabilityTwoEntityValues.Session {
override suspend fun onFinish(
argument: CapabilityTwoEntityValues.Argument,
@@ -360,7 +360,7 @@
"fakeId",
CapabilityTwoEntityValues.ACTION_SPEC,
property,
- sessionBuilder,
+ sessionFactory,
sessionBridge,
::EmptyTaskUpdater,
)
@@ -420,7 +420,7 @@
.build()
val capability: ActionCapability = createCapability(
property,
- sessionBuilder = SessionBuilder { Session.DEFAULT },
+ sessionFactory = SessionFactory { Session.DEFAULT },
sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
sessionUpdaterSupplier = ::EmptyTaskUpdater,
)
@@ -473,7 +473,7 @@
fun disambig_singleParam_disambigEntitiesInContext() {
val capability: ActionCapability = createCapability(
SINGLE_REQUIRED_FIELD_PROPERTY,
- sessionBuilder = {
+ sessionFactory = {
object : Session {
override suspend fun onFinish(argument: Argument) =
ExecutionResult.getDefaultInstance<Output>()
@@ -641,7 +641,7 @@
val onFinishListItemCb: SettableFutureWrapper<ListItem> = SettableFutureWrapper()
val onFinishStringCb: SettableFutureWrapper<String> = SettableFutureWrapper()
- val sessionBuilder = SessionBuilder<CapabilityStructFill.Session> {
+ val sessionFactory = SessionFactory<CapabilityStructFill.Session> {
object : CapabilityStructFill.Session {
override suspend fun onFinish(
argument: CapabilityStructFill.Argument,
@@ -689,7 +689,7 @@
"selectListItem",
CapabilityStructFill.ACTION_SPEC,
property,
- sessionBuilder,
+ sessionFactory,
sessionBridge,
::EmptyTaskUpdater,
)
@@ -795,7 +795,7 @@
@Test
@kotlin.Throws(Exception::class)
fun executionResult_resultReturned() {
- val sessionBuilder = SessionBuilder<Session> {
+ val sessionFactory = SessionFactory<Session> {
object : Session {
override suspend fun onFinish(argument: Argument) =
ExecutionResult.Builder<Output>()
@@ -811,7 +811,7 @@
}
}
val capability = CapabilityBuilder()
- .setSessionBuilder(sessionBuilder)
+ .setSessionFactory(sessionFactory)
.build()
val session = capability.createSession(hostProperties)
val onSuccessInvoked: SettableFutureWrapper<FulfillmentResponse> = SettableFutureWrapper()
@@ -883,9 +883,9 @@
RequiredTaskUpdater()
}
- public override fun setSessionBuilder(
- sessionBuilder: SessionBuilder<Session>,
- ): CapabilityBuilder = super.setSessionBuilder(sessionBuilder)
+ public override fun setSessionFactory(
+ sessionFactory: SessionFactory<Session>,
+ ): CapabilityBuilder = super.setSessionFactory(sessionFactory)
}
companion object {
@@ -995,7 +995,7 @@
*/
private fun <SessionUpdaterT : AbstractTaskUpdater> createCapability(
property: Property,
- sessionBuilder: SessionBuilder<Session>,
+ sessionFactory: SessionFactory<Session>,
sessionBridge: SessionBridge<Session, Confirmation>,
sessionUpdaterSupplier: Supplier<SessionUpdaterT>,
): TaskCapabilityImpl<Property,
@@ -1008,7 +1008,7 @@
"id",
ACTION_SPEC,
property,
- sessionBuilder,
+ sessionFactory,
sessionBridge,
sessionUpdaterSupplier,
)
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
index 530aebd..5dfd041 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessorTest.java
@@ -34,10 +34,10 @@
import androidx.appactions.interaction.proto.DisambiguationData;
import androidx.appactions.interaction.proto.Entity;
import androidx.appactions.interaction.proto.ParamValue;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import com.google.common.util.concurrent.ListenableFuture;
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java
index 32a97b4..84d481d 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/ArgumentUtils.java
@@ -22,9 +22,8 @@
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment;
import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.FulfillmentParam;
import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
+import androidx.appactions.interaction.protobuf.Struct;
+import androidx.appactions.interaction.protobuf.Value;
import java.util.ArrayList;
import java.util.Collections;
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
index 76b48b7..a6e64ba 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/TestingUtils.java
@@ -18,13 +18,11 @@
import androidx.annotation.NonNull;
import androidx.appactions.interaction.capabilities.core.ActionExecutor;
-import androidx.appactions.interaction.capabilities.core.ConfirmationOutput;
import androidx.appactions.interaction.capabilities.core.ExecutionResult;
import androidx.appactions.interaction.capabilities.core.impl.CallbackInternal;
import androidx.appactions.interaction.capabilities.core.impl.ErrorStatusInternal;
import androidx.appactions.interaction.capabilities.core.impl.TouchEventCallback;
import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures;
-import androidx.appactions.interaction.capabilities.core.task.OnReadyToConfirmListener;
import androidx.appactions.interaction.capabilities.core.testing.spec.SettableFutureWrapper;
import androidx.appactions.interaction.proto.FulfillmentResponse;
import androidx.appactions.interaction.proto.TouchEventMetadata;
@@ -33,7 +31,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public final class TestingUtils {
@@ -93,16 +90,6 @@
};
}
- public static <ArgumentT, ConfirmationT>
- Optional<OnReadyToConfirmListener<ArgumentT, ConfirmationT>>
- buildOnReadyToConfirmListener(SettableFutureWrapper<ArgumentT> future) {
- return Optional.of(
- (finalArgs) -> {
- future.set(finalArgs);
- return Futures.immediateFuture(ConfirmationOutput.getDefaultInstance());
- });
- }
-
public static TouchEventCallback buildTouchEventCallback(
SettableFutureWrapper<FulfillmentResponse> future) {
return new TouchEventCallback() {
diff --git a/appactions/interaction/interaction-capabilities-productivity/build.gradle b/appactions/interaction/interaction-capabilities-productivity/build.gradle
index 8703ac7..fff3438 100644
--- a/appactions/interaction/interaction-capabilities-productivity/build.gradle
+++ b/appactions/interaction/interaction-capabilities-productivity/build.gradle
@@ -24,11 +24,19 @@
dependencies {
api(libs.kotlinStdlib)
+ implementation(libs.protobufLite)
+ implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
implementation("androidx.annotation:annotation:1.1.0")
+ implementation(project(":appactions:interaction:interaction-capabilities-core"))
+ implementation(project(":appactions:interaction:interaction-proto"))
}
android {
namespace "androidx.appactions.interaction.capabilities.productivity"
+ defaultConfig {
+ // TODO(b/266649259): lower minSdk version once Optional is removed.
+ minSdkVersion 33
+ }
}
androidx {
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
new file mode 100644
index 0000000..a9a7f52
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTImer.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.ActionCapability
+import androidx.appactions.interaction.capabilities.core.BaseSession
+import androidx.appactions.interaction.capabilities.core.CapabilityBuilderBase
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
+import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
+import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
+import androidx.appactions.interaction.capabilities.core.values.Timer
+import androidx.appactions.interaction.proto.ParamValue
+import com.google.protobuf.Struct
+import com.google.protobuf.Value
+import java.util.Optional
+
+/** PauseTimer.kt in interaction-capabilities-productivity */
+private const val CAPABILITY_NAME = "actions.intent.PAUSE_TIMER"
+
+private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setDescriptor(PauseTimer.Property::class.java)
+ .setArgument(PauseTimer.Argument::class.java, PauseTimer.Argument::Builder)
+ .setOutput(PauseTimer.Output::class.java).bindRepeatedStructParameter(
+ "timer",
+ { property -> Optional.ofNullable(property.timerList) },
+ PauseTimer.Argument.Builder::setTimerList,
+ TypeConverters::toTimer
+ ).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.
+class PauseTimer private constructor() {
+
+ class CapabilityBuilder :
+ CapabilityBuilderBase<
+ CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+ >(ACTION_SPEC) {
+ override fun build(): ActionCapability {
+ super.setProperty(Property.Builder().build())
+ return super.build()
+ }
+ }
+
+ // TODO(b/268369632): Remove Property from public capability APIs.
+ class Property internal constructor(
+ val timerList: SimpleProperty?
+ ) {
+ override fun toString(): String {
+ return "Property(timerList=$timerList}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Property
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder {
+ private var timerList: SimpleProperty? = null
+
+ fun setTimerList(timerList: SimpleProperty): Builder =
+ apply { this.timerList = timerList }
+
+ fun build(): Property = Property(timerList)
+ }
+ }
+
+ class Argument internal constructor(
+ val timerList: List<Timer>?
+ ) {
+ override fun toString(): String {
+ return "Argument(timerList=$timerList)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Argument
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder : BuilderOf<Argument> {
+ private var timerList: List<Timer>? = null
+
+ fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+
+ override fun build(): Argument = Argument(timerList)
+ }
+ }
+
+ class Output internal constructor(val executionStatus: ExecutionStatus?) {
+ override fun toString(): String {
+ return "Output(executionStatus=$executionStatus)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Output
+
+ if (executionStatus != other.executionStatus) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return executionStatus.hashCode()
+ }
+
+ class Builder {
+ private var executionStatus: ExecutionStatus? = null
+
+ fun setExecutionStatus(executionStatus: ExecutionStatus): Builder =
+ apply { this.executionStatus = executionStatus }
+
+ fun build(): Output = Output(executionStatus)
+ }
+ }
+
+ class ExecutionStatus {
+ private var successStatus: SuccessStatus? = null
+ private var genericErrorStatus: GenericErrorStatus? = null
+
+ constructor(successStatus: SuccessStatus) {
+ this.successStatus = successStatus
+ }
+
+ constructor(genericErrorStatus: GenericErrorStatus) {
+ this.genericErrorStatus = genericErrorStatus
+ }
+
+ internal fun toParamValue(): ParamValue {
+ var status: String = ""
+ if (successStatus != null) {
+ status = successStatus.toString()
+ }
+ if (genericErrorStatus != null) {
+ status = genericErrorStatus.toString()
+ }
+ val value: Value = Value.newBuilder().setStringValue(status).build()
+ return ParamValue.newBuilder().setStructValue(
+ Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build(),
+ ).build()
+ }
+ }
+
+ class Confirmation internal constructor()
+
+ class TaskUpdater internal constructor() : AbstractTaskUpdater()
+
+ sealed interface Session : BaseSession<Argument, Output>
+}
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
new file mode 100644
index 0000000..23eccd2
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.ActionCapability
+import androidx.appactions.interaction.capabilities.core.BaseSession
+import androidx.appactions.interaction.capabilities.core.CapabilityBuilderBase
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
+import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
+import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
+import androidx.appactions.interaction.capabilities.core.values.Timer
+import androidx.appactions.interaction.proto.ParamValue
+import com.google.protobuf.Struct
+import com.google.protobuf.Value
+import java.util.Optional
+
+/** ResetTimer.kt in interaction-capabilities-productivity */
+private const val CAPABILITY_NAME = "actions.intent.RESET_TIMER"
+
+private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setDescriptor(ResetTimer.Property::class.java)
+ .setArgument(ResetTimer.Argument::class.java, ResetTimer.Argument::Builder)
+ .setOutput(ResetTimer.Output::class.java).bindRepeatedStructParameter(
+ "timer",
+ { property -> Optional.ofNullable(property.timerList) },
+ ResetTimer.Argument.Builder::setTimerList,
+ TypeConverters::toTimer
+ ).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.
+class ResetTimer private constructor() {
+
+ class CapabilityBuilder :
+ CapabilityBuilderBase<
+ CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+ >(ACTION_SPEC) {
+ override fun build(): ActionCapability {
+ super.setProperty(Property.Builder().build())
+ return super.build()
+ }
+ }
+
+ // TODO(b/268369632): Remove Property from public capability APIs.
+ class Property internal constructor(
+ val timerList: SimpleProperty?
+ ) {
+ override fun toString(): String {
+ return "Property(timerList=$timerList}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Property
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder {
+ private var timerList: SimpleProperty? = null
+
+ fun setTimerList(timerList: SimpleProperty): Builder =
+ apply { this.timerList = timerList }
+
+ fun build(): Property = Property(timerList)
+ }
+ }
+
+ class Argument internal constructor(
+ val timerList: List<Timer>?
+ ) {
+ override fun toString(): String {
+ return "Argument(timerList=$timerList)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Argument
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder : BuilderOf<Argument> {
+ private var timerList: List<Timer>? = null
+
+ fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+
+ override fun build(): Argument = Argument(timerList)
+ }
+ }
+
+ class Output internal constructor(val executionStatus: ExecutionStatus?) {
+ override fun toString(): String {
+ return "Output(executionStatus=$executionStatus)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Output
+
+ if (executionStatus != other.executionStatus) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return executionStatus.hashCode()
+ }
+
+ class Builder {
+ private var executionStatus: ExecutionStatus? = null
+
+ fun setExecutionStatus(executionStatus: ExecutionStatus): Builder =
+ apply { this.executionStatus = executionStatus }
+
+ fun build(): Output = Output(executionStatus)
+ }
+ }
+
+ class ExecutionStatus {
+ private var successStatus: SuccessStatus? = null
+ private var genericErrorStatus: GenericErrorStatus? = null
+
+ constructor(successStatus: SuccessStatus) {
+ this.successStatus = successStatus
+ }
+
+ constructor(genericErrorStatus: GenericErrorStatus) {
+ this.genericErrorStatus = genericErrorStatus
+ }
+
+ internal fun toParamValue(): ParamValue {
+ var status: String = ""
+ if (successStatus != null) {
+ status = successStatus.toString()
+ }
+ if (genericErrorStatus != null) {
+ status = genericErrorStatus.toString()
+ }
+ val value: Value = Value.newBuilder().setStringValue(status).build()
+ return ParamValue.newBuilder().setStructValue(
+ Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build(),
+ ).build()
+ }
+ }
+
+ class Confirmation internal constructor()
+
+ class TaskUpdater internal constructor() : AbstractTaskUpdater()
+
+ sealed interface Session : BaseSession<Argument, Output>
+}
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
new file mode 100644
index 0000000..18a4a76
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.ActionCapability
+import androidx.appactions.interaction.capabilities.core.BaseSession
+import androidx.appactions.interaction.capabilities.core.CapabilityBuilderBase
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
+import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
+import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
+import androidx.appactions.interaction.capabilities.core.values.Timer
+import androidx.appactions.interaction.proto.ParamValue
+import com.google.protobuf.Struct
+import com.google.protobuf.Value
+import java.util.Optional
+
+/** ResumeTimer.kt in interaction-capabilities-productivity */
+private const val CAPABILITY_NAME = "actions.intent.RESUME_TIMER"
+
+private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setDescriptor(ResumeTimer.Property::class.java)
+ .setArgument(ResumeTimer.Argument::class.java, ResumeTimer.Argument::Builder)
+ .setOutput(ResumeTimer.Output::class.java).bindRepeatedStructParameter(
+ "timer",
+ { property -> Optional.ofNullable(property.timerList) },
+ ResumeTimer.Argument.Builder::setTimerList,
+ TypeConverters::toTimer
+ ).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.
+class ResumeTimer private constructor() {
+
+ class CapabilityBuilder :
+ CapabilityBuilderBase<
+ CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+ >(ACTION_SPEC) {
+ override fun build(): ActionCapability {
+ super.setProperty(Property.Builder().build())
+ return super.build()
+ }
+ }
+
+ // TODO(b/268369632): Remove Property from public capability APIs.
+ class Property internal constructor(
+ val timerList: SimpleProperty?
+ ) {
+ override fun toString(): String {
+ return "Property(timerList=$timerList}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Property
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder {
+ private var timerList: SimpleProperty? = null
+
+ fun setTimerList(timerList: SimpleProperty): Builder =
+ apply { this.timerList = timerList }
+
+ fun build(): Property = Property(timerList)
+ }
+ }
+
+ class Argument internal constructor(
+ val timerList: List<Timer>?
+ ) {
+ override fun toString(): String {
+ return "Argument(timerList=$timerList)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Argument
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder : BuilderOf<Argument> {
+ private var timerList: List<Timer>? = null
+
+ fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+
+ override fun build(): Argument = Argument(timerList)
+ }
+ }
+
+ class Output internal constructor(val executionStatus: ExecutionStatus?) {
+ override fun toString(): String {
+ return "Output(executionStatus=$executionStatus)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Output
+
+ if (executionStatus != other.executionStatus) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return executionStatus.hashCode()
+ }
+
+ class Builder {
+ private var executionStatus: ExecutionStatus? = null
+
+ fun setExecutionStatus(executionStatus: ExecutionStatus): Builder =
+ apply { this.executionStatus = executionStatus }
+
+ fun build(): Output = Output(executionStatus)
+ }
+ }
+
+ class ExecutionStatus {
+ private var successStatus: SuccessStatus? = null
+ private var genericErrorStatus: GenericErrorStatus? = null
+
+ constructor(successStatus: SuccessStatus) {
+ this.successStatus = successStatus
+ }
+
+ constructor(genericErrorStatus: GenericErrorStatus) {
+ this.genericErrorStatus = genericErrorStatus
+ }
+
+ internal fun toParamValue(): ParamValue {
+ var status: String = ""
+ if (successStatus != null) {
+ status = successStatus.toString()
+ }
+ if (genericErrorStatus != null) {
+ status = genericErrorStatus.toString()
+ }
+ val value: Value = Value.newBuilder().setStringValue(status).build()
+ return ParamValue.newBuilder().setStructValue(
+ Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build(),
+ ).build()
+ }
+ }
+
+ class Confirmation internal constructor()
+
+ class TaskUpdater internal constructor() : AbstractTaskUpdater()
+
+ sealed interface Session : BaseSession<Argument, Output>
+}
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
new file mode 100644
index 0000000..c934ef1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.ActionCapability
+import androidx.appactions.interaction.capabilities.core.BaseSession
+import androidx.appactions.interaction.capabilities.core.CapabilityBuilderBase
+import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
+import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
+import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
+import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
+import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
+import androidx.appactions.interaction.capabilities.core.values.Timer
+import androidx.appactions.interaction.proto.ParamValue
+import com.google.protobuf.Struct
+import com.google.protobuf.Value
+import java.util.Optional
+
+/** StopTimer.kt in interaction-capabilities-productivity */
+private const val CAPABILITY_NAME = "actions.intent.STOP_TIMER"
+
+private val ACTION_SPEC = ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+ .setDescriptor(StopTimer.Property::class.java)
+ .setArgument(StopTimer.Argument::class.java, StopTimer.Argument::Builder)
+ .setOutput(StopTimer.Output::class.java).bindRepeatedStructParameter(
+ "timer",
+ { property -> Optional.ofNullable(property.timerList) },
+ StopTimer.Argument.Builder::setTimerList,
+ TypeConverters::toTimer
+ ).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.
+class StopTimer private constructor() {
+
+ class CapabilityBuilder :
+ CapabilityBuilderBase<
+ CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+ >(ACTION_SPEC) {
+ override fun build(): ActionCapability {
+ super.setProperty(Property.Builder().build())
+ return super.build()
+ }
+ }
+
+ // TODO(b/268369632): Remove Property from public capability APIs.
+ class Property internal constructor(
+ val timerList: SimpleProperty?
+ ) {
+ override fun toString(): String {
+ return "Property(timerList=$timerList}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Property
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder {
+ private var timerList: SimpleProperty? = null
+
+ fun setTimerList(timerList: SimpleProperty): Builder =
+ apply { this.timerList = timerList }
+
+ fun build(): Property = Property(timerList)
+ }
+ }
+
+ class Argument internal constructor(
+ val timerList: List<Timer>?
+ ) {
+ override fun toString(): String {
+ return "Argument(timerList=$timerList)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Argument
+
+ if (timerList != other.timerList) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return timerList.hashCode()
+ }
+
+ class Builder : BuilderOf<Argument> {
+ private var timerList: List<Timer>? = null
+
+ fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+
+ override fun build(): Argument = Argument(timerList)
+ }
+ }
+
+ class Output internal constructor(val executionStatus: ExecutionStatus?) {
+ override fun toString(): String {
+ return "Output(executionStatus=$executionStatus)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Output
+
+ if (executionStatus != other.executionStatus) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return executionStatus.hashCode()
+ }
+
+ class Builder {
+ private var executionStatus: ExecutionStatus? = null
+
+ fun setExecutionStatus(executionStatus: ExecutionStatus): Builder =
+ apply { this.executionStatus = executionStatus }
+
+ fun build(): Output = Output(executionStatus)
+ }
+ }
+
+ class ExecutionStatus {
+ private var successStatus: SuccessStatus? = null
+ private var genericErrorStatus: GenericErrorStatus? = null
+
+ constructor(successStatus: SuccessStatus) {
+ this.successStatus = successStatus
+ }
+
+ constructor(genericErrorStatus: GenericErrorStatus) {
+ this.genericErrorStatus = genericErrorStatus
+ }
+
+ internal fun toParamValue(): ParamValue {
+ var status: String = ""
+ if (successStatus != null) {
+ status = successStatus.toString()
+ }
+ if (genericErrorStatus != null) {
+ status = genericErrorStatus.toString()
+ }
+ val value: Value = Value.newBuilder().setStringValue(status).build()
+ return ParamValue.newBuilder().setStructValue(
+ Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build(),
+ ).build()
+ }
+ }
+
+ class Confirmation internal constructor()
+
+ class TaskUpdater internal constructor() : AbstractTaskUpdater()
+
+ sealed interface Session : BaseSession<Argument, Output>
+}
diff --git a/appactions/interaction/interaction-capabilities-safety/build.gradle b/appactions/interaction/interaction-capabilities-safety/build.gradle
index 61cd1d5..27a878e 100644
--- a/appactions/interaction/interaction-capabilities-safety/build.gradle
+++ b/appactions/interaction/interaction-capabilities-safety/build.gradle
@@ -26,11 +26,9 @@
api(libs.autoValueAnnotations)
api(libs.kotlinStdlib)
annotationProcessor(libs.autoValue)
- implementation(libs.protobufLite)
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
implementation("androidx.annotation:annotation:1.1.0")
implementation(project(":appactions:interaction:interaction-capabilities-core"))
- implementation(project(":appactions:interaction:interaction-proto"))
}
android {
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 ec517a7..13770e9 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
@@ -30,8 +30,8 @@
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyAccountNotLoggedIn
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyFeatureNotOnboarded
import androidx.appactions.interaction.proto.ParamValue
-import com.google.protobuf.Struct
-import com.google.protobuf.Value
+import androidx.appactions.interaction.protobuf.Struct
+import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
/** StartEmergencySharing.kt in interaction-capabilities-safety */
@@ -54,7 +54,7 @@
// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
class StartEmergencySharing private constructor() {
- // TODO(b/267805819): Update to include the SessionBuilder once Session API is ready.
+ // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
CapabilityBuilderBase<
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session,
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 df6f81f..accef44 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
@@ -33,8 +33,8 @@
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyAccountNotLoggedIn
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyFeatureNotOnboarded
import androidx.appactions.interaction.proto.ParamValue
-import com.google.protobuf.Struct
-import com.google.protobuf.Value
+import androidx.appactions.interaction.protobuf.Struct
+import androidx.appactions.interaction.protobuf.Value
import java.time.Duration
import java.time.ZonedDateTime
import java.util.Optional
@@ -73,7 +73,7 @@
// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
class StartSafetyCheck private constructor() {
- // TODO(b/267805819): Update to include the SessionBuilder once Session API is ready.
+ // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
CapabilityBuilderBase<
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
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 e36f3b44..43d790e 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
@@ -30,8 +30,8 @@
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyAccountNotLoggedIn
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyFeatureNotOnboarded
import androidx.appactions.interaction.proto.ParamValue
-import com.google.protobuf.Struct
-import com.google.protobuf.Value
+import androidx.appactions.interaction.protobuf.Struct
+import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
/** StopEmergencySharing.kt in interaction-capabilities-safety */
@@ -54,7 +54,7 @@
// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
class StopEmergencySharing private constructor() {
- // TODO(b/267805819): Update to include the SessionBuilder once Session API is ready.
+ // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
CapabilityBuilderBase<
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session,
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 257ba85..f3db417 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
@@ -30,8 +30,8 @@
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyAccountNotLoggedIn
import androidx.appactions.interaction.capabilities.safety.executionstatus.SafetyFeatureNotOnboarded
import androidx.appactions.interaction.proto.ParamValue
-import com.google.protobuf.Struct
-import com.google.protobuf.Value
+import androidx.appactions.interaction.protobuf.Struct
+import androidx.appactions.interaction.protobuf.Value
import java.util.Optional
/** StopSafetyCheck.kt in interaction-capabilities-safety */
@@ -51,7 +51,7 @@
// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
class StopSafetyCheck private constructor() {
- // TODO(b/267805819): Update to include the SessionBuilder once Session API is ready.
+ // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
class CapabilityBuilder :
CapabilityBuilderBase<
CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
diff --git a/appactions/interaction/interaction-proto/build.gradle b/appactions/interaction/interaction-proto/build.gradle
index 7fe89ea..b34ff5c 100644
--- a/appactions/interaction/interaction-proto/build.gradle
+++ b/appactions/interaction/interaction-proto/build.gradle
@@ -18,14 +18,47 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("com.github.johnrengelman.shadow")
id("com.google.protobuf")
}
+configurations {
+ shadowed
+ compileOnly.extendsFrom(shadowed)
+ testCompile.extendsFrom(shadowed)
+ shadowJar // configuration containing Jar built by shadow plugin
+ protoJar // configuration containing compiled proto and source
+}
+
dependencies {
- implementation(libs.protobufLite)
+ shadowed(libs.protobufLite)
+
implementation("androidx.annotation:annotation:1.1.0")
}
+task protoLiteJar(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ archiveClassifier = 'proto-lite-jar'
+ dependsOn(":appactions:interaction:interaction-proto:syncReleaseLibJars")
+ from("${buildDir}/intermediates/aar_main_jar/release")
+}
+assemble.dependsOn(protoLiteJar)
+
+task shadowJar(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ archiveClassifier = 'shadow-jar'
+ configurations = [project.configurations.shadowed]
+ dependsOn(protoLiteJar)
+ from(protoLiteJar.archiveFile)
+ relocate "com.google.protobuf", "androidx.appactions.interaction.protobuf"
+ exclude("**/*.proto")
+}
+assemble.dependsOn(shadowJar)
+
+artifacts {
+ // Specifies the output files for the shadowJar and protoJar configurations
+ shadowJar shadowJar.archiveFile
+ protoJar protoLiteJar.archiveFile
+}
+
protobuf {
protoc {
artifact = libs.protobufCompiler.get()
@@ -46,6 +79,22 @@
android {
namespace "androidx.appactions.interaction.proto"
+ defaultConfig {
+ minSdkVersion 19
+ }
+ libraryVariants.all { variant ->
+ // Replace the default jar with the shadow jar in the AAR.
+ def packageLib = variant.getPackageLibraryProvider().get()
+ packageLib.exclude('classes.jar')
+ packageLib.into('') {
+ from(project.tasks.getByName("shadowJar").outputs)
+ rename { "classes.jar" }
+ }
+ }
+ lintOptions {
+ // protobuf generates unannotated and synthetic accessor methods
+ disable("UnknownNullness", "SyntheticAccessor")
+ }
}
androidx {
@@ -53,4 +102,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "Protos for use with App Action interaction libraries."
-}
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-proto/src/main/java/androidx/appactions/interaction/proto/package-info.java b/appactions/interaction/interaction-proto/src/main/java/androidx/appactions/interaction/proto/package-info.java
index bc0d29b7..5a37125 100644
--- a/appactions/interaction/interaction-proto/src/main/java/androidx/appactions/interaction/proto/package-info.java
+++ b/appactions/interaction/interaction-proto/src/main/java/androidx/appactions/interaction/proto/package-info.java
@@ -15,7 +15,4 @@
*/
/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-package androidx.appactions.interaction.proto;
-
-import androidx.annotation.RestrictTo;
+package androidx.appactions.interaction.proto;
\ No newline at end of file
diff --git a/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto b/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
index 3548f99..e07f1aa 100644
--- a/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
+++ b/appactions/interaction/interaction-proto/src/main/proto/app_actions_data.proto
@@ -31,8 +31,9 @@
string string_value = 2;
bool bool_value = 3;
double number_value = 4;
- google.protobuf.Struct struct_value = 5;
+ google.protobuf.Struct struct_value = 6;
}
+ reserved 5; // Deleted DateTime type.
}
message Entity {
@@ -182,8 +183,7 @@
bool is_focused = 3;
}
- // Current version of the protocol. Populated with values from the artifact version of the
- // interaction-proto Jetpack library.
+ // Current version of the protocol. Populated with from the interaction-capabiliites-core library.
optional Version version = 4;
// Represents the dynamic capabilities declared by an App. Capabilities
@@ -372,7 +372,7 @@
// The patch version.
optional uint64 patch = 3;
- // The prerelease version: a series of dot-separated identifiers.
+ // The prerelease version suffix.
optional string prerelease_id = 4;
}
diff --git a/appactions/interaction/interaction-service-proto/build.gradle b/appactions/interaction/interaction-service-proto/build.gradle
index c586cf5..5fcc43c 100644
--- a/appactions/interaction/interaction-service-proto/build.gradle
+++ b/appactions/interaction/interaction-service-proto/build.gradle
@@ -13,23 +13,85 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import androidx.build.LibraryType
import androidx.build.Publish
import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
- id("com.android.library")
+ id("java-library")
+ id("com.google.protobuf")
}
dependencies {
- annotationProcessor(libs.nullaway)
- // Add dependencies here
+ // TODO(b/268709908): Bump this to version 1.52.0 and make available from libs.grpcProtobufLite
+ implementation("io.grpc:grpc-protobuf-lite:1.45.1") {
+ // Ensure we only bundle grpc-protobuf-lite. Any of its dependencies should be added
+ // as `compileOnly` dependencies below.
+ exclude group: 'com.google.protobuf'
+ exclude group: 'com.google.guava'
+ exclude group: 'io.grpc'
+ exclude group: 'com.google.code.findbugs'
+ }
+
+ // We need to use the non-shadow configurations at compile time to pick up the protos at the
+ // original package location (before renaming) since that's what the compiled service protos
+ // expect. The final AAR (interaction-service) will have the renamed/shadowed configurations.
+ compileOnly(project(path:":appactions:interaction:interaction-proto", configuration:"protoJar"))
+
+ // These are the compile-time dependencies needed to build the interaction-service-proto
+ // with the grpc-protobuf-lite dependencies bundled. They need to be added as dependencies in
+ // any library that bundles interaction-service-proto.
+ compileOnly(libs.protobufLite)
+ compileOnly(libs.grpcStub)
+ compileOnly("androidx.annotation:annotation:1.1.0")
+ compileOnly("javax.annotation:javax.annotation-api:1.3.2")
}
-android {
- namespace "androidx.appactions.interaction.service.proto"
+protobuf {
+ protoc {
+ artifact = libs.protobufCompiler.get()
+ }
+ // Configure the codegen plugins for GRPC.
+ plugins {
+ grpc {
+ artifact = 'io.grpc:protoc-gen-grpc-java:1.52.0'
+ }
+ }
+
+ // Generates the java proto-lite code for the protos in this project. See
+ // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+ // for more information.
+ generateProtoTasks {
+ // Add any additional directories specified in the "main" source set to the Java
+ // source directories of the main source set.
+ ofSourceSet("main").each { task ->
+ sourceSets.main.java.srcDir(task)
+ }
+ all().each { task ->
+ task.builtins {
+ java {
+ option "lite"
+ }
+ }
+ task.plugins {
+ grpc {
+ option 'lite'
+ }
+ }
+ }
+ }
+}
+
+afterEvaluate {
+ lint {
+ lintOptions {
+ // protobuf generates unannotated and synthetic accessor methods
+ disable("UnknownNullness", "SyntheticAccessor")
+ abortOnError(false)
+ checkReleaseBuilds(false)
+ }
+ }
}
androidx {
diff --git a/appactions/interaction/interaction-service-proto/src/main/java/androidx/appactions/interaction/package-info.java b/appactions/interaction/interaction-service-proto/src/main/java/androidx/appactions/interaction/package-info.java
index f6042aa..28ff8dc 100644
--- a/appactions/interaction/interaction-service-proto/src/main/java/androidx/appactions/interaction/package-info.java
+++ b/appactions/interaction/interaction-service-proto/src/main/java/androidx/appactions/interaction/package-info.java
@@ -15,6 +15,7 @@
*/
/**
- * Insert package level documentation here
+ * Internal protos for interaction-service.
+ * @hide
*/
package androidx.appactions.interaction.service.proto;
diff --git a/appactions/interaction/interaction-service/src/main/proto/app_interaction_service.proto b/appactions/interaction/interaction-service-proto/src/main/proto/app_interaction_service.proto
similarity index 100%
rename from appactions/interaction/interaction-service/src/main/proto/app_interaction_service.proto
rename to appactions/interaction/interaction-service-proto/src/main/proto/app_interaction_service.proto
diff --git a/appactions/interaction/interaction-service/build.gradle b/appactions/interaction/interaction-service/build.gradle
index 226a274..acac500 100644
--- a/appactions/interaction/interaction-service/build.gradle
+++ b/appactions/interaction/interaction-service/build.gradle
@@ -13,31 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import androidx.build.BundleInsideHelper
import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("com.google.protobuf")
id("org.jetbrains.kotlin.android")
}
+BundleInsideHelper.forInsideAar(
+ project,
+ [
+ new BundleInsideHelper.Relocation(
+ /* from = */ "io.grpc.protobuf",
+ /* to = */ "androidx.appactions.interaction.grpc.protobuf"),
+ new BundleInsideHelper.Relocation(
+ /* from = */ "com.google.protobuf",
+ /* to = */ "androidx.appactions.interaction.protobuf")
+ ]
+)
+
dependencies {
+ bundleInside(project(":appactions:interaction:interaction-service-proto"))
+
+ implementation(project(":appactions:interaction:interaction-capabilities-core"))
implementation(libs.grpcAndroid)
implementation(libs.grpcBinder)
implementation(libs.grpcStub)
- implementation(libs.protobufLite)
+ implementation(libs.kotlinStdlib)
implementation("androidx.annotation:annotation:1.1.0")
implementation("androidx.concurrent:concurrent-futures:1.1.0")
implementation("androidx.wear.tiles:tiles:1.1.0")
implementation("javax.annotation:javax.annotation-api:1.3.2")
- // TODO(b/268709908): Bump this to version 1.52.0 and make available from libs.grpcProtobufLite
- implementation("io.grpc:grpc-protobuf-lite:1.45.1")
- implementation(project(":appactions:interaction:interaction-capabilities-core"))
- implementation(project(":appactions:interaction:interaction-proto"))
- implementation(libs.kotlinStdlib)
+ testImplementation(project(":appactions:interaction:interaction-service-proto"))
testImplementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testImplementation(libs.robolectric)
@@ -46,36 +56,7 @@
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.truth)
-}
-
-protobuf {
- protoc {
- artifact = libs.protobufCompiler.get()
- }
- // Configure the codegen plugins
- plugins {
- grpc {
- artifact = 'io.grpc:protoc-gen-grpc-java:1.52.0'
- }
- }
-
- // Generates the java proto-lite code for the protos in this project. See
- // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
- // for more information.
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- java {
- option "lite"
- }
- }
- task.plugins {
- grpc {
- option 'lite'
- }
- }
- }
- }
+ testImplementation(libs.protobufLite)
}
android {
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/TileLayoutInternal.kt b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/TileLayoutInternal.kt
index fa4f3d8..2604f05 100644
--- a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/TileLayoutInternal.kt
+++ b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/TileLayoutInternal.kt
@@ -20,7 +20,7 @@
import androidx.appactions.interaction.service.proto.AppInteractionServiceProto
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.ResourceBuilders
-import com.google.protobuf.ByteString
+import androidx.appactions.interaction.protobuf.ByteString
/**
* Holder for TileLayout response.
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
index 4316711..2684330 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerExtension.kt
@@ -26,7 +26,6 @@
open class BaselineProfileConsumerExtension {
companion object {
-
private const val EXTENSION_NAME = "baselineProfile"
internal fun registerExtension(project: Project): BaselineProfileConsumerExtension {
@@ -40,18 +39,23 @@
}
/**
- * Enables on-demand baseline profile generation. Baseline profiles can be generated
- * periodically or on-demand. Setting this flag to true will enable on-demand generation.
- * When on-demand generation is enabled the baseline profile is regenerated before building the
- * release build type. Note that in on-demand mode the baseline profile file is NOT saved in
- * `src/<variant>/generated/baselineProfiles` folder, as opposite to the periodic generation
- * where the latest baseline profile is always stored in the sources.
+ * Specifies whether generated baseline profiles should be stored in the src folder.
+ * When this flag is set to true, the generated baseline profiles are stored in
+ * `src/<variant>/generated/baselineProfiles`.
*/
- var onDemandGeneration = false
+ var saveInSrc = true
/**
- * Specifies the output directory for generated baseline profile when [onDemandGeneration] is
- * off. Note that the dir specified here is created in the `src/<variant>/` folder.
+ * Specifies whether baseline profiles should be regenerated when building, for example, during
+ * a full release build for distribution. When set to true a new profile is generated as part
+ * of building the release build. This including rebuilding the non minified release, running
+ * the baseline profile tests and ultimately building the release build.
+ */
+ var automaticGenerationDuringBuild = false
+
+ /**
+ * Specifies the output directory for generated baseline profiles when [saveInSrc] is
+ * `true`. Note that the dir specified here is created in the `src/<variant>/` folder.
*/
var baselineProfileOutputDir = "generated/baselineProfiles"
@@ -64,9 +68,9 @@
* `src/<variant>/generated/baselineProfiles`'.
* If this is not specified, by default it will be true for library modules and false for
* application modules.
- * Note that when generation is onDemand the output folder is always in the build output
- * folder but this setting still determines whether the profile included in the built apk or
- * aar is merged into a single one.
+ * Note that when `saveInSrc` is false the output folder is in the build output folder but
+ * this setting still determines whether the profile included in the built apk or
+ * aar includes all the variant profiles.
*/
var mergeIntoMain: Boolean? = null
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
index fc3fb4f..b2423b9 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt
@@ -23,6 +23,7 @@
import androidx.baselineprofile.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
import androidx.baselineprofile.gradle.utils.INTERMEDIATES_BASE_FOLDER
import androidx.baselineprofile.gradle.utils.TASK_NAME_SUFFIX
+import androidx.baselineprofile.gradle.utils.afterVariants
import androidx.baselineprofile.gradle.utils.camelCase
import androidx.baselineprofile.gradle.utils.checkAgpVersion
import androidx.baselineprofile.gradle.utils.isAgpVersionAtLeast
@@ -34,20 +35,23 @@
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.api.variant.Variant
import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Category
-import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
+import org.gradle.work.DisableCachingByDefault
+
+private const val GENERATE_TASK_NAME = "generate"
+private const val MERGE_TASK_NAME = "merge"
+private const val COPY_TASK_NAME = "copy"
/**
* This is the consumer plugin for baseline profile generation. In order to generate baseline
@@ -59,7 +63,6 @@
class BaselineProfileConsumerPlugin : Plugin<Project> {
companion object {
- private const val GENERATE_TASK_NAME = "generate"
private const val RELEASE = "release"
private const val PROPERTY_R8_REWRITE_BASELINE_PROFILE_RULES =
"android.experimental.art-profile-r8-rewriting"
@@ -158,6 +161,9 @@
.map { it.name })
}
+ // A list of blocks to execute after agp tasks have been created
+ val afterVariantBlocks = mutableListOf<() -> (Unit)>()
+
// Iterate baseline profile variants to create per-variant tasks and configurations
project
.extensions
@@ -209,20 +215,23 @@
// is true, calling a generation task for a specific build type will merge
// profiles for all the variants of that build type and output it in the `main`
// folder.
- val (taskName, outputVariantFolder) = if (mergeIntoMain) {
+ val (mergeAwareVariantName, mergeAwareVariantOutput) = if (mergeIntoMain) {
listOf(variant.buildType ?: "", "main")
} else {
listOf(variant.name, variant.name)
}
- // Creates the task to merge the baseline profile artifacts coming from different
- // configurations. Note that this is the last task of the chain that triggers the
- // whole generation, hence it's called `generate`. The name is generated according
- // to the value of the `merge`.
- val genBaselineProfileTaskProvider = project
+ // Creates the task to merge the baseline profile artifacts coming from
+ // different configurations.
+ val mergedTaskOutputDir = project
+ .layout
+ .buildDirectory
+ .dir("$INTERMEDIATES_BASE_FOLDER/$mergeAwareVariantOutput/merged")
+
+ val mergeTaskProvider = project
.tasks
- .maybeRegister<GenerateBaselineProfileTask>(
- GENERATE_TASK_NAME, taskName, TASK_NAME_SUFFIX,
+ .maybeRegister<MergeBaselineProfileTask>(
+ MERGE_TASK_NAME, mergeAwareVariantName, TASK_NAME_SUFFIX,
) { task ->
// These are all the configurations this task depends on,
@@ -233,14 +242,9 @@
.from
.add(baselineProfileConfiguration)
- // This is the task output for the generated baseline profile
- task.baselineProfileDir.set(
- baselineProfileExtension.baselineProfileOutputDir(
- project = project,
- variantName = outputVariantFolder,
- outputDir = baselineProfileExtension.baselineProfileOutputDir
- )
- )
+ // This is the task output for the generated baseline profile. Output
+ // is always stored in the intermediates
+ task.baselineProfileDir.set(mergedTaskOutputDir)
// Sets the package filter rules. If this is the first task
task.filterRules.addAll(
@@ -257,46 +261,126 @@
)
}
- // The output folders for variant and main profiles are added as source dirs using
- // source sets api. This cannot be done in the `configure` block of the generation
- // task. The `onDemand` flag is checked here and the src set folder is chosen
- // accordingly: if `true`, baseline profiles are saved in the src folder so they
- // can be committed with srcs, if `false` they're stored in the generated build
- // files.
- if (baselineProfileExtension.onDemandGeneration) {
- variant.sources.baselineProfiles?.apply {
- addGeneratedSourceDirectory(
- genBaselineProfileTaskProvider,
- GenerateBaselineProfileTask::baselineProfileDir
- )
- }
- } else {
- val baselineProfileSourcesFile = baselineProfileExtension
- .baselineProfileOutputDir(
- project = project,
- variantName = outputVariantFolder,
- outputDir = baselineProfileExtension.baselineProfileOutputDir
- )
- .get()
- .asFile
+ // If `saveInSrc` is true, we create an additional task to copy the output
+ // of the merge task in the src folder.
+ val lastTaskProvider = if (baselineProfileExtension.saveInSrc) {
- // If the folder does not exist it means that the profile has not been
- // generated so we don't need to add to sources.
- if (baselineProfileSourcesFile.exists()) {
- variant.sources.baselineProfiles?.addStaticSourceDirectory(
- baselineProfileSourcesFile.absolutePath
- )
+ val baselineProfileOutputDir =
+ baselineProfileExtension.baselineProfileOutputDir
+ val srcOutputDir = project
+ .layout
+ .projectDirectory
+ .dir("src/$mergeAwareVariantOutput/$baselineProfileOutputDir/")
+
+ // This task copies the baseline profile generated from the merge task.
+ // Note that we're reutilizing the [MergeBaselineProfileTask] because
+ // if the flag `mergeIntoMain` is true tasks will have the same name
+ // and we just want to add more file to copy to the same output. This is
+ // already handled in the MergeBaselineProfileTask.
+ val copyTaskProvider = project
+ .tasks
+ .maybeRegister<MergeBaselineProfileTask>(
+ COPY_TASK_NAME, mergeAwareVariantName, "baselineProfileIntoSrc",
+ ) { task ->
+ task.baselineProfileFileCollection
+ .from
+ .add(mergeTaskProvider.flatMap { it.baselineProfileDir })
+ task.baselineProfileDir.set(srcOutputDir)
+ }
+
+ // Applies the source path for this variant
+ srcOutputDir.asFile.apply {
+ mkdirs()
+ variant
+ .sources
+ .baselineProfiles?.addStaticSourceDirectory(absolutePath)
}
+
+ // Depending on whether the flag `automaticGenerationDuringBuild` is enabled
+ // we can set either a dependsOn or a mustRunAfter dependency between the
+ // task that packages the profile and the copy. Note that we cannot use
+ // the variant src set api `addGeneratedSourceDirectory` since that
+ // overwrites the outputDir, that would be re-set in the build dir.
+ afterVariantBlocks.add {
+
+ // Determines which AGP task to depend on based on whether this is an
+ // app or a library.
+ if (isApplication) {
+ project.tasks.named(
+ camelCase("merge", variant.name, "artProfile")
+ )
+ } else {
+ project.tasks.named(
+ camelCase("prepare", variant.name, "artProfile")
+ )
+ }.configure {
+
+ // Sets the task dependency according to the configuration flag.
+ if (baselineProfileExtension.automaticGenerationDuringBuild) {
+ it.dependsOn(copyTaskProvider)
+ } else {
+ it.mustRunAfter(copyTaskProvider)
+ }
+ }
+ }
+
+ // In this case the last task is the copy task.
+ copyTaskProvider
+ } else {
+
+ if (baselineProfileExtension.automaticGenerationDuringBuild) {
+
+ // If the flag `automaticGenerationDuringBuild` is true, we can set the
+ // merge task to provide generated sources for the variant, using the
+ // src set variant api. This means that we don't need to manually depend
+ // on the merge or prepare art profile task.
+ variant
+ .sources
+ .baselineProfiles?.addGeneratedSourceDirectory(
+ taskProvider = mergeTaskProvider,
+ wiredWith = MergeBaselineProfileTask::baselineProfileDir
+ )
+ } else {
+
+ // This is the case of `saveInSrc` and `automaticGenerationDuringBuild`
+ // both false, that is unsupported. In this case we simply throw an
+ // error.
+ if (!project.isGradleSyncRunning()) {
+ throw GradleException(
+ """
+ The current configuration of flags `saveInSrc` and
+ `automaticGenerationDuringBuild` is not supported. At least
+ one of these should be set to `true`. Please review your
+ baseline profile plugin configuration in your build.gradle.
+ """.trimIndent()
+ )
+ }
+ }
+
+ // In this case the last task is the merge task.
+ mergeTaskProvider
}
- // Here we create a task hierarchy to trigger generations for all the variants
- // of a specific build type, flavor or all of them. If `mergeIntoMain` is true,
- // only one generation task exists so there is no need to create parent tasks.
- if (!mergeIntoMain && variant.name != variant.buildType) {
- maybeCreateParentGenTask<Task>(
- project,
- variant.buildType,
- genBaselineProfileTaskProvider
+ // Here we create the final generate task that triggers the whole generation
+ // for this variant and all the parent tasks. For this one the child task
+ // is either copy or merge, depending on the configuration.
+ val variantGenerateTask = maybeCreateGenerateTask<Task>(
+ project = project,
+ variantName = mergeAwareVariantName,
+ childGenerationTaskProvider = lastTaskProvider
+ )
+
+ // Create the build type task. For example `generateReleaseBaselineProfile`
+ // The variant name is equal to the build type name if there are no flavors.
+ // Note that if `mergeIntoMain` is `true` the build type task already exists.
+ if (!mergeIntoMain &&
+ !variant.buildType.isNullOrBlank() &&
+ variant.name != variant.buildType
+ ) {
+ maybeCreateGenerateTask<Task>(
+ project = project,
+ variantName = variant.buildType!!,
+ childGenerationTaskProvider = variantGenerateTask
)
}
@@ -307,29 +391,30 @@
// build type until that bug is fixed, when running the global task
// `generateBaselineProfile`. This can be removed after fix.
if (variant.buildType == RELEASE) {
- maybeCreateParentGenTask<MainGenerateBaselineProfileTask>(
+ maybeCreateGenerateTask<MainGenerateBaselineProfileTask>(
project,
"",
- genBaselineProfileTaskProvider
+ variantGenerateTask
)
}
}
}
+
+ // After variants have been resolved the AGP tasks have been created, so we can set our
+ // task dependency if any.
+ project.afterVariants {
+ afterVariantBlocks.forEach { it() }
+ }
}
- private inline fun <reified T : Task> maybeCreateParentGenTask(
+ private inline fun <reified T : Task> maybeCreateGenerateTask(
project: Project,
- parentName: String?,
- childGenerationTaskProvider: TaskProvider<GenerateBaselineProfileTask>
- ) {
- if (parentName == null) return
- project.tasks.maybeRegister<T>(GENERATE_TASK_NAME, parentName, TASK_NAME_SUFFIX) {
- it.group =
- "Baseline Profile"
- it.description =
- "Generates a baseline profile for the specified variants or dimensions."
- it.dependsOn(childGenerationTaskProvider)
- }
+ variantName: String,
+ childGenerationTaskProvider: TaskProvider<*>? = null
+ ) = project.tasks.maybeRegister<T>(GENERATE_TASK_NAME, variantName, TASK_NAME_SUFFIX) {
+ it.group = "Baseline Profile"
+ it.description = "Generates a baseline profile for the specified variants or dimensions."
+ if (childGenerationTaskProvider != null) it.dependsOn(childGenerationTaskProvider)
}
private fun createBaselineProfileConfigurationForVariant(
@@ -409,39 +494,16 @@
}
}
}
-
- private fun BaselineProfileConsumerExtension.baselineProfileOutputDir(
- project: Project,
- outputDir: String,
- variantName: String
- ): Provider<Directory> =
- if (onDemandGeneration) {
-
- // In on demand mode, the baseline profile is regenerated when building
- // release and it's not saved in the module sources. To achieve this
- // we can create an intermediate folder for the profile and add the
- // generation task to src sets.
- project
- .layout
- .buildDirectory
- .dir("$INTERMEDIATES_BASE_FOLDER/$variantName/$outputDir")
- } else {
-
- // In periodic mode the baseline profile generation is manually triggered.
- // The baseline profile is stored in the baseline profile sources for
- // the variant.
- project.providers.provider {
- project
- .layout
- .projectDirectory
- .dir("src/$variantName/$outputDir/")
- }
- }
}
-@CacheableTask
+@DisableCachingByDefault(because = "Not worth caching.")
abstract class MainGenerateBaselineProfileTask : DefaultTask() {
+ init {
+ group = "Baseline Profile"
+ description = "Generates a baseline profile"
+ }
+
@TaskAction
fun exec() {
this.logger.warn(
@@ -464,7 +526,7 @@
}
}
-@CacheableTask
+@DisableCachingByDefault(because = "Not worth caching.")
abstract class GenerateDummyBaselineProfileTask : DefaultTask() {
companion object {
@@ -485,6 +547,7 @@
)
it.variantName.set(variant.name)
}
+ @Suppress("UnstableApiUsage")
variant.sources.baselineProfiles?.addGeneratedSourceDirectory(
taskProvider, GenerateDummyBaselineProfileTask::outputDir
)
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/GenerateBaselineProfileTask.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/MergeBaselineProfileTask.kt
similarity index 91%
rename from benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/GenerateBaselineProfileTask.kt
rename to benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/MergeBaselineProfileTask.kt
index cc84875..3ddb23c 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/GenerateBaselineProfileTask.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/MergeBaselineProfileTask.kt
@@ -40,7 +40,7 @@
* duplication but mostly the profile file will be unnecessarily larger.
*/
@CacheableTask
-abstract class GenerateBaselineProfileTask : DefaultTask() {
+abstract class MergeBaselineProfileTask : DefaultTask() {
companion object {
@@ -58,11 +58,6 @@
@get:OutputDirectory
abstract val baselineProfileDir: DirectoryProperty
- init {
- group = "Baseline Profile"
- description = "Generates a baseline profile."
- }
-
@TaskAction
fun exec() {
@@ -98,7 +93,22 @@
// - apply the filters
// - sort with comparator
val profileRules = baselineProfileFileCollection.files
- .flatMap { it.readLines() }
+ .flatMap {
+ if (!it.exists()) {
+ // Note this can happen only if this task is misconfigured because of a bug and
+ // not because of any user configuration.
+ throw GradleException(
+ """
+ The specified merge task input `${it.absolutePath}` does not exist.
+ """.trimIndent()
+ )
+ }
+ if (it.isFile) {
+ it.readLines()
+ } else {
+ it.listFiles()!!.flatMap { f -> f.readLines() }
+ }
+ }
.sorted()
.asSequence()
.mapNotNull { ProfileRule.parse(it) }
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Agp.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Agp.kt
index 718d9fd..b4692b74f 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Agp.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Agp.kt
@@ -17,7 +17,10 @@
package androidx.baselineprofile.gradle.utils
import com.android.build.api.AndroidPluginVersion
+import com.android.build.api.dsl.TestedExtension
import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.gradle.AppExtension
+import com.android.build.gradle.LibraryExtension
import org.gradle.api.GradleException
import org.gradle.api.Project
@@ -69,3 +72,28 @@
val agpVersion = agpVersion() ?: return false
return agpVersion >= minVersion
}
+
+internal fun Project.afterVariants(block: () -> (Unit)) {
+ val extensionVariants =
+ when (val tested = extensions.getByType(TestedExtension::class.java)) {
+ is AppExtension -> tested.applicationVariants
+ is LibraryExtension -> tested.libraryVariants
+ else -> {
+ if (isGradleSyncRunning()) {
+ return
+ }
+ throw GradleException(
+ """
+ Unrecognized extension: $tested not of type AppExtension or LibraryExtension.
+ """.trimIndent()
+ )
+ }
+ }
+
+ var applied = false
+ extensionVariants.all {
+ if (applied) return@all
+ applied = true
+ block()
+ }
+}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
index bf96ca3..b679766 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
@@ -18,6 +18,7 @@
import androidx.baselineprofile.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
import androidx.baselineprofile.gradle.utils.GRADLE_CODE_PRINT_TASK
+import androidx.baselineprofile.gradle.utils.build
import androidx.baselineprofile.gradle.utils.buildAndAssertThatOutput
import androidx.baselineprofile.gradle.utils.camelCase
import androidx.testutils.gradle.ProjectSetupRule
@@ -40,6 +41,9 @@
companion object {
private const val expectedBaselineProfileOutputFolder = "generated/baselineProfiles"
+ private const val ANDROID_APPLICATION_PLUGIN = "com.android.application"
+ private const val ANDROID_LIBRARY_PLUGIN = "com.android.library"
+ private const val ANDROID_TEST_PLUGIN = "com.android.test"
}
private val rootFolder = TemporaryFolder().also { it.create() }
@@ -70,69 +74,24 @@
.withPluginClasspath()
}
- private fun writeDefaultProducerProject() {
- producerProjectSetup.writeDefaultBuildGradle(
- prefix = MockProducerBuildGrade()
- .withConfiguration(flavor = "", buildType = "release")
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_1_METHOD_1,
- Fixtures.CLASS_2_METHOD_2,
- Fixtures.CLASS_2,
- Fixtures.CLASS_1,
- ),
- flavor = "",
- buildType = "release"
- )
- .build(),
- suffix = ""
- )
- }
+ private fun baselineProfileFile(variantName: String) = File(
+ consumerProjectSetup.rootDir,
+ "src/$variantName/$expectedBaselineProfileOutputFolder/baseline-prof.txt"
+ )
- private fun readBaselineProfileFileContent(variantName: String) =
- File(
- consumerProjectSetup.rootDir,
- "src/$variantName/$expectedBaselineProfileOutputFolder/baseline-prof.txt"
- ).readLines()
+ private fun readBaselineProfileFileContent(variantName: String): List<String> =
+ baselineProfileFile(variantName).readLines()
@Test
fun testGenerateTaskWithNoFlavors() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.library")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- """.trimIndent(),
- suffix = ""
+ setupConsumerProject(
+ androidPlugin = ANDROID_LIBRARY_PLUGIN,
+ dependencyOnProducerProject = true,
+ flavors = false
)
- producerProjectSetup.writeDefaultBuildGradle(
- prefix = MockProducerBuildGrade()
- .withConfiguration(flavor = "", buildType = "release")
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_1_METHOD_1,
- Fixtures.CLASS_1,
- ),
- flavor = "",
- buildType = "release"
- )
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_2_METHOD_1,
- Fixtures.CLASS_2,
- ),
- flavor = "",
- buildType = "release"
- )
- .build(),
- suffix = ""
+ setupProducerProject(
+ listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
)
gradleRunner
@@ -150,64 +109,22 @@
@Test
fun testGenerateTaskWithFlavorsAndDefaultMerge() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.application")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- productFlavors {
- flavorDimensions = ["version"]
- free {
- dimension "version"
- }
- paid {
- dimension "version"
- }
- }
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- """.trimIndent(),
- suffix = ""
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ dependencyOnProducerProject = true
)
- producerProjectSetup.writeDefaultBuildGradle(
- prefix = MockProducerBuildGrade()
- .withConfiguration(flavor = "free", buildType = "release")
- .withConfiguration(flavor = "paid", buildType = "release")
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_1_METHOD_1,
- Fixtures.CLASS_1,
- ),
- flavor = "free",
- buildType = "release"
- )
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_2_METHOD_1,
- Fixtures.CLASS_2,
- ),
- flavor = "paid",
- buildType = "release"
- )
- .build(),
- suffix = ""
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
)
// Asserts that all per-variant, per-flavor and per-build type tasks are being generated.
- gradleRunner
- .withArguments("tasks", "--stacktrace")
- .build()
- .output
- .also {
- assertThat(it).contains("generateReleaseBaselineProfile - ")
- assertThat(it).contains("generateFreeReleaseBaselineProfile - ")
- assertThat(it).contains("generatePaidReleaseBaselineProfile - ")
- }
+ gradleRunner.buildAndAssertThatOutput("tasks") {
+ contains("generateReleaseBaselineProfile - ")
+ contains("generateFreeReleaseBaselineProfile - ")
+ contains("generatePaidReleaseBaselineProfile - ")
+ }
gradleRunner
.withArguments("generateReleaseBaselineProfile", "--stacktrace")
@@ -228,68 +145,26 @@
@Test
fun testGenerateTaskWithFlavorsAndMergeAll() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.application")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- productFlavors {
- flavorDimensions = ["version"]
- free {
- dimension "version"
- }
- paid {
- dimension "version"
- }
- }
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- baselineProfile {
- mergeIntoMain = true
- }
- """.trimIndent(),
- suffix = ""
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ dependencyOnProducerProject = true,
+ baselineProfileBlock = """
+ mergeIntoMain = true
+ """.trimIndent()
)
- producerProjectSetup.writeDefaultBuildGradle(
- prefix = MockProducerBuildGrade()
- .withConfiguration(flavor = "free", buildType = "release")
- .withConfiguration(flavor = "paid", buildType = "release")
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_1_METHOD_1,
- Fixtures.CLASS_1,
- ),
- flavor = "free",
- buildType = "release"
- )
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_2_METHOD_1,
- Fixtures.CLASS_2,
- ),
- flavor = "paid",
- buildType = "release"
- )
- .build(),
- suffix = ""
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2)
)
// Asserts that all per-variant, per-flavor and per-build type tasks are being generated.
- gradleRunner
- .withArguments("tasks", "--stacktrace")
- .build()
- .output
- .also {
- assertThat(it).contains("generateBaselineProfile - ")
- assertThat(it).contains("generateReleaseBaselineProfile - ")
- assertThat(it).doesNotContain("generateFreeReleaseBaselineProfile - ")
- assertThat(it).doesNotContain("generatePaidReleaseBaselineProfile - ")
- }
+ gradleRunner.buildAndAssertThatOutput("tasks") {
+ contains("generateBaselineProfile - ")
+ contains("generateReleaseBaselineProfile - ")
+ doesNotContain("generateFreeReleaseBaselineProfile - ")
+ doesNotContain("generatePaidReleaseBaselineProfile - ")
+ }
gradleRunner
.withArguments("generateBaselineProfile", "--stacktrace")
@@ -306,80 +181,39 @@
@Test
fun testPluginAppliedToApplicationModule() {
-
- // For this test the producer is not important
- writeDefaultProducerProject()
-
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.application")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- """.trimIndent(),
- suffix = ""
+ setupProducerProject()
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ addAppTargetPlugin = false,
+ dependencyOnProducerProject = true
)
-
gradleRunner
- .withArguments("generateReleaseBaselineProfile", "--stacktrace")
+ .withArguments("generateBaselineProfile", "--stacktrace")
.build()
-
// This should not fail.
}
@Test
fun testPluginAppliedToLibraryModule() {
- // For this test the producer is not important
- writeDefaultProducerProject()
-
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.library")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- """.trimIndent(),
- suffix = ""
+ setupProducerProject()
+ setupConsumerProject(
+ androidPlugin = ANDROID_LIBRARY_PLUGIN,
+ addAppTargetPlugin = false,
+ dependencyOnProducerProject = true
)
-
gradleRunner
.withArguments("generateBaselineProfile", "--stacktrace")
.build()
-
// This should not fail.
}
@Test
fun testPluginAppliedToNonApplicationAndNonLibraryModule() {
- // For this test the producer is not important
- writeDefaultProducerProject()
-
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.test")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- """.trimIndent(),
- suffix = ""
+ setupProducerProject()
+ setupConsumerProject(
+ androidPlugin = ANDROID_TEST_PLUGIN,
+ addAppTargetPlugin = false,
+ dependencyOnProducerProject = true
)
gradleRunner
@@ -389,25 +223,14 @@
@Test
fun testSrcSetAreAddedToVariants() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.application")
- id("androidx.baselineprofile.apptarget")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- productFlavors {
- flavorDimensions = ["version"]
- free { dimension "version" }
- paid { dimension "version" }
- }
- }
- baselineProfile {
- enableR8BaselineProfileRewrite = false
- }
-
+ setupConsumerProject(
+ androidPlugin = "com.android.application",
+ flavors = true,
+ dependencyOnProducerProject = false,
+ baselineProfileBlock = """
+ enableR8BaselineProfileRewrite = false
+ """.trimIndent(),
+ additionalGradleCodeBlock = """
$GRADLE_CODE_PRINT_TASK
androidComponents {
@@ -417,8 +240,7 @@
}
}
}
- """.trimIndent(),
- suffix = ""
+ """.trimIndent()
)
arrayOf("freeRelease", "paidRelease")
@@ -428,13 +250,13 @@
// not exist so we need to create it.
val expected =
File(
- consumerProjectSetup.rootDir,
- "src/$it/$expectedBaselineProfileOutputFolder"
- )
- .apply {
- mkdirs()
- deleteOnExit()
- }
+ consumerProjectSetup.rootDir,
+ "src/$it/$expectedBaselineProfileOutputFolder"
+ )
+ .apply {
+ mkdirs()
+ deleteOnExit()
+ }
gradleRunner.buildAndAssertThatOutput("${it}Print") {
contains(expected.absolutePath)
@@ -444,24 +266,12 @@
@Test
fun testR8RewriteBaselineProfilePropertySet() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.library")
- id("androidx.baselineprofile.consumer")
- }
- android {
- namespace 'com.example.namespace'
- productFlavors {
- flavorDimensions = ["version"]
- free { dimension "version" }
- paid { dimension "version" }
- }
- buildTypes {
- anotherRelease { initWith(release) }
- }
- }
-
+ setupConsumerProject(
+ androidPlugin = "com.android.library",
+ dependencyOnProducerProject = false,
+ flavors = true,
+ buildTypeAnotherRelease = true,
+ additionalGradleCodeBlock = """
$GRADLE_CODE_PRINT_TASK
androidComponents {
@@ -478,8 +288,7 @@
}
}
}
- """.trimIndent(),
- suffix = ""
+ """.trimIndent()
)
arrayOf(
@@ -492,66 +301,29 @@
@Test
fun testFilterAndSortAndMerge() {
- consumerProjectSetup.writeDefaultBuildGradle(
- prefix = """
- plugins {
- id("com.android.library")
- id("androidx.baselineprofile.consumer")
+ setupConsumerProject(
+ androidPlugin = "com.android.library",
+ flavors = true,
+ baselineProfileBlock = """
+ filter {
+ include("com.sample.Utils")
}
- android {
- namespace 'com.example.namespace'
- productFlavors {
- flavorDimensions = ["version"]
- free {
- dimension "version"
- }
- paid {
- dimension "version"
- }
- }
- }
- dependencies {
- baselineProfile(project(":$producerModuleName"))
- }
- baselineProfile {
- filter { include("com.sample.Utils") }
- }
- """.trimIndent(),
- suffix = ""
+ """.trimIndent()
)
- producerProjectSetup.writeDefaultBuildGradle(
- prefix = MockProducerBuildGrade()
- .withConfiguration(
- flavor = "free",
- buildType = "release"
- )
- .withConfiguration(
- flavor = "paid",
- buildType = "release"
- )
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_1_METHOD_1,
- Fixtures.CLASS_1_METHOD_2,
- Fixtures.CLASS_1,
- ),
- flavor = "free",
- buildType = "release"
- )
- .withProducedBaselineProfile(
- lines = listOf(
- Fixtures.CLASS_2_METHOD_1,
- Fixtures.CLASS_2_METHOD_2,
- Fixtures.CLASS_2_METHOD_3,
- Fixtures.CLASS_2_METHOD_4,
- Fixtures.CLASS_2_METHOD_5,
- Fixtures.CLASS_2,
- ),
- flavor = "paid",
- buildType = "release"
- )
- .build(),
- suffix = ""
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(
+ Fixtures.CLASS_1_METHOD_1,
+ Fixtures.CLASS_1_METHOD_2,
+ Fixtures.CLASS_1,
+ ),
+ paidReleaseProfileLines = listOf(
+ Fixtures.CLASS_2_METHOD_1,
+ Fixtures.CLASS_2_METHOD_2,
+ Fixtures.CLASS_2_METHOD_3,
+ Fixtures.CLASS_2_METHOD_4,
+ Fixtures.CLASS_2_METHOD_5,
+ Fixtures.CLASS_2,
+ )
)
gradleRunner
@@ -570,6 +342,238 @@
Fixtures.CLASS_2_METHOD_3,
)
}
+
+ @Test
+ fun testSaveInSrcTrueAndAutomaticGenerationDuringBuildTrue() {
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ baselineProfileBlock = """
+ saveInSrc = true
+ automaticGenerationDuringBuild = true
+ """.trimIndent()
+ )
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+
+ // Asserts that assembling release triggers generation of profile
+ gradleRunner.buildAndAssertThatOutput("assembleFreeRelease", "--dry-run") {
+ contains(":$consumerModuleName:mergeFreeReleaseBaselineProfile")
+ contains(":$consumerModuleName:copyFreeReleaseBaselineProfileIntoSrc")
+ contains(":$consumerModuleName:mergeFreeReleaseArtProfile")
+ contains(":$consumerModuleName:compileFreeReleaseArtProfile")
+ contains(":$consumerModuleName:assembleFreeRelease")
+ }
+
+ // Asserts that the profile is generated in the src folder
+ gradleRunner.build("generateFreeReleaseBaselineProfile") {
+ assertThat(readBaselineProfileFileContent("freeRelease"))
+ .containsExactly(
+ Fixtures.CLASS_1,
+ Fixtures.CLASS_1_METHOD_1,
+ )
+ }
+ }
+
+ @Test
+ fun testSaveInSrcTrueAndAutomaticGenerationDuringBuildFalse() {
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ baselineProfileBlock = """
+ saveInSrc = true
+ automaticGenerationDuringBuild = false
+ """.trimIndent()
+ )
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+
+ // Asserts that assembling release does not trigger generation of profile
+ gradleRunner.buildAndAssertThatOutput("assembleFreeRelease", "--dry-run") {
+ doesNotContain(":$consumerModuleName:mergeFreeReleaseBaselineProfile")
+ doesNotContain(":$consumerModuleName:copyFreeReleaseBaselineProfileIntoSrc")
+ contains(":$consumerModuleName:mergeFreeReleaseArtProfile")
+ contains(":$consumerModuleName:compileFreeReleaseArtProfile")
+ contains(":$consumerModuleName:assembleFreeRelease")
+ }
+
+ // Asserts that the profile is generated in the src folder
+ gradleRunner.build("generateFreeReleaseBaselineProfile") {
+ assertThat(readBaselineProfileFileContent("freeRelease"))
+ .containsExactly(
+ Fixtures.CLASS_1,
+ Fixtures.CLASS_1_METHOD_1,
+ )
+ }
+ }
+
+ @Test
+ fun testSaveInSrcFalseAndAutomaticGenerationDuringBuildTrue() {
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ flavors = true,
+ baselineProfileBlock = """
+ saveInSrc = false
+ automaticGenerationDuringBuild = true
+ """.trimIndent()
+ )
+ setupProducerProjectWithFlavors(
+ freeReleaseProfileLines = listOf(Fixtures.CLASS_1_METHOD_1, Fixtures.CLASS_1),
+ paidReleaseProfileLines = listOf(Fixtures.CLASS_2_METHOD_1, Fixtures.CLASS_2),
+ )
+
+ // Asserts that assembling release triggers generation of profile
+ gradleRunner.buildAndAssertThatOutput("assembleFreeRelease", "--dry-run") {
+ contains(":$consumerModuleName:mergeFreeReleaseBaselineProfile")
+ contains(":$consumerModuleName:mergeFreeReleaseArtProfile")
+ contains(":$consumerModuleName:compileFreeReleaseArtProfile")
+ contains(":$consumerModuleName:assembleFreeRelease")
+ }
+
+ // Asserts that the profile is not generated in the src folder
+ gradleRunner.build("generateFreeReleaseBaselineProfile") {}
+
+ val profileFile = baselineProfileFile("freeRelease")
+ assertThat(profileFile.exists()).isFalse()
+ }
+
+ @Test
+ fun testSaveInSrcFalseAndAutomaticGenerationDuringBuildFalse() {
+ setupProducerProject()
+ setupConsumerProject(
+ androidPlugin = ANDROID_APPLICATION_PLUGIN,
+ baselineProfileBlock = """
+ saveInSrc = false
+ automaticGenerationDuringBuild = false
+ """.trimIndent()
+ )
+ gradleRunner
+ .withArguments("generateReleaseBaselineProfile", "--stacktrace")
+ .buildAndFail()
+ .output
+ .replace(System.lineSeparator(), " ")
+ .also {
+ assertThat(it)
+ .contains(
+ "The current configuration of flags `saveInSrc` and " +
+ "`automaticGenerationDuringBuild` is not supported"
+ )
+ }
+ }
+
+ private fun setupConsumerProject(
+ androidPlugin: String,
+ flavors: Boolean = false,
+ dependencyOnProducerProject: Boolean = true,
+ buildTypeAnotherRelease: Boolean = false,
+ addAppTargetPlugin: Boolean = androidPlugin == ANDROID_APPLICATION_PLUGIN,
+ baselineProfileBlock: String = "",
+ additionalGradleCodeBlock: String = "",
+ ) {
+ val flavorsBlock = """
+ productFlavors {
+ flavorDimensions = ["version"]
+ free { dimension "version" }
+ paid { dimension "version" }
+ }
+
+ """.trimIndent()
+
+ val buildTypeAnotherReleaseBlock = """
+ buildTypes {
+ anotherRelease { initWith(release) }
+ }
+
+ """.trimIndent()
+
+ val dependencyOnProducerProjectBlock = """
+ dependencies {
+ baselineProfile(project(":$producerModuleName"))
+ }
+
+ """.trimIndent()
+
+ consumerProjectSetup.writeDefaultBuildGradle(
+ prefix = """
+ plugins {
+ id("$androidPlugin")
+ id("androidx.baselineprofile.consumer")
+ ${if (addAppTargetPlugin) "id(\"androidx.baselineprofile.apptarget\")" else ""}
+ }
+ android {
+ namespace 'com.example.namespace'
+ ${if (flavors) flavorsBlock else ""}
+ ${if (buildTypeAnotherRelease) buildTypeAnotherReleaseBlock else ""}
+ }
+
+ ${if (dependencyOnProducerProject) dependencyOnProducerProjectBlock else ""}
+
+ baselineProfile {
+ $baselineProfileBlock
+ }
+
+ $additionalGradleCodeBlock
+
+ """.trimIndent(),
+ suffix = ""
+ )
+ }
+
+ private fun setupProducerProjectWithFlavors(
+ freeReleaseProfileLines: List<String>,
+ paidReleaseProfileLines: List<String>,
+ ) {
+ producerProjectSetup.writeDefaultBuildGradle(
+ prefix = MockProducerBuildGrade()
+ .withConfiguration(flavor = "free", buildType = "release")
+ .withConfiguration(flavor = "paid", buildType = "release")
+ .withProducedBaselineProfile(
+ lines = freeReleaseProfileLines,
+ flavor = "free",
+ buildType = "release"
+ )
+ .withProducedBaselineProfile(
+ lines = paidReleaseProfileLines,
+ flavor = "paid",
+ buildType = "release"
+ )
+ .build(),
+ suffix = ""
+ )
+ }
+
+ private fun setupProducerProject(
+ releaseProfile: List<String> = listOf(
+ Fixtures.CLASS_1_METHOD_1,
+ Fixtures.CLASS_2_METHOD_2,
+ Fixtures.CLASS_2,
+ Fixtures.CLASS_1
+ ),
+ vararg additionalReleaseProfiles: List<String>
+ ) {
+ val mock = MockProducerBuildGrade()
+ .withConfiguration(flavor = "", buildType = "release")
+ .withProducedBaselineProfile(
+ lines = releaseProfile,
+ flavor = "",
+ buildType = "release"
+ )
+ for (profile in additionalReleaseProfiles) {
+ mock.withProducedBaselineProfile(
+ lines = profile,
+ flavor = "",
+ buildType = "release"
+ )
+ }
+ producerProjectSetup.writeDefaultBuildGradle(
+ prefix = mock.build(),
+ suffix = ""
+ )
+ }
}
private class MockProducerBuildGrade {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
index 2d688ff..6e63906 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/TestUtils.kt
@@ -28,17 +28,17 @@
"""
-internal fun GradleRunner.build(taskName: String, block: (String) -> (Unit)) {
+internal fun GradleRunner.build(vararg arguments: String, block: (String) -> (Unit)) {
this
- .withArguments(taskName, "--stacktrace")
+ .withArguments(*arguments, "--stacktrace")
.build()
.output
.let(block)
}
internal fun GradleRunner.buildAndAssertThatOutput(
- taskName: String,
+ vararg arguments: String,
assertBlock: StringSubject.() -> (Unit)
) {
- this.build(taskName) { assertBlock(assertThat(it)) }
+ this.build(*arguments) { assertBlock(assertThat(it)) }
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index e8cd413..7a16cf9 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import androidx.build.LibraryType
-import androidx.build.SdkResourceGenerator
+import androidx.build.*
plugins {
id("AndroidXPlugin")
@@ -46,6 +45,7 @@
androidx {
name = "AndroidX Benchmarks - Darwin Gradle Plugin"
+ publish = Publish.SNAPSHOT_ONLY
type = LibraryType.GRADLE_PLUGIN
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Gradle Plugin"
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index ce57b42..25e0f54 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -155,7 +155,10 @@
// We want to write metrics to library metrics specific location
// Context: b/257326666
return providers.environmentVariable(DIST_DIR).map { value ->
- File(value, LIBRARY_METRICS)
+ val parent = value.ifBlank {
+ project.buildDir.absolutePath
+ }
+ File(parent, LIBRARY_METRICS)
}
}
diff --git a/benchmark/integration-tests/baselineprofile-consumer/build.gradle b/benchmark/integration-tests/baselineprofile-consumer/build.gradle
index 4000139..ffbb3ed 100644
--- a/benchmark/integration-tests/baselineprofile-consumer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-consumer/build.gradle
@@ -39,7 +39,6 @@
}
baselineProfile {
- onDemandGeneration = false
filter {
include "androidx.benchmark.integration.baselineprofile.consumer.**"
}
diff --git a/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle b/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
index b647e4b..e6ca785 100644
--- a/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-flavors-consumer/build.gradle
@@ -53,7 +53,6 @@
}
baselineProfile {
- onDemandGeneration = false
filter {
include "androidx.benchmark.integration.baselineprofile.flavors.consumer.*"
}
diff --git a/benchmark/integration-tests/baselineprofile-library-consumer/build.gradle b/benchmark/integration-tests/baselineprofile-library-consumer/build.gradle
index e91eeca..2bb5cb7 100644
--- a/benchmark/integration-tests/baselineprofile-library-consumer/build.gradle
+++ b/benchmark/integration-tests/baselineprofile-library-consumer/build.gradle
@@ -31,7 +31,6 @@
}
baselineProfile {
- onDemandGeneration = false
filter {
include "androidx.benchmark.integration.baselineprofile.library.consumer.**"
exclude "androidx.benchmark.integration.baselineprofile.library.consumer.exclude.*"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index cd96eac..1e981d4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -123,15 +123,21 @@
chooseProjectVersion()
}
- fun getAllProjectPathsInSameGroup(): List<String> {
- val allProjectPaths = listProjectsService.get().allPossibleProjectPaths
+ internal var projectDirectlySpecifiesMavenVersion: Boolean = false
+
+ fun getOtherProjectsInSameGroup(): List<SettingsParser.IncludedProject> {
+ val allProjects = listProjectsService.get().allPossibleProjects
val ourGroup = chooseLibraryGroup()
if (ourGroup == null)
- return listOf(project.path)
- val projectPathsInSameGroup = allProjectPaths.filter { otherPath ->
- getLibraryGroupFromProjectPath(otherPath) == ourGroup
+ return listOf()
+ val otherProjectsInSameGroup = allProjects.filter { otherProject ->
+ if (otherProject.gradlePath == project.path) {
+ false
+ } else {
+ getLibraryGroupFromProjectPath(otherProject.gradlePath) == ourGroup
+ }
}
- return projectPathsInSameGroup
+ return otherProjectsInSameGroup
}
/**
@@ -222,6 +228,7 @@
val groupVersion: Version? = mavenGroup?.atomicGroupVersion
val mavenVersion: Version? = mavenVersion
if (mavenVersion != null) {
+ projectDirectlySpecifiesMavenVersion = true
if (groupVersion != null && !isGroupVersionOverrideAllowed()) {
throw GradleException(
"Cannot set mavenVersion (" + mavenVersion +
@@ -234,6 +241,7 @@
version = mavenVersion
}
} else {
+ projectDirectlySpecifiesMavenVersion = false
if (groupVersion != null) {
verifyVersionExtraFormat(groupVersion)
version = groupVersion
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 53b3ae3..05930af 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -155,6 +155,7 @@
it.configureWithAndroidXExtension(extension)
}
project.configureConstraintsWithinGroup(extension)
+ project.validateProjectParser(extension)
}
private fun Project.registerProjectOrArtifact() {
@@ -900,13 +901,15 @@
configuration.extendsFrom(constraintConfiguration)
}
- val otherProjectPathsInSameGroup = extension.getAllProjectPathsInSameGroup()
+ val otherProjectsInSameGroup = extension.getOtherProjectsInSameGroup()
val constraints = project.dependencies.constraints
val allProjectsExist = buildContainsAllStandardProjects()
- for (otherPath in otherProjectPathsInSameGroup) {
- // don't need a constraint pointing at self
- if (otherPath == project.path)
+ for (otherProject in otherProjectsInSameGroup) {
+ val otherGradlePath = otherProject.gradlePath
+ if (otherGradlePath == ":compose:ui:ui-android-stubs") {
+ // exemption for library that doesn't truly get published: b/168127161
continue
+ }
// We only enable constraints for builds that we intend to be able to publish from.
// If a project isn't included in a build we intend to be able to publish from,
// the project isn't going to be published.
@@ -914,14 +917,34 @@
// The KMP project subset enabled by androidx_multiplatform_mac.sh contains
// :benchmark:benchmark-common but not :benchmark:benchmark-benchmark
// This is ok because we don't intend to publish that artifact from that build
- val otherProjectShouldExist = allProjectsExist || findProject(otherPath) != null
- if (otherProjectShouldExist) {
- val dependencyConstraint = project(otherPath)
- constraints.add(
- constraintConfiguration.name,
- dependencyConstraint
- )
+ val otherProjectShouldExist =
+ allProjectsExist || findProject(otherGradlePath) != null
+ if (!otherProjectShouldExist) {
+ continue
}
+ // We only emit constraints referring to projects that will release
+ val otherFilepath = File(otherProject.filePath, "build.gradle")
+ val parsed = parseBuildFile(otherFilepath)
+ if (!parsed.shouldRelease()) {
+ continue
+ }
+ if (parsed.libraryType == LibraryType.SAMPLES) {
+ // a SAMPLES project knows how to publish, but we don't intend to actually
+ // publish it
+ continue
+ }
+ // Under certain circumstances, a project is allowed to override its
+ // version see ( isGroupVersionOverrideAllowed ), in which case it's
+ // not participating in the versioning policy yet and we don't emit
+ // version constraints referencing it
+ if (parsed.specifiesVersion) {
+ continue
+ }
+ val dependencyConstraint = project(otherGradlePath)
+ constraints.add(
+ constraintConfiguration.name,
+ dependencyConstraint
+ )
}
}
}
@@ -1157,6 +1180,35 @@
}
/**
+ * Verifies that ProjectParser computes the correct values for this project
+ */
+fun Project.validateProjectParser(extension: AndroidXExtension) {
+ project.afterEvaluate {
+ val parsed = project.parse()
+ check(extension.type == parsed.libraryType) {
+ "ProjectParser incorrectly computed libraryType = ${parsed.libraryType} " +
+ "instead of ${extension.type}"
+ }
+ check(extension.publish == parsed.publish) {
+ "ProjectParser incorrectly computed publish = ${parsed.publish} " +
+ "instead of ${extension.publish}"
+ }
+ check(extension.shouldPublish() == parsed.shouldPublish()) {
+ "ProjectParser incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " +
+ "instead of ${extension.shouldPublish()}"
+ }
+ check(extension.shouldRelease() == parsed.shouldRelease()) {
+ "ProjectParser incorrectly computed shouldRelease() = ${parsed.shouldRelease()} " +
+ "instead of ${extension.shouldRelease()}"
+ }
+ check(extension.projectDirectlySpecifiesMavenVersion == parsed.specifiesVersion) {
+ "ProjectParser incorrectly computed specifiesVersion = ${parsed.specifiesVersion}" +
+ "instead of ${extension.projectDirectlySpecifiesMavenVersion}"
+ }
+ }
+}
+
+/**
* Validates the Maven version against Jetpack guidelines.
*/
fun AndroidXExtension.validateMavenVersion() {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt b/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt
index e63c5a3..d84efcb6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt
@@ -32,9 +32,7 @@
// Note that this might be more than the full list of projects configured in this build:
// a) Configuration-on-demand can disable projects mentioned in settings.gradle
// B) Playground builds use their own settings.gradle files
- val allPossibleProjectPaths: List<String> by lazy {
- val allProjects = SettingsParser.findProjects(parameters.settingsFile.get())
- val projectPaths = allProjects.map { project -> project.gradlePath }
- projectPaths
+ val allPossibleProjects: List<SettingsParser.IncludedProject> by lazy {
+ SettingsParser.findProjects(parameters.settingsFile.get())
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
new file mode 100644
index 0000000..f12cbd1
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import java.util.concurrent.ConcurrentHashMap
+import org.gradle.api.Project
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+
+import java.io.File
+
+abstract class ProjectParser : BuildService<BuildServiceParameters.None> {
+ @Transient
+ val cache: MutableMap<File, ParsedProject> = ConcurrentHashMap()
+
+ fun get(buildFile: File): ParsedProject {
+ return cache.getOrPut(
+ key = buildFile
+ ) {
+ val text = buildFile.readLines()
+ parseProject(text)
+ }
+ }
+
+ private fun parseProject(fileLines: List<String>): ParsedProject {
+ var libraryType: String? = null
+ var publish: String? = null
+ var specifiesVersion: Boolean = false
+ fileLines.forEach { line ->
+ if (libraryType == null)
+ libraryType = line.extractVariableValue(" type = LibraryType.")
+ if (publish == null)
+ publish = line.extractVariableValue(" publish = Publish.")
+ if (line.contains("mavenVersion ="))
+ specifiesVersion = true
+ }
+ val libraryTypeEnum = libraryType?.let { LibraryType.valueOf(it) } ?: LibraryType.UNSET
+ val publishEnum = publish?.let { Publish.valueOf(it) } ?: Publish.UNSET
+ return ParsedProject(
+ libraryType = libraryTypeEnum,
+ publish = publishEnum,
+ specifiesVersion = specifiesVersion
+ )
+ }
+
+ data class ParsedProject(
+ val libraryType: LibraryType,
+ val publish: Publish,
+ val specifiesVersion: Boolean
+ ) {
+ fun shouldPublish(): Boolean =
+ if (publish != Publish.UNSET) {
+ publish.shouldPublish()
+ } else if (libraryType != LibraryType.UNSET) {
+ libraryType.publish.shouldPublish()
+ } else {
+ false
+ }
+
+ fun shouldRelease(): Boolean =
+ if (publish != Publish.UNSET) {
+ publish.shouldRelease()
+ } else if (libraryType != LibraryType.UNSET) {
+ libraryType.publish.shouldRelease()
+ } else {
+ false
+ }
+ }
+}
+
+private fun String.extractVariableValue(prefix: String): String? {
+ val declarationIndex = this.indexOf(prefix)
+ if (declarationIndex >= 0) {
+ val suffix = this.substring(declarationIndex + prefix.length)
+ val spaceIndex = suffix.indexOf(" ")
+ if (spaceIndex > 0)
+ return suffix.substring(0, spaceIndex)
+ return suffix
+ }
+ return null
+}
+
+fun Project.parse(): ProjectParser.ParsedProject {
+ return parseBuildFile(project.buildFile)
+}
+
+fun Project.parseBuildFile(buildFile: File): ProjectParser.ParsedProject {
+ if (buildFile.path.contains("compose/material/material-icons-extended-")) {
+ // These projects all read from this Gradle script
+ return parseBuildFile(
+ File(buildFile.parentFile.parentFile, "material-icons-extended/generate.gradle")
+ )
+ }
+ val parserProvider = project.rootProject.gradle.sharedServices.registerIfAbsent(
+ "ProjectParser",
+ ProjectParser::class.java
+ ) {
+ }
+ val parser = parserProvider.get()
+ return parser.get(buildFile)
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 52c0b20..d6a880e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -121,6 +121,7 @@
"generateProjectStructureMetadata",
// https://github.com/google/protobuf-gradle-plugin/issues/667
+ ":appactions:interaction:interaction-service-proto:extractIncludeTestProto",
":datastore:datastore-preferences-proto:extractIncludeTestProto",
":glance:glance-appwidget-proto:extractIncludeTestProto",
":health:connect:connect-client-proto:extractIncludeTestProto",
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index 5a4424f..b6c5c29 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -82,6 +82,32 @@
val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
val IDE_PLUGIN = IdePlugin()
val UNSET = Unset()
+
+ private val allTypes = mapOf(
+ "PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
+ "PUBLISHED_TEST_LIBRARY" to PUBLISHED_TEST_LIBRARY,
+ "PUBLISHED_NATIVE_LIBRARY" to PUBLISHED_NATIVE_LIBRARY,
+ "INTERNAL_TEST_LIBRARY" to INTERNAL_TEST_LIBRARY,
+ "INTERNAL_HOST_TEST_LIBRARY" to INTERNAL_HOST_TEST_LIBRARY,
+ "SAMPLES" to SAMPLES,
+ "LINT" to LINT,
+ "COMPILER_DAEMON" to COMPILER_DAEMON,
+ "COMPILER_DAEMON_TEST" to COMPILER_DAEMON_TEST,
+ "COMPILER_PLUGIN" to COMPILER_PLUGIN,
+ "GRADLE_PLUGIN" to GRADLE_PLUGIN,
+ "ANNOTATION_PROCESSOR" to ANNOTATION_PROCESSOR,
+ "ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
+ "OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
+ "IDE_PLUGIN" to IDE_PLUGIN,
+ "UNSET" to UNSET
+ )
+ fun valueOf(name: String): LibraryType {
+ val result = allTypes[name]
+ check(result != null) {
+ "LibraryType with name $name not found"
+ }
+ return result
+ }
}
open class PublishedLibrary(allowCallingVisibleForTestsApis: Boolean = false) : LibraryType(
publish = Publish.SNAPSHOT_AND_RELEASE,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index e854e47..f8bb7fb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -32,6 +32,7 @@
import androidx.camera.camera2.pipe.integration.internal.CameraSelectionOptimizer
import androidx.camera.core.CameraSelector
import androidx.camera.core.concurrent.CameraCoordinator
+import androidx.camera.core.concurrent.CameraCoordinator.ConcurrentCameraModeListener
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraThreadConfig
@@ -116,10 +117,10 @@
override fun setCameraOperatingMode(cameraOperatingMode: Int) {
}
- override fun addListener(listener: CameraCoordinator.ConcurrentCameraModeListener) {
+ override fun addListener(listener: ConcurrentCameraModeListener) {
}
- override fun removeListener(listener: CameraCoordinator.ConcurrentCameraModeListener) {
+ override fun removeListener(listener: ConcurrentCameraModeListener) {
}
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index bef9275..ad5975f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -134,8 +134,9 @@
* @param cameraId the camera id of the camera device used by the use cases
* @param existingSurfaces list of surfaces already configured and used by the camera. The
* resolutions for these surface can not change.
- * @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested stream specification
+ * @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
+ * supported sizes list that will be given a
+ * suggested stream specification
* @return map of suggested stream specifications for given use cases
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
* there isn't a supported combination of surfaces
@@ -146,7 +147,7 @@
isConcurrentCameraModeOn: Boolean,
cameraId: String,
existingSurfaces: List<AttachedSurfaceInfo>,
- newUseCaseConfigs: List<UseCaseConfig<*>>
+ newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
): Map<UseCaseConfig<*>, StreamSpec> {
if (!checkIfSupportedCombinationExist(cameraId)) {
@@ -158,7 +159,7 @@
return supportedSurfaceCombinationMap[cameraId]!!.getSuggestedStreamSpecifications(
isConcurrentCameraModeOn,
existingSurfaces,
- newUseCaseConfigs
+ newUseCaseConfigsSupportedSizeMap
)
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index 2c23b2f..6733cc7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -26,33 +26,22 @@
import android.media.CamcorderProfile
import android.media.MediaRecorder
import android.os.Build
-import android.util.Rational
import android.util.Size
import android.view.Display
-import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.core.AspectRatio
-import androidx.camera.core.Logger
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.ImageFormatConstants
-import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.StreamSpec
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceSizeDefinition
import androidx.camera.core.impl.UseCaseConfig
-import androidx.camera.core.impl.utils.AspectRatioUtil
-import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
-import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
-import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.CompareSizesByArea
-import androidx.camera.core.internal.utils.SizeUtil
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
-import androidx.core.util.Preconditions
import java.util.Arrays
import java.util.Collections
@@ -77,7 +66,6 @@
private val hardwareLevel =
cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
?: CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
- private val isSensorLandscapeResolution = isSensorLandscapeResolution(cameraMetadata)
private val concurrentSurfaceCombinations: MutableList<SurfaceCombination> = ArrayList()
private val surfaceCombinations: MutableList<SurfaceCombination> = ArrayList()
private val outputSizesCache: MutableMap<Int, Array<Size>> = HashMap()
@@ -86,8 +74,6 @@
internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
private val displayManager: DisplayManager =
(context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
- private val activeArraySize =
- cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]
init {
checkCapabilities()
@@ -145,7 +131,8 @@
*
* @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
* @param existingSurfaces the existing surfaces.
- * @param newUseCaseConfigs newly added UseCaseConfig.
+ * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output sizes
+ * map.
* @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
* stream specification.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
@@ -154,13 +141,14 @@
fun getSuggestedStreamSpecifications(
isConcurrentCameraModeOn: Boolean,
existingSurfaces: List<AttachedSurfaceInfo>,
- newUseCaseConfigs: List<UseCaseConfig<*>>
+ newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
): Map<UseCaseConfig<*>, StreamSpec> {
refreshPreviewSize()
val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
for (scc in existingSurfaces) {
surfaceConfigs.add(scc.surfaceConfig)
}
+ val newUseCaseConfigs = newUseCaseConfigsSupportedSizeMap.keys.toList()
// Use the small size (640x480) for new use cases to check whether there is any possible
// supported combination first
for (useCaseConfig in newUseCaseConfigs) {
@@ -192,9 +180,8 @@
// Collect supported output sizes for all use cases
for (index in useCasesPriorityOrder) {
- val supportedOutputSizes: List<Size> = getSupportedOutputSizes(
- newUseCaseConfigs[index]
- )
+ val supportedOutputSizes: List<Size> =
+ newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
supportedOutputSizesList.add(supportedOutputSizes)
}
// Get all possible size arrangements
@@ -405,17 +392,6 @@
}
/**
- * Check if the size obtained from sensor info indicates landscape mode.
- */
- private fun isSensorLandscapeResolution(cameraMetadata: CameraMetadata): Boolean {
- val pixelArraySize: Size? =
- cameraMetadata.get<Size>(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)
-
- // Make the default value is true since usually the sensor resolution is landscape.
- return if (pixelArraySize != null) pixelArraySize.width >= pixelArraySize.height else true
- }
-
- /**
* Calculates the size for preview. If the max size is larger than 1080p, use 1080p.
*/
@SuppressWarnings("deprecation")
@@ -546,389 +522,6 @@
}
/**
- * Retrieves the sorted customized supported resolutions from the given config
- */
- private fun getCustomizedSupportSizesFromConfig(
- imageFormat: Int,
- config: ImageOutputConfig
- ): Array<Size>? {
- var outputSizes: Array<Size>? = null
-
- // Try to retrieve customized supported resolutions from config.
- val formatResolutionsPairList = config.getSupportedResolutions(null)
- if (formatResolutionsPairList != null) {
- for (formatResolutionPair in formatResolutionsPairList) {
- if (formatResolutionPair.first == imageFormat) {
- outputSizes = formatResolutionPair.second
- break
- }
- }
- }
- if (outputSizes != null) {
- // TODO(b/244477758): Exclude problematic sizes
-
- // Sort the output sizes. The Comparator result must be reversed to have a descending
- // order result.
- Arrays.sort(outputSizes, CompareSizesByArea(true))
- }
- return outputSizes
- }
-
- /**
- * Flips the size if rotation is needed.
- */
- private fun flipSizeByRotation(size: Size?, targetRotation: Int): Size? {
- var outputSize = size
- // Calibrates the size with the display and sensor rotation degrees values.
- if (size != null && isRotationNeeded(targetRotation)) {
- outputSize = Size(/* width= */size.height, /* height= */size.width)
- }
- return outputSize
- }
-
- /**
- * Determines whether rotation needs to be done on target rotation.
- */
- private fun isRotationNeeded(targetRotation: Int): Boolean {
- val sensorOrientation: Int? =
- cameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION]
- Preconditions.checkNotNull(
- sensorOrientation, "Camera HAL in bad state, unable to " +
- "retrieve the SENSOR_ORIENTATION"
- )
- val relativeRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(targetRotation)
-
- // Currently this assumes that a back-facing camera is always opposite to the screen.
- // This may not be the case for all devices, so in the future we may need to handle that
- // scenario.
- val lensFacing: Int? = cameraMetadata[CameraCharacteristics.LENS_FACING]
- Preconditions.checkNotNull(
- lensFacing, "Camera HAL in bad state, unable to retrieve the " +
- "LENS_FACING"
- )
- val isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing
- val sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
- relativeRotationDegrees,
- sensorOrientation!!,
- isOppositeFacingScreen
- )
- return sensorRotationDegrees == 90 || sensorRotationDegrees == 270
- }
-
- /**
- * Obtains the target size from ImageOutputConfig.
- */
- private fun getTargetSize(imageOutputConfig: ImageOutputConfig): Size? {
- val targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0)
- // Calibrate targetSize by the target rotation value.
- var targetSize = imageOutputConfig.getTargetResolution(null)
- targetSize = flipSizeByRotation(targetSize, targetRotation)
- return targetSize
- }
-
- /**
- * Returns the aspect ratio group key of the target size when grouping the input resolution
- * candidate list.
- *
- * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
- * also need to consider the mod 16 factor to find which aspect ratio of group the target size
- * might be put in. So that sizes of the group will be selected to use in the highest priority.
- */
- private fun getAspectRatioGroupKeyOfTargetSize(
- targetSize: Size?,
- resolutionCandidateList: List<Size>
- ): Rational? {
- if (targetSize == null) {
- return null
- }
-
- val aspectRatios = getResolutionListGroupingAspectRatioKeys(
- resolutionCandidateList
- )
- aspectRatios.forEach {
- if (hasMatchingAspectRatio(targetSize, it)) {
- return it
- }
- }
- return Rational(targetSize.width, targetSize.height)
- }
-
- /**
- * Returns the grouping aspect ratio keys of the input resolution list.
- *
- * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
- * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
- */
- private fun getResolutionListGroupingAspectRatioKeys(
- resolutionCandidateList: List<Size>
- ): List<Rational> {
- val aspectRatios: MutableList<Rational> = mutableListOf()
-
- // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
- // additional items.
- aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_4_3)
- aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_16_9)
-
- // Tries to find the aspect ratio which the target size belongs to.
- resolutionCandidateList.forEach { size ->
- val newRatio = Rational(size.width, size.height)
- var aspectRatioFound = aspectRatios.contains(newRatio)
-
- // The checking size might be a mod16 size which can be mapped to an existing aspect
- // ratio group.
- if (!aspectRatioFound) {
- var hasMatchingAspectRatio = false
- aspectRatios.forEach loop@{ aspectRatio ->
- if (hasMatchingAspectRatio(size, aspectRatio)) {
- hasMatchingAspectRatio = true
- return@loop
- }
- }
- if (!hasMatchingAspectRatio) {
- aspectRatios.add(newRatio)
- }
- }
- }
- return aspectRatios
- }
-
- /**
- * Returns the target aspect ratio value corrected by quirks.
- *
- * The final aspect ratio is determined by the following order:
- * 1. The aspect ratio returned by TargetAspectRatio quirk (not implemented yet).
- * 2. The use case's original aspect ratio if TargetAspectRatio quirk returns RATIO_ORIGINAL
- * and the use case has target aspect ratio setting.
- * 3. The aspect ratio of use case's target size setting if TargetAspectRatio quirk returns
- * RATIO_ORIGINAL and the use case has no target aspect ratio but has target size setting.
- *
- * @param imageOutputConfig the image output config of the use case.
- * @param resolutionCandidateList the resolution candidate list which will be used to
- * determine the aspect ratio by target size when target
- * aspect ratio setting is not set.
- */
- private fun getTargetAspectRatio(
- imageOutputConfig: ImageOutputConfig,
- resolutionCandidateList: List<Size>
- ): Rational? {
- var outputRatio: Rational? = null
- // TODO(b/245622117) Get the corrected aspect ratio from quirks instead of always using
- // TargetAspectRatio.RATIO_ORIGINAL
- if (imageOutputConfig.hasTargetAspectRatio()) {
- when (@AspectRatio.Ratio val aspectRatio = imageOutputConfig.targetAspectRatio) {
- AspectRatio.RATIO_4_3 -> outputRatio =
- if (isSensorLandscapeResolution) AspectRatioUtil.ASPECT_RATIO_4_3
- else AspectRatioUtil.ASPECT_RATIO_3_4
- AspectRatio.RATIO_16_9 -> outputRatio =
- if (isSensorLandscapeResolution) AspectRatioUtil.ASPECT_RATIO_16_9
- else AspectRatioUtil.ASPECT_RATIO_9_16
- AspectRatio.RATIO_DEFAULT -> Unit
- else -> Logger.e(
- TAG,
- "Undefined target aspect ratio: $aspectRatio"
- )
- }
- } else {
- // The legacy resolution API will use the aspect ratio of the target size to
- // be the fallback target aspect ratio value when the use case has no target
- // aspect ratio setting.
- val targetSize = getTargetSize(imageOutputConfig)
- if (targetSize != null) {
- outputRatio = getAspectRatioGroupKeyOfTargetSize(
- targetSize,
- resolutionCandidateList
- )
- }
- }
- return outputRatio
- }
-
- /**
- * Removes unnecessary sizes by target size.
- *
- *
- * If the target resolution is set, a size that is equal to or closest to the target
- * resolution will be selected. If the list includes more than one size equal to or larger
- * than the target resolution, only one closest size needs to be kept. The other larger sizes
- * can be removed so that they won't be selected to use.
- *
- * @param supportedSizesList The list should have been sorted in descending order.
- * @param targetSize The target size used to remove unnecessary sizes.
- */
- private fun removeSupportedSizesByTargetSize(
- supportedSizesList: MutableList<Size>?,
- targetSize: Size
- ) {
- if (supportedSizesList == null || supportedSizesList.isEmpty()) {
- return
- }
- var indexBigEnough = -1
- val removeSizes: MutableList<Size> = ArrayList()
-
- // Get the index of the item that is equal to or closest to the target size.
- for (i in supportedSizesList.indices) {
- val outputSize = supportedSizesList[i]
- if (outputSize.width >= targetSize.width && outputSize.height >= targetSize.height) {
- // New big enough item closer to the target size is found. Adding the previous
- // one into the sizes list that will be removed.
- if (indexBigEnough >= 0) {
- removeSizes.add(supportedSizesList[indexBigEnough])
- }
- indexBigEnough = i
- } else {
- break
- }
- }
- // Remove the unnecessary items that are larger than the item closest to the target size.
- supportedSizesList.removeAll(removeSizes)
- }
-
- /**
- * Groups sizes together according to their aspect ratios.
- */
- private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
- val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = mutableMapOf()
-
- val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
-
- aspectRatioKeys.forEach {
- aspectRatioSizeListMap[it] = mutableListOf()
- }
-
- sizes.forEach { size ->
- aspectRatioSizeListMap.keys.forEach { aspectRatio ->
- // Put the size into all groups that is matched in mod16 condition since a size
- // may match multiple aspect ratio in mod16 algorithm.
- if (hasMatchingAspectRatio(size, aspectRatio)) {
- aspectRatioSizeListMap[aspectRatio]?.add(size)
- }
- }
- }
- return aspectRatioSizeListMap
- }
-
- /**
- * Obtains the supported sizes for a given user case.
- */
- internal fun getSupportedOutputSizes(config: UseCaseConfig<*>): List<Size> {
- val imageFormat = config.inputFormat
- val imageOutputConfig = config as ImageOutputConfig
- val customOrderedResolutions = imageOutputConfig.getCustomOrderedResolutions(null)
- if (customOrderedResolutions != null) {
- return customOrderedResolutions
- }
- var outputSizes: Array<Size>? =
- getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig)
- if (outputSizes == null) {
- outputSizes = getAllOutputSizesByFormat(imageFormat)
- }
- val outputSizeCandidates: MutableList<Size> = ArrayList()
- var maxSize = imageOutputConfig.getMaxResolution(null)
- val maxOutputSizeByFormat: Size = getMaxOutputSizeByFormat(imageFormat)
-
- // Set maxSize as the max resolution setting or the max supported output size for the
- // image format, whichever is smaller.
- if (maxSize == null ||
- SizeUtil.getArea(maxOutputSizeByFormat) < SizeUtil.getArea(maxSize)
- ) {
- maxSize = maxOutputSizeByFormat
- }
-
- // Sort the output sizes. The Comparator result must be reversed to have a descending order
- // result.
- Arrays.sort(outputSizes, CompareSizesByArea(true))
- var targetSize: Size? = getTargetSize(imageOutputConfig)
- var minSize = RESOLUTION_VGA
- val defaultSizeArea = SizeUtil.getArea(RESOLUTION_VGA)
- val maxSizeArea = SizeUtil.getArea(maxSize)
- // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
- // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
- // TARGET_RESOLUTION).
- if (maxSizeArea < defaultSizeArea) {
- minSize = SizeUtil.RESOLUTION_ZERO
- } else if (targetSize != null && SizeUtil.getArea(targetSize) < defaultSizeArea) {
- minSize = targetSize
- }
-
- // Filter out the ones that exceed the maximum size and the minimum size. The output
- // sizes candidates list won't have duplicated items.
- for (outputSize: Size in outputSizes) {
- if (SizeUtil.getArea(outputSize) <= SizeUtil.getArea(maxSize) &&
- SizeUtil.getArea(outputSize) >= SizeUtil.getArea(minSize!!) &&
- !outputSizeCandidates.contains(outputSize)
- ) {
- outputSizeCandidates.add(outputSize)
- }
- }
- if (outputSizeCandidates.isEmpty()) {
- throw java.lang.IllegalArgumentException(
- "Can not get supported output size under supported maximum for the format: " +
- imageFormat
- )
- }
-
- val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates)
-
- // Check the default resolution if the target resolution is not set
- targetSize = targetSize ?: imageOutputConfig.getDefaultResolution(null)
- var supportedResolutions: MutableList<Size> = ArrayList()
- var aspectRatioSizeListMap: Map<Rational, MutableList<Size>>
- if (aspectRatio == null) {
- // If no target aspect ratio is set, all sizes can be added to the result list
- // directly. No need to sort again since the source list has been sorted previously.
- supportedResolutions.addAll(outputSizeCandidates)
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- targetSize?.let { removeSupportedSizesByTargetSize(supportedResolutions, it) }
- } else {
- // Rearrange the supported size to put the ones with the same aspect ratio in the front
- // of the list and put others in the end from large to small. Some low end devices may
- // not able to get an supported resolution that match the preferred aspect ratio.
-
- // Group output sizes by aspect ratio.
- aspectRatioSizeListMap = groupSizesByAspectRatio(outputSizeCandidates)
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- if (targetSize != null) {
- // Remove unnecessary larger sizes from each aspect ratio size list
- for (key: Rational? in aspectRatioSizeListMap.keys) {
- removeSupportedSizesByTargetSize(aspectRatioSizeListMap[key], targetSize)
- }
- }
-
- // Sort the aspect ratio key set by the target aspect ratio.
- val aspectRatios: List<Rational?> = ArrayList(aspectRatioSizeListMap.keys)
- val fullFovRatio = if (activeArraySize != null) {
- Rational(activeArraySize.width(), activeArraySize.height())
- } else {
- null
- }
- Collections.sort(
- aspectRatios,
- CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio,
- fullFovRatio
- )
- )
-
- // Put available sizes into final result list by aspect ratio distance to target ratio.
- for (rational: Rational? in aspectRatios) {
- for (size: Size in aspectRatioSizeListMap[rational]!!) {
- // A size may exist in multiple groups in mod16 condition. Keep only one in
- // the final list.
- if (!supportedResolutions.contains(size)) {
- supportedResolutions.add(size)
- }
- }
- }
- }
-
- // TODO(b/245619094): Use ExtraCroppingQuirk to insert selected resolutions
-
- return supportedResolutions
- }
-
- /**
* Given all supported output sizes, lists out all possible size arrangements.
*/
private fun getAllPossibleSizeArrangements(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 11ce161..2f82f0a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -28,10 +28,8 @@
import android.media.CamcorderProfile.QUALITY_720P
import android.media.MediaRecorder
import android.os.Build
-import android.util.Pair
-import android.util.Rational
+import android.util.Range
import android.util.Size
-import android.view.Surface
import android.view.WindowManager
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
@@ -45,58 +43,41 @@
import androidx.camera.camera2.pipe.testing.FakeCameraBackend
import androidx.camera.camera2.pipe.testing.FakeCameraDevices
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraSelector.LensFacing
import androidx.camera.core.CameraX
import androidx.camera.core.CameraXConfig
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCase
import androidx.camera.core.concurrent.CameraCoordinator
+import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraThreadConfig
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
-import androidx.camera.core.impl.MutableStateObservable
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
-import androidx.camera.core.impl.utils.CompareSizesByArea
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.internal.utils.SizeUtil
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1440P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.Configs
import androidx.camera.testing.EncoderProfilesUtil
-import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraCoordinator
import androidx.camera.testing.fakes.FakeCameraFactory
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
import androidx.camera.testing.fakes.FakeUseCaseConfig
-import androidx.camera.video.FallbackStrategy
-import androidx.camera.video.MediaSpec
-import androidx.camera.video.Quality
-import androidx.camera.video.QualitySelector
-import androidx.camera.video.VideoCapture
-import androidx.camera.video.VideoOutput
-import androidx.camera.video.VideoOutput.SourceState
-import androidx.camera.video.VideoSpec
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
-import java.util.Arrays
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import org.junit.After
+import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -116,24 +97,15 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class SupportedSurfaceCombinationTest {
- private val sensorOrientation0 = 0
private val sensorOrientation90 = 90
- private val aspectRatio43 = Rational(4, 3)
- private val aspectRatio169 = Rational(16, 9)
private val landscapePixelArraySize = Size(4032, 3024)
- private val portraitPixelArraySize = Size(3024, 4032)
private val displaySize = Size(720, 1280)
private val vgaSize = Size(640, 480)
- private val vgaSizeStreamSpec = StreamSpec.builder(vgaSize).build()
private val previewSize = Size(1280, 720)
- private val previewSizeStreamSpec = StreamSpec.builder(previewSize).build()
private val recordSize = Size(3840, 2160)
- private val recordSizeStreamSpec = StreamSpec.builder(recordSize).build()
private val maximumSize = Size(4032, 3024)
- private val maximumSizeStreamSpec = StreamSpec.builder(maximumSize).build()
private val legacyVideoMaximumVideoSize = Size(1920, 1080)
private val mod16Size = Size(960, 544)
- private val mod16SizeStreamSpec = StreamSpec.builder(mod16Size).build()
private val profileUhd = EncoderProfilesUtil.createFakeEncoderProfilesProxy(
recordSize.width, recordSize.height
)
@@ -157,9 +129,6 @@
Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
Size(800, 450), // 16:9
Size(640, 480), // 4:3
- Size(320, 240), // 4:3
- Size(320, 180), // 16:9
- Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
)
private val context = InstrumentationRegistry.getInstrumentation().context
private var cameraFactory: FakeCameraFactory? = null
@@ -194,6 +163,12 @@
CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
}
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Surface combination support tests for guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
@Test
fun checkLegacySurfaceCombinationSupportedInLegacyDevice() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
@@ -519,614 +494,11 @@
assertThat(isSupported).isTrue()
}
- @Test
- fun checkTargetAspectRatio() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val fakeUseCase = FakeUseCaseConfig.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(fakeUseCase)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- val selectedStreamSpec = suggestedStreamSpecMap[useCaseToConfigMap[fakeUseCase]]!!
- val resultAspectRatio = Rational(
- selectedStreamSpec.resolution.width,
- selectedStreamSpec.resolution.height
- )
- assertThat(resultAspectRatio).isEqualTo(aspectRatio169)
- }
-
- @Test
- fun checkResolutionForMixedUseCase_AfterBindToLifecycle() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
-
- // The test case make sure the selected result is expected after the regular flow.
- val targetAspectRatio = aspectRatio169
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(mock())
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val imageAnalysis = ImageAnalysis.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val cameraUseCaseAdapter = CameraUtil
- .createCameraUseCaseAdapter(
- context,
- cameraCoordinator,
- CameraSelector.DEFAULT_BACK_CAMERA
- )
- cameraUseCaseAdapter.addUseCases(listOf(preview, imageCapture, imageAnalysis))
- val previewResolution = preview.attachedSurfaceResolution!!
- val previewRatio = Rational(
- previewResolution.width,
- previewResolution.height
- )
- val imageCaptureResolution = preview.attachedSurfaceResolution
- val imageCaptureRatio = Rational(
- imageCaptureResolution!!.width,
- imageCaptureResolution.height
- )
- val imageAnalysisResolution = preview.attachedSurfaceResolution
- val imageAnalysisRatio = Rational(
- imageAnalysisResolution!!.width,
- imageAnalysisResolution.height
- )
-
- // Checks no correction is needed.
- assertThat(previewRatio).isEqualTo(targetAspectRatio)
- assertThat(imageCaptureRatio).isEqualTo(targetAspectRatio)
- assertThat(imageAnalysisRatio).isEqualTo(targetAspectRatio)
- }
-
- @Test
- fun checkDefaultAspectRatioAndResolutionForMixedUseCase() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val preview = Preview.Builder().build()
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(mock())
- )
- val imageCapture = ImageCapture.Builder().build()
- val imageAnalysis = ImageAnalysis.Builder().build()
-
- // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
- // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
- // default aspect ratio will be selected.
- val cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(
- context,
- cameraCoordinator,
- CameraSelector.DEFAULT_BACK_CAMERA
- )
- cameraUseCaseAdapter.addUseCases(
- listOf(
- preview,
- imageCapture, imageAnalysis
- )
- )
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(preview)
- useCases.add(imageCapture)
- useCases.add(imageAnalysis)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]!!.resolution
- val imageCaptureSize = suggestedStreamSpecMap[useCaseToConfigMap[imageCapture]]!!.resolution
- val imageAnalysisSize =
- suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]!!.resolution
-
- val previewAspectRatio = Rational(
- previewSize.width,
- previewSize.height
- )
-
- val imageCaptureAspectRatio = Rational(
- imageCaptureSize.width,
- imageCaptureSize.height
- )
-
- val imageAnalysisAspectRatio = Rational(
- imageAnalysisSize.width,
- imageAnalysisSize.height
- )
-
- // Checks the default aspect ratio.
- assertThat(previewAspectRatio).isEqualTo(aspectRatio43)
- assertThat(imageCaptureAspectRatio).isEqualTo(aspectRatio43)
- assertThat(imageAnalysisAspectRatio).isEqualTo(aspectRatio43)
-
- // Checks the default resolution.
- assertThat(imageAnalysisSize).isEqualTo(vgaSize)
- }
-
- @Test
- fun checkSmallSizesAreFilteredOutByDefaultSize480p() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- /* This test case is for b/139018208 that get small resolution 144x256 with below
- conditions:
- 1. The target aspect ratio is set to the screen size 1080 x 2220 (9:18.5).
- 2. The camera doesn't provide any 9:18.5 resolution and the size 144x256(9:16)
- is considered the 9:18.5 mod16 version.
- 3. There is no other bigger resolution matched the target aspect ratio.
- */
- val displayWidth = 1080
- val displayHeight = 2220
- val preview = Preview.Builder()
- .setTargetResolution(Size(displayHeight, displayWidth))
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // Checks the preconditions.
- val preconditionSize = Size(256, 144)
- val targetRatio = Rational(displayHeight, displayWidth)
- val sizeList = ArrayList(listOf(*supportedSizes))
- assertThat(sizeList).contains(preconditionSize)
- for (s in supportedSizes) {
- val supportedRational = Rational(s.width, s.height)
- assertThat(supportedRational).isNotEqualTo(targetRatio)
- }
-
- // Checks the mechanism has filtered out the sizes which are smaller than default size
- // 480p.
- val previewSize = suggestedStreamSpecMap[useCaseToConfigMap[preview]]
- assertThat(previewSize).isNotEqualTo(preconditionSize)
- }
-
- @Test
- fun checkAspectRatioMatchedSizeCanBeSelected() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- // Sets each of mSupportedSizes as target resolution and also sets target rotation as
- // Surface.ROTATION to make it aligns the sensor direction and then exactly the same size
- // will be selected as the result. This test can also verify that size smaller than
- // 640x480 can be selected after set as target resolution.
- for (targetResolution in supportedSizes) {
- val imageCapture = ImageCapture.Builder().setTargetResolution(
- targetResolution
- ).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedStreamSpecMap =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- listOf(imageCapture.currentConfig)
- )
- assertThat(targetResolution).isEqualTo(
- suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
- )
- }
- }
-
- @Test
- fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
- // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
- // checks whether 1280x720 is selected or not.
- val targetResolution = Size(1280, 640)
- val imageCapture = ImageCapture.Builder().setTargetResolution(
- targetResolution
- ).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- listOf(imageCapture.currentConfig)
- )
- assertThat(Size(1280, 720)).isEqualTo(
- suggestedStreamSpecMap[imageCapture.currentConfig]?.resolution
- )
- }
-
- @Test
- fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = createVideoCapture()
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- @Test
- fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
- val quality = Quality.UHD
- val previewResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(previewSize))
- )
- val videoCapture = createVideoCapture(quality)
- val preview = Preview.Builder()
- .setSupportedResolutions(previewResolutionsPairs)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- }
- }
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- @Test
- fun suggestedStreamSpecsForMixedUseCaseInLimitedDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = createVideoCapture(Quality.HIGHEST)
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- recordSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- recordSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSizeStreamSpec
- )
- }
-
- @Test
- fun suggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val videoCapture = createVideoCapture(
- QualitySelector.from(
- Quality.UHD,
- FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
- )
- )
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // There are two possible combinations in Full level device
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
- // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- recordSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- recordSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSizeStreamSpec
- )
- }
-
- @Test
- fun suggestedResInFullDevice_videoRecordSizeLowPriority_imageCanGetMaxSize() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_4_3) // mMaximumSize(4032x3024) is 4:3
- .build()
- val videoCapture = createVideoCapture(
- QualitySelector.fromOrderedList(
- listOf(Quality.HD, Quality.FHD, Quality.UHD)
- )
- )
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // There are two possible combinations in Full level device
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- maximumSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- previewSizeStreamSpec
- ) // Quality.HD
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSizeStreamSpec
- )
- }
-
- @Test
- fun suggestedStreamSpecsWithSameSupportedListForDifferentUseCases() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- /* This test case is for b/132603284 that divide by zero issue crash happened in below
- conditions:
- 1. There are duplicated two 1280x720 supported sizes for ImageCapture and Preview.
- 2. supportedOutputSizes for ImageCapture and Preview in
- SupportedSurfaceCombination#getAllPossibleSizeArrangements are the same.
- */
- val imageCapture = ImageCapture.Builder()
- .setTargetResolution(displaySize)
- .build()
- val preview = Preview.Builder()
- .setTargetResolution(displaySize)
- .build()
- val imageAnalysis = ImageAnalysis.Builder()
- .setTargetResolution(displaySize)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(preview)
- useCases.add(imageAnalysis)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- previewSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- previewSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageAnalysis],
- previewSizeStreamSpec
- )
- }
-
- @Test
- fun throwsWhenSetBothTargetResolutionAndAspectRatioForDifferentUseCases() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
- var previewExceptionHappened = false
- val previewBuilder = Preview.Builder()
- .setTargetResolution(displaySize)
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- try {
- previewBuilder.build()
- } catch (e: IllegalArgumentException) {
- previewExceptionHappened = true
- }
- assertThat(previewExceptionHappened).isTrue()
- var imageCaptureExceptionHappened = false
- val imageCaptureConfigBuilder = ImageCapture.Builder()
- .setTargetResolution(displaySize)
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- try {
- imageCaptureConfigBuilder.build()
- } catch (e: IllegalArgumentException) {
- imageCaptureExceptionHappened = true
- }
- assertThat(imageCaptureExceptionHappened).isTrue()
- var imageAnalysisExceptionHappened = false
- val imageAnalysisConfigBuilder = ImageAnalysis.Builder()
- .setTargetResolution(displaySize)
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- try {
- imageAnalysisConfigBuilder.build()
- } catch (e: IllegalArgumentException) {
- imageAnalysisExceptionHappened = true
- }
- assertThat(imageAnalysisExceptionHappened).isTrue()
- }
-
- @Test
- fun suggestedStreamSpecsForCustomizedSupportedResolutions() {
-
- // Checks all suggested stream specs will have their resolutions become 640x480.
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val formatResolutionsPairList: MutableList<Pair<Int, Array<Size>>> = ArrayList()
- formatResolutionsPairList.add(Pair.create(ImageFormat.JPEG, arrayOf(vgaSize)))
- formatResolutionsPairList.add(
- Pair.create(ImageFormat.YUV_420_888, arrayOf(vgaSize))
- )
- formatResolutionsPairList.add(Pair.create(ImageFormat.PRIVATE, arrayOf(vgaSize)))
-
- // Sets use cases customized supported resolutions to 640x480 only.
- val imageCapture = ImageCapture.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val videoCapture = createVideoCapture(Quality.SD)
- val preview = Preview.Builder()
- .setSupportedResolutions(formatResolutionsPairList)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(imageCapture)
- useCases.add(videoCapture)
- useCases.add(preview)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
-
- // Checks all suggested stream specs will have their resolutions become 640x480.
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- vgaSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[videoCapture],
- vgaSizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- vgaSizeStreamSpec
- )
- }
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Surface config transformation tests
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
@Test
fun transformSurfaceConfigWithYUVAnalysisSize() {
@@ -1364,6 +736,755 @@
assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
}
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for LEGACY-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singlePrivStream_inLegacyDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singleJpegStream_inLegacyDevice() {
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singleYuvStream_inLegacyDevice() {
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusJpeg_inLegacyDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusJpeg_inLegacyDevice() {
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, previewSize)
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLegacyDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, previewSize)
+ put(privUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLegacyDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, previewSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusJpeg_inLegacyDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, previewSize)
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * Unsupported PRIV + JPEG + PRIV for legacy level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLegacyDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val privUseCas2 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RESOLUTION_VGA)
+ put(jpegUseCase, RESOLUTION_VGA)
+ put(privUseCas2, RESOLUTION_VGA)
+ }
+ Assert.assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for LIMITED-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLimitedDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCas2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, recordSize)
+ put(privUseCas2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLimitedDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, recordSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, recordSize)
+ put(yuvUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/RECORD + JPEG/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpeg_inLimitedDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, recordSize)
+ put(privUseCase2, previewSize)
+ put(jpegUseCase, recordSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusJpeg_inLimitedDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, recordSize)
+ put(jpegUseCase, recordSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusJpeg_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, previewSize)
+ put(yuvUseCase2, previewSize)
+ put(jpegUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * Unsupported YUV + PRIV + YUV for limited level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, RESOLUTION_VGA)
+ put(privUseCase, RESOLUTION_VGA)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ }
+ Assert.assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for FULL-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inFullDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, maximumSize)
+ put(privUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inFullDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inFullDevice() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, maximumSize)
+ put(yuvUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpeg_inFullDevice() {
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(jpegUseCase, maximumSize)
+ put(privUseCase1, previewSize)
+ put(privUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/VGA + PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusPrivPlusYuv_inFullDevice() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase1, maximumSize)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/VGA + YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusYuv_inFullDevice() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase3 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, maximumSize)
+ put(yuvUseCase2, previewSize)
+ put(yuvUseCase3, RESOLUTION_VGA)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * Unsupported PRIV + PRIV + YUV + RAW for full level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inFullDevice() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RESOLUTION_VGA)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(yuvUseCase, RESOLUTION_VGA)
+ put(rawUseCase, RESOLUTION_VGA)
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for Level-3 guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/VGA + YUV/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusYuvPlusRaw_inLevel3Device() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, previewSize)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(yuvUseCase, maximumSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/VGA + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpegPlusRaw_inLevel3Device() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, previewSize)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(jpegUseCase, maximumSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+
+ /**
+ * Unsupported PRIV + YUV + YUV + RAW for level-3 devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLevel3Device() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, RESOLUTION_VGA)
+ put(yuvUseCase1, RESOLUTION_VGA)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ put(rawUseCase, RESOLUTION_VGA)
+ }
+ Assert.assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for Burst-capability guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLimitedDevice_withBurstCapability() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, maximumSize)
+ put(privUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLimitedDevice_withBurstCapability() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inLimitedDevice_withBurstCapability() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, maximumSize)
+ put(yuvUseCase2, previewSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for RAW-capability guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * RAW/MAX
+ */
+ @Test
+ fun canSelectCorrectSizes_singleRawStream_inLimitedDevice_withRawCapability() {
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, previewSize)
+ put(privUseCase2, previewSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(yuvUseCase, previewSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusRAW_inLimitedDevice_withRawCapability() {
+ val yuvUseCase1 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, previewSize)
+ put(yuvUseCase2, previewSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusJpegPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, previewSize)
+ put(jpegUseCase, maximumSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusJpegPlusRAW_inLimitedDevice_withRawCapability() {
+ val yuvUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, previewSize)
+ put(jpegUseCase, maximumSize)
+ put(rawUseCase, maximumSize)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ private fun getSuggestedSpecsAndVerify(
+ useCasesExpectedResultMap: Map<UseCase, Size>,
+ attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
+ hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ capabilities: IntArray? = null,
+ compareWithAtMost: Boolean = false
+ ) {
+ setupCamera(hardwareLevel = hardwareLevel, capabilities = capabilities)
+ val supportedSurfaceCombination = SupportedSurfaceCombination(
+ context, fakeCameraMetadata,
+ mockEncoderProfilesAdapter
+ )
+
+ val useCaseConfigMap = getUseCaseToConfigMap(useCasesExpectedResultMap.keys.toList())
+ val useCaseConfigToOutputSizesMap =
+ getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
+ val suggestedStreamSpecs = supportedSurfaceCombination.getSuggestedStreamSpecifications(
+ false,
+ attachedSurfaceInfoList,
+ useCaseConfigToOutputSizesMap
+ )
+
+ useCasesExpectedResultMap.keys.forEach {
+ val resultSize = suggestedStreamSpecs[useCaseConfigMap[it]]!!.resolution
+ val expectedSize = useCasesExpectedResultMap[it]!!
+ if (!compareWithAtMost) {
+ assertThat(resultSize).isEqualTo(expectedSize)
+ } else {
+ assertThat(sizeIsAtMost(resultSize, expectedSize)).isTrue()
+ }
+ }
+ }
+
+ private fun getUseCaseToConfigMap(useCases: List<UseCase>): Map<UseCase, UseCaseConfig<*>> {
+ val useCaseConfigMap = mutableMapOf<UseCase, UseCaseConfig<*>>().apply {
+ useCases.forEach {
+ put(it, it.currentConfig)
+ }
+ }
+ return useCaseConfigMap
+ }
+
+ private fun getUseCaseConfigToOutputSizesMap(
+ useCaseConfigs: List<UseCaseConfig<*>>
+ ): Map<UseCaseConfig<*>, List<Size>> {
+ val resultMap = mutableMapOf<UseCaseConfig<*>, List<Size>>().apply {
+ useCaseConfigs.forEach {
+ put(it, supportedSizes.toList())
+ }
+ }
+
+ return resultMap
+ }
+
+ /**
+ * Helper function that returns whether size is <= maxSize
+ *
+ */
+ private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
+ return (size.height * size.width) <= (maxSize.height * maxSize.width)
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Other tests
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
@Test
fun maximumSizeForImageFormat() {
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY)
@@ -1380,981 +1501,6 @@
}
@Test
- fun isAspectRatioMatchWithSupportedMod16Resolution() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val preview = Preview.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .setDefaultResolution(mod16Size)
- .build()
- val imageCapture = ImageCapture.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
- .setDefaultResolution(mod16Size)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(preview)
- useCases.add(imageCapture)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec> =
- supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[preview],
- mod16SizeStreamSpec
- )
- assertThat(suggestedStreamSpecMap).containsEntry(
- useCaseToConfigMap[imageCapture],
- mod16SizeStreamSpec
- )
- }
-
- @Test
- fun sortByCompareSizesByArea_canSortSizesCorrectly() {
- val sizes = arrayOfNulls<Size>(supportedSizes.size)
-
- // Generates a unsorted array from mSupportedSizes.
- val centerIndex = supportedSizes.size / 2
- // Puts 2nd half sizes in the front
- if (supportedSizes.size - centerIndex >= 0) {
- System.arraycopy(
- supportedSizes,
- centerIndex, sizes, 0,
- supportedSizes.size - centerIndex
- )
- }
- // Puts 1st half sizes inversely in the tail
- for (j in centerIndex - 1 downTo 0) {
- sizes[supportedSizes.size - j - 1] = supportedSizes[j]
- }
-
- // The testing sizes array will be equal to mSupportedSizes after sorting.
- Arrays.sort(sizes, CompareSizesByArea(true))
- assertThat(listOf(*sizes)).isEqualTo(listOf(*supportedSizes))
- }
-
- @Test
- fun supportedOutputSizes_noConfigSettings() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. No any aspect ratio related setting. The returned sizes list will be sorted in
- // descending order.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(4032, 3024),
- Size(3840, 2160),
- Size(1920, 1440),
- Size(1920, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_aspectRatio4x3() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
- .build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 4/3 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(
- 640,
- 480
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_aspectRatio16x9() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
- AspectRatio.RATIO_16_9
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(
- 800,
- 450
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_targetResolution1080x1920InRotation0() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(1080, 1920)
- ).build()
-
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // target resolution will be calibrated by default target rotation 0 degree. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 16/9 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(
- 800,
- 450
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_targetResolutionLargerThan640x480() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
- Surface.ROTATION_90
- ).setTargetResolution(Size(1280, 960)).build()
-
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Target resolution larger than 640x480 won't overwrite
- // minimum size setting. Sizes smaller than 640x480 will be removed. The auto-resolution
- // mechanism will try to select the sizes which aspect ratio is nearest to the aspect
- // ratio of target resolution in priority. Therefore, sizes of aspect ratio 4/3 will be
- // in front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(
- 640,
- 480
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_targetResolutionSmallerThan640x480() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
- Surface.ROTATION_90
- ).setTargetResolution(Size(320, 240)).build()
-
- // Unnecessary big enough sizes will be removed from the result list. Minimum size will
- // be overwritten as 320x240. Sizes smaller than 320x240 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 4/3 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(
- 320,
- 240
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_targetResolution1800x1440NearTo4x3() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetRotation(
- Surface.ROTATION_90
- ).setTargetResolution(Size(1800, 1440)).build()
-
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Size 1800x1440 is near to 4/3
- // therefore, sizes of aspect ratio 4/3 will be in front of the returned sizes list and
- // the list is sorted in descending order.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Sizes of 4/3 are near to aspect ratio of 1800/1440
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480), // Sizes of 16/9 are far to aspect ratio of 1800/1440
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_targetResolution1280x600NearTo16x9() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(1280, 600)
- ).setTargetRotation(Surface.ROTATION_90).build()
-
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Size 1280x600 is near to 16/9,
- // therefore, sizes of aspect ratio 16/9 will be in front of the returned sizes list and
- // the list is sorted in descending order.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Sizes of 16/9 are near to aspect ratio of 1280/600
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450), // Sizes of 4/3 are far to aspect ratio of 1280/600
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_maxResolution1280x720() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(Size(1280, 720)).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 or
- // larger than 1280x720 will be removed. The returned sizes list will be sorted in
- // descending order.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_setCustomOrderedResolutions() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val customOrderedResolutions = listOf(
- Size(640, 480),
- Size(1280, 720),
- Size(1920, 1080),
- Size(3840, 2160),
- )
- val useCase = FakeUseCaseConfig.Builder()
- .setCustomOrderedResolutions(customOrderedResolutions)
- .setTargetResolution(Size(1280, 720))
- .setMaxResolution(Size(1920, 1440))
- .setDefaultResolution(Size(1280, 720))
- .setSupportedResolutions(
- listOf(
- Pair.create(
- ImageFormat.PRIVATE, arrayOf(
- Size(800, 450),
- Size(640, 480),
- Size(320, 240),
- )
- )
- )
- ).build()
-
- // Custom ordered resolutions is fully respected, meaning it will not be sorted or filtered
- // by other configurations such as max/default/target/supported resolutions.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- assertThat(resultList).containsExactlyElementsIn(customOrderedResolutions).inOrder()
- }
-
- @Test
- fun supportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
- Size(
- 1280,
- 720
- )
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. If there is no target resolution setting, it will be overwritten by default
- // resolution as 1280x720. Unnecessary big enough sizes will also be removed. The
- // returned sizes list will be sorted in descending order.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_defaultResolution1280x720_targetResolution1920x1080() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setDefaultResolution(
- Size(1280, 720)
- ).setTargetRotation(Surface.ROTATION_90).setTargetResolution(
- Size(1920, 1080)
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. There is target resolution 1920x1080, it won't be overwritten by default
- // resolution 1280x720. Unnecessary big enough sizes will also be removed. Sizes of
- // aspect ratio 16/9 will be in front of the returned sizes list and the list is sorted
- // in descending order. Other items will be put in the following that are sorted by
- // aspect ratio delta and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(
- 800,
- 450
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(1920, 1080)
- ).setTargetRotation(Surface.ROTATION_90).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. There is target resolution 1920x1080 (16:9). Even 640x480 does not match 16:9
- // requirement, it will still be returned to use.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(Size(640, 480))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenMaxSizeSmallerThanDefaultMiniSize() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(320, 240)
- ).build()
-
- // There is default minimum size 640x480 setting. Originally, sizes smaller than 640x480
- // will be removed. Due to maximal size bound is smaller than the default minimum size
- // bound and it is also smaller than 640x480, the default minimum size bound will be
- // ignored. Then, sizes equal to or smaller than 320x240 will be kept in the result list.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(320, 180)
- ).setTargetResolution(Size(320, 240)).setTargetRotation(
- Surface.ROTATION_90
- ).build()
-
- // The default minimum size 640x480 will be overwritten by the target resolution 320x240.
- // Originally, sizes smaller than 320x240 will be removed. Due to maximal size bound is
- // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
- // size bound will be ignored. Then, sizes equal to or smaller than 320x180 will be kept
- // in the result list.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenBothMaxAndTargetResolutionsSmallerThan640x480() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(320, 240)
- ).setTargetResolution(Size(320, 180)).setTargetRotation(
- Surface.ROTATION_90
- ).build()
-
- // The default minimum size 640x480 will be overwritten by the target resolution 320x180.
- // Originally, sizes smaller than 320x180 will be removed. Due to maximal size bound is
- // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
- // size bound will be ignored. Then, all sizes equal to or smaller than 320x320 will be
- // kept in the result list.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(320, 180),
- Size(256, 144),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenMaxSizeSmallerThanBigTargetResolution() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(1920, 1080)
- ).setTargetResolution(Size(3840, 2160)).setTargetRotation(
- Surface.ROTATION_90
- ).build()
-
- // Because the target size 3840x2160 is larger than 640x480, it won't overwrite the
- // default minimum size 640x480. Sizes smaller than 640x480 will be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 16/9 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(
- 800,
- 450
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenNoSizeBetweenMaxSizeAndTargetResolution() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(320, 200)
- ).setTargetResolution(Size(320, 190)).setTargetRotation(
- Surface.ROTATION_90
- ).build()
-
- // The default minimum size 640x480 will be overwritten by the target resolution 320x190.
- // Originally, sizes smaller than 320x190 will be removed. Due to there is no available
- // size between the maximal size and the minimum size bound and the maximal size is
- // smaller than 640x480, the default minimum size bound will be ignored. Then, sizes
- // equal to or smaller than 320x200 will be kept in the result list.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenTargetResolutionSmallerThanAnySize() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(192, 144)
- ).setTargetRotation(Surface.ROTATION_90).build()
-
- // The default minimum size 640x480 will be overwritten by the target resolution 192x144.
- // Because 192x144 is smaller than any size in the supported list, no one will be
- // filtered out by it. The result list will only keep one big enough size of aspect ratio
- // 4:3 and 16:9.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(320, 240),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(
- Size(192, 144)
- ).build()
-
- // All sizes will be filtered out by the max resolution 192x144 setting and an
- // IllegalArgumentException will be thrown.
- assertThrows(IllegalArgumentException::class.java) {
- supportedSurfaceCombination.getSupportedOutputSizes(useCase.currentConfig)
- }
- }
-
- @Test
- fun supportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(296, 144),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(185, 90)
- ).setTargetRotation(Surface.ROTATION_90).build()
-
- // The default minimum size 640x480 will be overwritten by the target resolution 185x90
- // (18.5:9). If mod 16 calculation is not ignored for the sizes smaller than 640x480, the
- // size 256x144 will be considered to match 18.5:9 and then become the first item in the
- // result list. After ignoring mod 16 calculation for small sizes, 256x144 will still be
- // kept as a 16:9 resolution as the result.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(296, 144),
- Size(256, 144),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
- Size(768, 432),
- Size(640, 480),
- Size(640, 360),
- Size(480, 360),
- Size(384, 288)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- Size(1080, 2016)
- ).build()
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf(
- Size(1920, 1080),
- Size(1280, 720),
- Size(864, 480),
- Size(768, 432),
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
- val supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(1280, 720),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
-
- // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
- // phone device which majorly supports portrait output sizes.
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation0, portraitPixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
- AspectRatio.RATIO_16_9
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
- // front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(
- 720,
- 1280
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(640, 480),
- Size(1280, 720)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
- val supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(1280, 720),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
-
- // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
- // tablet device which majorly supports portrait output sizes.
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation90, portraitPixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
- AspectRatio.RATIO_16_9
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
- // front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(
- 720,
- 1280
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(640, 480),
- Size(1280, 720)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizesOnTablet_aspectRatio16x9() {
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation0, landscapePixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
- AspectRatio.RATIO_16_9
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(
- 800,
- 450
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun supportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
- val supportedSizes = arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(720, 1280),
- Size(640, 480),
- Size(480, 640),
- Size(480, 360)
- )
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation0, landscapePixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val useCase = FakeUseCaseConfig.Builder().setTargetAspectRatio(
- AspectRatio.RATIO_16_9
- ).build()
-
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList: List<Size?> = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.currentConfig
- )
- val expectedList = listOf( // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(
- 1280,
- 720
- ), // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480),
- Size(480, 640),
- Size(720, 1280)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
fun determineRecordSizeFromStreamConfigurationMap() {
// Setup camera with non-integer camera Id
setupCamera(
@@ -2374,173 +1520,6 @@
)
}
- @Test
- fun canGet640x480_whenAnotherGroupMatchedInMod16Exists() {
- val supportedSizes = arrayOf(
- Size(4000, 3000),
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1024, 738), // This will create a 512/269 aspect ratio group that
- // 640x480 will be considered to match in mod16 condition.
- Size(800, 600),
- Size(640, 480),
- Size(320, 240)
- )
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation90, landscapePixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- // Sets the target resolution as 640x480 with target rotation as ROTATION_90 because the
- // sensor orientation is 90.
- val useCase = FakeUseCaseConfig.Builder().setTargetResolution(
- vgaSize
- ).setTargetRotation(Surface.ROTATION_90).build()
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- listOf(useCase.currentConfig)
- )
-
- // Checks 640x480 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(vgaSize)
- }
-
- @Test
- fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet() {
- val supportedSizes = arrayOf(
- Size(480, 480)
- )
- setupCamera(
- CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation90, landscapePixelArraySize, supportedSizes, null
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
-
- // Sets the max resolution as 720x1280
- val useCase = FakeUseCaseConfig.Builder().setMaxResolution(displaySize).build()
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- listOf(useCase.currentConfig)
- )
-
- // Checks 480x480 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase.currentConfig]?.resolution).isEqualTo(
- Size(480, 480)
- )
- }
-
- @Test
- fun previewSizeIsSelectedForImageAnalysis_imageCaptureHasNoSetSizeInLimitedDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val preview = Preview.Builder().build()
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- mock()
- )
- )
-
- // ImageCapture has no explicit target resolution setting
- val imageCapture = ImageCapture.Builder().build()
-
- // A LEGACY-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
- //
- // A LIMITED-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
- //
- // Even there is a RECORD size target resolution setting for ImageAnalysis, ImageCapture
- // will still have higher priority to have a MAXIMUM size resolution if the app doesn't
- // explicitly specify a RECORD size target resolution to ImageCapture.
- val imageAnalysis = ImageAnalysis.Builder()
- .setTargetRotation(Surface.ROTATION_90)
- .setTargetResolution(recordSize)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(preview)
- useCases.add(imageCapture)
- useCases.add(imageAnalysis)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
- previewSize
- )
- }
-
- @Test
- fun recordSizeIsSelectedForImageAnalysis_imageCaptureHasExplicitSizeInLimitedDevice() {
- setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED)
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, fakeCameraMetadata,
- mockEncoderProfilesAdapter
- )
- val preview = Preview.Builder().build()
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- mock()
- )
- )
-
- // ImageCapture has no explicit RECORD size target resolution setting
- val imageCapture = ImageCapture.Builder()
- .setTargetRotation(Surface.ROTATION_90)
- .setTargetResolution(recordSize)
- .build()
-
- // A LEGACY-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
- //
- // A LIMITED-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
- //
- // A RECORD can be selected for ImageAnalysis if the ImageCapture has a explicit RECORD
- // size target resolution setting. It means that the application know the trade-off and
- // the ImageAnalysis has higher priority to get a larger resolution than ImageCapture.
- val imageAnalysis = ImageAnalysis.Builder()
- .setTargetRotation(Surface.ROTATION_90)
- .setTargetResolution(recordSize)
- .build()
- val useCases: MutableList<UseCase> = ArrayList()
- useCases.add(preview)
- useCases.add(imageCapture)
- useCases.add(imageAnalysis)
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(fakeCameraMetadata.camera.value).cameraInfoInternal,
- useCases,
- useCaseConfigFactory
- )
- val suggestedStreamSpecMap = supportedSurfaceCombination.getSuggestedStreamSpecifications(
- false,
- emptyList(),
- ArrayList(useCaseToConfigMap.values)
- )
- assertThat(suggestedStreamSpecMap[useCaseToConfigMap[imageAnalysis]]?.resolution).isEqualTo(
- recordSize
- )
- }
-
private fun setupCamera(hardwareLevel: Int, capabilities: IntArray) {
setupCamera(
hardwareLevel, sensorOrientation90, landscapePixelArraySize,
@@ -2696,43 +1675,27 @@
return true
}
- /** Creates a VideoCapture with one ore more specific Quality */
- private fun createVideoCapture(vararg quality: Quality): VideoCapture<TestVideoOutput> {
- return createVideoCapture(QualitySelector.fromOrderedList(listOf(*quality)))
- }
- /** Creates a VideoCapture with a customized QualitySelector */
- /** Creates a VideoCapture with a default QualitySelector */
- @JvmOverloads
- fun createVideoCapture(
- qualitySelector: QualitySelector = VideoSpec.QUALITY_SELECTOR_AUTO
- ): VideoCapture<TestVideoOutput> {
- val mediaSpecBuilder = MediaSpec.builder()
- mediaSpecBuilder.configureVideo { builder: VideoSpec.Builder ->
- builder.setQualitySelector(
- qualitySelector
- )
+ private fun createUseCase(
+ captureType: UseCaseConfigFactory.CaptureType,
+ targetFrameRate: Range<Int>? = null
+ ): UseCase {
+ val builder = FakeUseCaseConfig.Builder(captureType, when (captureType) {
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
+ UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
+ else -> ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ })
+ targetFrameRate?.let {
+ builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
}
- val videoOutput = TestVideoOutput()
- videoOutput.mediaSpecObservable.setState(mediaSpecBuilder.build())
- return VideoCapture.withOutput(videoOutput)
+ return builder.build()
}
- /** A fake implementation of VideoOutput */
- class TestVideoOutput : VideoOutput {
- var mediaSpecObservable =
- MutableStateObservable.withInitialState(MediaSpec.builder().build())
- private var surfaceRequest: SurfaceRequest? = null
- private var sourceState: SourceState? = null
- override fun onSurfaceRequested(request: SurfaceRequest) {
- surfaceRequest = request
- }
-
- override fun getMediaSpec(): Observable<MediaSpec> {
- return mediaSpecObservable
- }
-
- override fun onSourceStateChanged(sourceState: SourceState) {
- this.sourceState = sourceState
- }
+ private fun createRawUseCase(): UseCase {
+ val builder = FakeUseCaseConfig.Builder()
+ builder.mutableConfig.insertOption(
+ UseCaseConfig.OPTION_INPUT_FORMAT,
+ ImageFormat.RAW_SENSOR
+ )
+ return builder.build()
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
index bde605f..b16cc83 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.kt
@@ -31,6 +31,7 @@
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraUnavailableException
import androidx.camera.core.Logger
+import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraStateRegistry
import androidx.camera.core.impl.Observable
@@ -38,6 +39,7 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.fakes.FakeCameraCoordinator
import androidx.core.os.HandlerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -69,12 +71,14 @@
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
PreTestCameraIdList(Camera2Config.defaultConfig())
)
+ private lateinit var cameraCoordinator: CameraCoordinator
private var camera2CameraImpl: Camera2CameraImpl? = null
private var cameraId: String? = null
private var anotherCameraDevice: AsyncCameraDevice? = null
@Before
fun setUp() {
+ cameraCoordinator = FakeCameraCoordinator()
cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)
Assume.assumeFalse("Device doesn't have an available back facing camera", cameraId == null)
}
@@ -463,7 +467,8 @@
cameraManagerCompat,
cameraId!!,
camera2CameraInfo,
- CameraStateRegistry(1),
+ cameraCoordinator,
+ CameraStateRegistry(cameraCoordinator, 1),
cameraExecutor!!,
cameraHandler!!,
DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext())
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
index 2fa3060..c62fed7 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
@@ -27,12 +27,14 @@
import androidx.camera.camera2.internal.compat.CameraManagerCompat
import androidx.camera.core.CameraSelector
import androidx.camera.core.Logger
+import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraInternal.State
import androidx.camera.core.impl.CameraStateRegistry
import androidx.camera.core.impl.Observable.Observer
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
+import androidx.camera.testing.fakes.FakeCameraCoordinator
import androidx.core.os.HandlerCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -155,11 +157,14 @@
cameraManagerCompat
)
+ val cameraCoordinator = FakeCameraCoordinator()
+
// Initialize camera instance
val camera = Camera2CameraImpl(
cameraManagerCompat,
camId,
camera2CameraInfo,
+ cameraCoordinator,
cameraRegistry,
cameraExecutor,
cameraHandler,
@@ -208,7 +213,9 @@
private lateinit var cameraHandlerThread: HandlerThread
private lateinit var cameraHandler: Handler
private lateinit var cameraExecutor: ExecutorService
- private val cameraRegistry: CameraStateRegistry by lazy { CameraStateRegistry(1) }
+ private lateinit var cameraCoordinator: CameraCoordinator
+ private val cameraRegistry: CameraStateRegistry by lazy { CameraStateRegistry(
+ cameraCoordinator, 1) }
@BeforeClass
@JvmStatic
@@ -217,6 +224,7 @@
cameraHandlerThread.start()
cameraHandler = HandlerCompat.createAsync(cameraHandlerThread.looper)
cameraExecutor = CameraXExecutors.newHandlerExecutor(cameraHandler)
+ cameraCoordinator = FakeCameraCoordinator()
}
@AfterClass
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
index d0c8fd7..c1f0759 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
@@ -29,6 +29,7 @@
import androidx.camera.core.CameraState.ERROR_CAMERA_IN_USE
import androidx.camera.core.CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED
import androidx.camera.core.CameraState.create
+import androidx.camera.core.concurrent.CameraCoordinator
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraStateRegistry
import androidx.camera.core.impl.Observable
@@ -37,6 +38,7 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCoordinator
import androidx.core.os.HandlerCompat
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
@@ -73,6 +75,7 @@
private lateinit var cameraId: String
private lateinit var camera: Camera2CameraImpl
+ private lateinit var cameraCoordinator: CameraCoordinator
private lateinit var cameraStateRegistry: CameraStateRegistry
@Before
@@ -107,7 +110,7 @@
// Open fake camera
val fakeCamera = FakeCamera()
- cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {})
+ cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {}, {})
cameraStateRegistry.tryOpenCamera(fakeCamera)
cameraStateRegistry.markCameraState(fakeCamera, CameraInternal.State.OPEN)
@@ -311,7 +314,7 @@
// Open fake camera
val fakeCamera = FakeCamera()
- cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {})
+ cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {}, {})
cameraStateRegistry.tryOpenCamera(fakeCamera)
cameraStateRegistry.markCameraState(fakeCamera, CameraInternal.State.OPEN)
@@ -342,14 +345,17 @@
cameraManagerCompat
)
+ cameraCoordinator = FakeCameraCoordinator()
+
// Initialize camera state registry and only allow 1 open camera at most inside CameraX
- cameraStateRegistry = CameraStateRegistry(1)
+ cameraStateRegistry = CameraStateRegistry(cameraCoordinator, 1)
// Initialize camera instance
camera = Camera2CameraImpl(
cameraManagerCompat,
cameraId,
camera2CameraInfo,
+ cameraCoordinator,
cameraStateRegistry,
CameraXExecutors.directExecutor(),
cameraHandler,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index b2a19f8..a7a5edc 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -16,6 +16,9 @@
package androidx.camera.camera2.internal;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE;
+
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.TestCase.assertTrue;
@@ -29,6 +32,7 @@
import static org.mockito.internal.verification.VerificationModeFactory.times;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
@@ -68,6 +72,7 @@
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.HandlerUtil;
import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraCoordinator;
import androidx.camera.testing.fakes.FakeCameraInfoInternal;
import androidx.camera.testing.fakes.FakeUseCase;
import androidx.camera.testing.fakes.FakeUseCaseConfig;
@@ -96,6 +101,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -116,6 +122,8 @@
public final class Camera2CameraImplTest {
@CameraSelector.LensFacing
private static final int DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_BACK;
+ @CameraSelector.LensFacing
+ private static final int DEFAULT_PAIRED_CAMERA_LENS_FACING = CameraSelector.LENS_FACING_FRONT;
// For the purpose of this test, always say we have 1 camera available.
private static final int DEFAULT_AVAILABLE_CAMERA_COUNT = 1;
private static final Set<CameraInternal.State> STABLE_STATES = new HashSet<>(Arrays.asList(
@@ -132,13 +140,16 @@
private ArrayList<FakeUseCase> mFakeUseCases = new ArrayList<>();
private Camera2CameraImpl mCamera2CameraImpl;
+ private Camera2CameraImpl mPairedCamera2CameraImpl;
private static HandlerThread sCameraHandlerThread;
private static Handler sCameraHandler;
+ private FakeCameraCoordinator mCameraCoordinator;
private CameraStateRegistry mCameraStateRegistry;
Semaphore mSemaphore;
OnImageAvailableListener mMockOnImageAvailableListener;
CameraCaptureCallback mMockRepeatingCaptureCallback;
String mCameraId;
+ String mPairedCameraId;
SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
@BeforeClass
@@ -160,14 +171,18 @@
mMockRepeatingCaptureCallback = Mockito.mock(CameraCaptureCallback.class);
mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
+ mPairedCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_PAIRED_CAMERA_LENS_FACING);
mSemaphore = new Semaphore(0);
- mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+ mCameraCoordinator = new FakeCameraCoordinator();
+ mCameraStateRegistry = new CameraStateRegistry(mCameraCoordinator,
+ DEFAULT_AVAILABLE_CAMERA_COUNT);
CameraManagerCompat cameraManagerCompat =
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
+
Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
mCameraId, cameraManagerCompat);
mCamera2CameraImpl = new Camera2CameraImpl(
- cameraManagerCompat, mCameraId, camera2CameraInfo,
+ cameraManagerCompat, mCameraId, camera2CameraInfo, mCameraCoordinator,
mCameraStateRegistry, sCameraExecutor, sCameraHandler,
DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext())
);
@@ -540,10 +555,11 @@
// Ensure real camera can't open due to max cameras being open
Camera mockCamera = mock(Camera.class);
-
- mCameraStateRegistry.registerCamera(mockCamera, CameraXExecutors.directExecutor(),
- () -> {
- });
+ mCameraStateRegistry.registerCamera(
+ mockCamera,
+ CameraXExecutors.directExecutor(),
+ () -> {},
+ () -> {});
mCameraStateRegistry.tryOpenCamera(mockCamera);
mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
@@ -973,6 +989,62 @@
.isTrue();
}
+ @Test
+ public void attachUseCaseWithTemplatePreviewInConcurrentMode() throws Exception {
+ // Arrange.
+ CameraManagerCompat cameraManagerCompat =
+ CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
+
+ PackageManager pm = ApplicationProvider.getApplicationContext().getPackageManager();
+ if (mPairedCameraId != null
+ && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT)) {
+ Camera2CameraInfoImpl pairedCamera2CameraInfo = new Camera2CameraInfoImpl(
+ mPairedCameraId, cameraManagerCompat);
+ mPairedCamera2CameraImpl = new Camera2CameraImpl(
+ cameraManagerCompat, mPairedCameraId, pairedCamera2CameraInfo,
+ mCameraCoordinator,
+ mCameraStateRegistry, sCameraExecutor, sCameraHandler,
+ DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext()));
+ mCameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(
+ new HashMap<String, CameraSelector>() {{
+ put(mCameraId, CameraSelector.DEFAULT_BACK_CAMERA);
+ put(mPairedCameraId, CameraSelector.DEFAULT_FRONT_CAMERA);
+ }});
+ mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+ mCameraStateRegistry.onCameraOperatingModeUpdated(
+ CAMERA_OPERATING_MODE_SINGLE, CAMERA_OPERATING_MODE_CONCURRENT);
+
+ // Act.
+ UseCase preview1 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
+ false);
+ mCamera2CameraImpl.attachUseCases(Arrays.asList(preview1));
+ mCamera2CameraImpl.onUseCaseActive(preview1);
+ HandlerUtil.waitForLooperToIdle(sCameraHandler);
+
+ // Assert.
+ ArgumentCaptor<CameraCaptureResult> captor =
+ ArgumentCaptor.forClass(CameraCaptureResult.class);
+ verify(mMockRepeatingCaptureCallback, never()).onCaptureCompleted(captor.capture());
+
+ // Act.
+ UseCase preview2 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
+ false);
+ mPairedCamera2CameraImpl.attachUseCases(Arrays.asList(preview2));
+ mPairedCamera2CameraImpl.onUseCaseActive(preview2);
+ HandlerUtil.waitForLooperToIdle(sCameraHandler);
+
+ // Assert.
+ captor = ArgumentCaptor.forClass(CameraCaptureResult.class);
+ verify(mMockRepeatingCaptureCallback, timeout(4000).atLeastOnce())
+ .onCaptureCompleted(captor.capture());
+ CaptureResult captureResult = (captor.getValue()).getCaptureResult();
+ assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
+ .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
+
+ mCamera2CameraImpl.detachUseCases(Arrays.asList(preview1));
+ }
+ }
+
private DeferrableSurface getUseCaseSurface(UseCase useCase) {
return useCase.getSessionConfig().getSurfaces().get(0);
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 178b0c4..f886234 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -152,7 +152,8 @@
mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
mSemaphore = new Semaphore(0);
- mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+ mCameraStateRegistry = new CameraStateRegistry(mCameraCoordinator,
+ DEFAULT_AVAILABLE_CAMERA_COUNT);
CameraManagerCompat cameraManagerCompat =
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
@@ -161,6 +162,7 @@
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext()),
mCameraId,
camera2CameraInfo,
+ mCameraCoordinator,
mCameraStateRegistry, sCameraExecutor, sCameraHandler,
DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext())
);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
index bc62ecf..0f531be 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
@@ -65,7 +65,6 @@
@NonNull CameraThreadConfig threadConfig,
@Nullable CameraSelector availableCamerasSelector) throws InitializationException {
mThreadConfig = threadConfig;
- mCameraStateRegistry = new CameraStateRegistry(DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS);
mCameraManager = CameraManagerCompat.from(context, mThreadConfig.getSchedulerHandler());
mDisplayInfoManager = DisplayInfoManager.getInstance(context);
@@ -73,6 +72,9 @@
this, availableCamerasSelector);
mAvailableCameraIds = getBackwardCompatibleCameraIds(optimizedCameraIds);
mCameraCoordinator = new Camera2CameraCoordinator(mCameraManager);
+ mCameraStateRegistry = new CameraStateRegistry(mCameraCoordinator,
+ DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS);
+ mCameraCoordinator.addListener(mCameraStateRegistry);
}
@Override
@@ -85,6 +87,7 @@
return new Camera2CameraImpl(mCameraManager,
cameraId,
getCameraInfo(cameraId),
+ mCameraCoordinator,
mCameraStateRegistry,
mThreadConfig.getCameraExecutor(),
mThreadConfig.getSchedulerHandler(),
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 27723ed..e11a3b9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -16,6 +16,8 @@
package androidx.camera.camera2.internal;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
+
import android.annotation.SuppressLint;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
@@ -48,6 +50,7 @@
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
+import androidx.camera.core.concurrent.CameraCoordinator;
import androidx.camera.core.impl.CameraConfig;
import androidx.camera.core.impl.CameraConfigs;
import androidx.camera.core.impl.CameraControlInternal;
@@ -165,8 +168,10 @@
final Map<CaptureSessionInterface, ListenableFuture<Void>> mReleasedCaptureSessions =
new LinkedHashMap<>();
- private final CameraAvailability mCameraAvailability;
- private final CameraStateRegistry mCameraStateRegistry;
+ @NonNull final CameraAvailability mCameraAvailability;
+ @NonNull final CameraConfigureAvailable mCameraConfigureAvailable;
+ @NonNull final CameraCoordinator mCameraCoordinator;
+ @NonNull final CameraStateRegistry mCameraStateRegistry;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Set<CaptureSession> mConfiguringForClose = new HashSet<>();
@@ -200,6 +205,7 @@
*
* @param cameraManager the camera service used to retrieve a camera
* @param cameraId the name of the camera as defined by the camera service
+ * @param cameraCoordinator the camera coordinator for concurrent camera mode
* @param cameraStateRegistry An registry used to track the state of multiple cameras.
* Used as a fence to ensure the number of simultaneously
* opened cameras is limited.
@@ -207,14 +213,17 @@
* @throws CameraUnavailableException if the {@link CameraCharacteristics} is unavailable. This
* could occur if the camera was disconnected.
*/
- Camera2CameraImpl(@NonNull CameraManagerCompat cameraManager,
+ Camera2CameraImpl(
+ @NonNull CameraManagerCompat cameraManager,
@NonNull String cameraId,
@NonNull Camera2CameraInfoImpl cameraInfoImpl,
+ @NonNull CameraCoordinator cameraCoordinator,
@NonNull CameraStateRegistry cameraStateRegistry,
@NonNull Executor executor,
@NonNull Handler schedulerHandler,
@NonNull DisplayInfoManager displayInfoManager) throws CameraUnavailableException {
mCameraManager = cameraManager;
+ mCameraCoordinator = cameraCoordinator;
mCameraStateRegistry = cameraStateRegistry;
mScheduledExecutorService = CameraXExecutors.newHandlerExecutor(schedulerHandler);
mExecutor = CameraXExecutors.newSequentialExecutor(executor);
@@ -243,9 +252,14 @@
cameraInfoImpl.getCameraQuirks(), DeviceQuirks.getAll());
mCameraAvailability = new CameraAvailability(cameraId);
+ mCameraConfigureAvailable = new CameraConfigureAvailable();
// Register an observer to update the number of available cameras
- mCameraStateRegistry.registerCamera(this, mExecutor, mCameraAvailability);
+ mCameraStateRegistry.registerCamera(
+ this,
+ mExecutor,
+ mCameraConfigureAvailable,
+ mCameraAvailability);
mCameraManager.registerAvailabilityCallback(mExecutor, mCameraAvailability);
}
@@ -315,6 +329,7 @@
debugLog("Closing camera.");
switch (mState) {
case OPENED:
+ case CONFIGURED:
setState(InternalState.CLOSING);
closeCamera(/*abortInFlightCaptures=*/false);
break;
@@ -477,6 +492,7 @@
finishClose();
break;
case OPENED:
+ case CONFIGURED:
setState(InternalState.RELEASING);
//TODO(b/162314023): Avoid calling abortCapture to prevent the many test failures
// caused by shutdown(). We should consider re-enabling it once the cause is
@@ -981,6 +997,7 @@
}
/** @hide */
+ @NonNull
@RestrictTo(RestrictTo.Scope.TESTS)
public CameraAvailability getCameraAvailability() {
return mCameraAvailability;
@@ -1131,6 +1148,15 @@
return;
}
+ // Checks if capture session is allowed to open in concurrent camera mode.
+ if (!mCameraStateRegistry.tryOpenCaptureSession(
+ mCameraDevice.getId(),
+ mCameraCoordinator.getPairedConcurrentCameraId(mCameraDevice.getId()))) {
+ debugLog("Unable to create capture session in camera operating mode = "
+ + mCameraCoordinator.getCameraOperatingMode());
+ return;
+ }
+
Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
mUseCaseAttachState.getAttachedSessionConfigs(),
@@ -1146,7 +1172,11 @@
@Override
@ExecutedBy("mExecutor")
public void onSuccess(@Nullable Void result) {
- // Nothing to do.
+ // TODO(b/271182406): Apply the CONFIGURED state to non-concurrent mode.
+ if (mCameraCoordinator.getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT
+ && mState == InternalState.OPENED) {
+ setState(InternalState.CONFIGURED);
+ }
}
@Override
@@ -1396,6 +1426,13 @@
*/
OPENED,
/**
+ * A stable state where the camera has been opened and capture session has been configured.
+ *
+ * <p>It is a state only used in concurrent mode to differentiate from OPENED state for
+ * capture session configuration status.
+ */
+ CONFIGURED,
+ /**
* A transitional state where the camera device is currently closing.
*
* <p>At the end of this state, the camera should move into the INITIALIZED state.
@@ -1468,6 +1505,9 @@
case OPENED:
publicState = State.OPEN;
break;
+ case CONFIGURED:
+ publicState = State.CONFIGURED;
+ break;
case CLOSING:
publicState = State.CLOSING;
break;
@@ -1581,7 +1621,12 @@
case OPENING:
case REOPENING:
setState(InternalState.OPENED);
- openCaptureSession();
+ if (mCameraStateRegistry.tryOpenCaptureSession(
+ cameraDevice.getId(),
+ mCameraCoordinator.getPairedConcurrentCameraId(
+ mCameraDevice.getId()))) {
+ openCaptureSession();
+ }
break;
default:
throw new IllegalStateException(
@@ -1643,6 +1688,7 @@
break;
case OPENING:
case OPENED:
+ case CONFIGURED:
case REOPENING:
Logger.d(TAG, String.format("CameraDevice.onError(): %s failed with %s while "
+ "in %s state. Will attempt recovering from error.",
@@ -1659,6 +1705,7 @@
private void handleErrorOnOpen(@NonNull CameraDevice cameraDevice, int error) {
Preconditions.checkState(
mState == InternalState.OPENING || mState == InternalState.OPENED
+ || mState == InternalState.CONFIGURED
|| mState == InternalState.REOPENING,
"Attempt to handle open error from non open state: " + mState);
switch (error) {
@@ -1975,6 +2022,17 @@
}
}
+ final class CameraConfigureAvailable
+ implements CameraStateRegistry.OnConfigureAvailableListener {
+
+ @Override
+ public void onConfigureAvailable() {
+ if (mState == InternalState.OPENED) {
+ openCaptureSession();
+ }
+ }
+ }
+
final class ControlUpdateListenerInternal implements
CameraControlInternal.ControlUpdateCallback {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index 5e8dd36..38a2e6a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -17,6 +17,7 @@
package androidx.camera.camera2.internal;
import android.content.Context;
+import android.hardware.camera2.CameraDevice;
import android.media.CamcorderProfile;
import android.util.Size;
@@ -43,7 +44,7 @@
* Camera device manager to provide the guaranteed supported stream capabilities related info for
* all camera devices
*
- * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * <p>{@link CameraDevice#createCaptureSession} defines the default
* guaranteed stream combinations for different hardware level devices. It defines what combination
* of surface configuration type and size pairs can be supported for different hardware level camera
* devices. This structure is used to store the guaranteed supported stream capabilities related
@@ -177,12 +178,16 @@
/**
* Retrieves a map of suggested stream specifications for the given list of use cases.
*
- * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
- * @param cameraId the camera id of the camera device used by the use cases
- * @param existingSurfaces list of surfaces already configured and used by the camera. The
- * stream specifications for these surface can not change.
- * @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested stream specification
+ * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise
+ * false.
+ * @param cameraId the camera id of the camera device used by the
+ * use cases
+ * @param existingSurfaces list of surfaces already configured and used by
+ * the camera. The stream specifications for these
+ * surface can not change.
+ * @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
+ * supported sizes list that will be given a
+ * suggested stream specification
* @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
@@ -193,11 +198,11 @@
@NonNull
@Override
public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
- boolean isConcurrentCameraModeOn,
- @NonNull String cameraId,
+ boolean isConcurrentCameraModeOn, @NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
- @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
- Preconditions.checkArgument(!newUseCaseConfigs.isEmpty(), "No new use cases to be bound.");
+ @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
+ Preconditions.checkArgument(!newUseCaseConfigsSupportedSizeMap.isEmpty(),
+ "No new use cases to be bound.");
SupportedSurfaceCombination supportedSurfaceCombination =
mCameraSupportedSurfaceCombinationMap.get(cameraId);
@@ -210,6 +215,6 @@
return supportedSurfaceCombination.getSuggestedStreamSpecifications(
isConcurrentCameraModeOn,
existingSurfaces,
- newUseCaseConfigs);
+ newUseCaseConfigsSupportedSizeMap);
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraStateMachine.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraStateMachine.java
index 3684bb5..a6c7b0f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraStateMachine.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraStateMachine.java
@@ -62,6 +62,7 @@
newPublicState = CameraState.create(CameraState.Type.OPENING, stateError);
break;
case OPEN:
+ case CONFIGURED:
newPublicState = CameraState.create(CameraState.Type.OPEN, stateError);
break;
case CLOSING:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
deleted file mode 100644
index b0b6984..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
+++ /dev/null
@@ -1,590 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.internal;
-
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
-
-import android.graphics.Rect;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.params.StreamConfigurationMap;
-import android.os.Build;
-import android.util.Rational;
-import android.util.Size;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
-import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
-import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
-import androidx.camera.core.AspectRatio;
-import androidx.camera.core.Logger;
-import androidx.camera.core.ResolutionSelector;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.SizeCoordinate;
-import androidx.camera.core.impl.SurfaceConfig;
-import androidx.camera.core.impl.utils.AspectRatioUtil;
-import androidx.camera.core.impl.utils.CameraOrientationUtil;
-import androidx.camera.core.impl.utils.CompareSizesByArea;
-import androidx.camera.core.internal.utils.SizeUtil;
-import androidx.core.util.Preconditions;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * The supported output sizes collector to help collect the available resolution candidate list
- * according to the use case config and the following settings in {@link ResolutionSelector}:
- *
- * 1. Preferred aspect ratio
- * 2. Preferred resolution
- * 3. Max resolution
- * 4. Is high resolution enabled
- */
-@RequiresApi(21)
-final class SupportedOutputSizesCollector {
- private static final String TAG = "SupportedOutputSizesCollector";
- @NonNull
- private final CameraCharacteristicsCompat mCharacteristics;
- @NonNull
- private final DisplayInfoManager mDisplayInfoManager;
- private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
- private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
- private final Map<Integer, Size[]> mHighResolutionOutputSizesCache = new HashMap<>();
- private final boolean mIsSensorLandscapeResolution;
- private final boolean mIsBurstCaptureSupported;
- private final Size mActiveArraySize;
- private final int mSensorOrientation;
- private final int mLensFacing;
-
- SupportedOutputSizesCollector(@NonNull String cameraId,
- @NonNull CameraCharacteristicsCompat cameraCharacteristics,
- @NonNull DisplayInfoManager displayInfoManager) {
- mCharacteristics = cameraCharacteristics;
- mDisplayInfoManager = displayInfoManager;
-
- mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
- mIsBurstCaptureSupported = isBurstCaptureSupported();
-
- Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
- mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
-
- mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
- mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
- }
-
- /**
- * Collects and sorts the resolution candidate list by the following steps:
- *
- * 1. Collects the candidate list by the high resolution enable setting.
- * 2. Filters out the candidate list according to the min size bound and max resolution.
- * 3. Sorts the candidate list according to the rules of legacy resolution API or new
- * Resolution API.
- * 4. Forces select specific resolutions according to ResolutionCorrector workaround.
- */
- @NonNull
- List<Size> getSupportedOutputSizes(@NonNull ResolutionSelector resolutionSelector,
- int imageFormat, @Nullable Size miniBoundingSize, boolean isHighResolutionDisabled,
- @Nullable Size[] customizedSupportSizes) {
- // 1. Collects the candidate list by the high resolution enable setting.
- List<Size> resolutionCandidateList = collectResolutionCandidateList(resolutionSelector,
- imageFormat, isHighResolutionDisabled, customizedSupportSizes);
-
- // 2. Filters out the candidate list according to the min size bound and max resolution.
- resolutionCandidateList = filterOutResolutionCandidateListBySettings(
- resolutionCandidateList, resolutionSelector);
-
- // 3. Sorts the candidate list according to the rules of new Resolution API.
- resolutionCandidateList = sortResolutionCandidateListByResolutionSelector(
- resolutionCandidateList, resolutionSelector,
- mDisplayInfoManager.getMaxSizeDisplay().getRotation(), miniBoundingSize);
-
- // 4. Forces select specific resolutions according to ResolutionCorrector workaround.
- resolutionCandidateList = mResolutionCorrector.insertOrPrioritize(
- SurfaceConfig.getConfigType(imageFormat), resolutionCandidateList);
-
- return resolutionCandidateList;
- }
-
- /**
- * Collects the resolution candidate list.
- *
- * 1. Customized supported resolutions list will be returned when it exists
- * 2. Otherwise, the sizes retrieved from {@link StreamConfigurationMap#getOutputSizes(int)}
- * will be the base of the resolution candidate list.
- * 3. High resolution sizes retrieved from
- * {@link StreamConfigurationMap#getHighResolutionOutputSizes(int)} will be included when
- * {@link ResolutionSelector#isHighResolutionEnabled()} returns true.
- *
- * The returned list will be sorted in descending order and duplicate items will be removed.
- */
- @NonNull
- private List<Size> collectResolutionCandidateList(
- @NonNull ResolutionSelector resolutionSelector, int imageFormat,
- boolean isHighResolutionDisabled, @Nullable Size[] customizedSupportedSizes) {
- Size[] outputSizes = customizedSupportedSizes;
-
- if (outputSizes == null) {
- boolean highResolutionEnabled =
- !isHighResolutionDisabled && resolutionSelector.isHighResolutionEnabled();
- outputSizes = getAllOutputSizesByFormat(imageFormat, highResolutionEnabled);
- }
-
- // Sort the output sizes. The Comparator result must be reversed to have a descending order
- // result.
- Arrays.sort(outputSizes, new CompareSizesByArea(true));
-
- List<Size> resultList = Arrays.asList(outputSizes);
-
- if (resultList.isEmpty()) {
- throw new IllegalArgumentException(
- "Resolution candidate list is empty when collecting by the settings!");
- }
-
- return resultList;
- }
-
- /**
- * Filters out the resolution candidate list by the max resolution setting.
- *
- * The input size list should have been sorted in descending order.
- */
- private List<Size> filterOutResolutionCandidateListBySettings(
- @NonNull List<Size> resolutionCandidateList,
- @NonNull ResolutionSelector resolutionSelector) {
- // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
- // selection logic should depend on ResolutionSelector's settings.
- Size maxResolution = resolutionSelector.getMaxResolution();
-
- // Filter out the resolution candidate list by the max resolution. Sizes that any edge
- // exceeds the max resolution will be filtered out.
- List<Size> resultList;
-
- if (maxResolution == null) {
- resultList = new ArrayList<>(resolutionCandidateList);
- } else {
- resultList = new ArrayList<>();
- for (Size outputSize : resolutionCandidateList) {
- if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
- resultList.add(outputSize);
- }
- }
- }
-
- if (resultList.isEmpty()) {
- throw new IllegalArgumentException(
- "Resolution candidate list is empty after filtering out by the settings!");
- }
-
- return resultList;
- }
-
- /**
- * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
- *
- * The list will be sorted by the following order:
- * 1. size of preferred resolution
- * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
- * preferred resolution.
- * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
- * size in descending order of resolution area size.
- * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
- * area size.
- */
- @NonNull
- private List<Size> sortResolutionCandidateListByResolutionSelector(
- @NonNull List<Size> resolutionCandidateList,
- @NonNull ResolutionSelector resolutionSelector,
- @ImageOutputConfig.RotationValue int targetRotation,
- @Nullable Size miniBoundingSize) {
- Rational aspectRatio = getTargetAspectRatioByResolutionSelector(resolutionSelector);
- Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
- + " value.");
-
- Size targetSize = getTargetSizeByResolutionSelector(resolutionSelector, targetRotation,
- mSensorOrientation, mLensFacing);
- List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
- resolutionCandidateList, aspectRatio, miniBoundingSize);
-
- // Moves the target size to the first position if it exists in the resolution candidate
- // list and there is no quirk that needs to select specific aspect ratio sizes in priority.
- if (resultList.contains(targetSize)) {
- resultList.remove(targetSize);
- resultList.add(0, targetSize);
- }
-
- return resultList;
- }
-
- @NonNull
- private Size[] getAllOutputSizesByFormat(int imageFormat, boolean highResolutionEnabled) {
- Size[] outputs = mOutputSizesCache.get(imageFormat);
- if (outputs == null) {
- outputs = doGetOutputSizesByFormat(imageFormat);
- mOutputSizesCache.put(imageFormat, outputs);
- }
-
- Size[] highResolutionOutputs = null;
-
- // A device that does not support the BURST_CAPTURE capability,
- // StreamConfigurationMap#getHighResolutionOutputSizes() will return null.
- if (highResolutionEnabled && mIsBurstCaptureSupported) {
- highResolutionOutputs = mHighResolutionOutputSizesCache.get(imageFormat);
-
- // High resolution output sizes list may be empty. If it is empty and cached in the
- // map, don't need to query it again.
- if (highResolutionOutputs == null && !mHighResolutionOutputSizesCache.containsKey(
- imageFormat)) {
- highResolutionOutputs = doGetHighResolutionOutputSizesByFormat(imageFormat);
- mHighResolutionOutputSizesCache.put(imageFormat, highResolutionOutputs);
- }
- }
-
- // Combines output sizes if high resolution sizes list is not empty.
- if (highResolutionOutputs != null) {
- Size[] allOutputs = Arrays.copyOf(highResolutionOutputs,
- highResolutionOutputs.length + outputs.length);
- System.arraycopy(outputs, 0, allOutputs, highResolutionOutputs.length, outputs.length);
- outputs = allOutputs;
- }
-
- return outputs;
- }
-
- @NonNull
- private Size[] doGetOutputSizesByFormat(int imageFormat) {
- StreamConfigurationMapCompat mapCompat = mCharacteristics.getStreamConfigurationMapCompat();
- Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
- if (outputSizes == null) {
- throw new IllegalArgumentException(
- "Can not get supported output size for the format: " + imageFormat);
- }
-
- return outputSizes;
- }
-
- @Nullable
- private Size[] doGetHighResolutionOutputSizesByFormat(int imageFormat) {
- if (Build.VERSION.SDK_INT < 23) {
- return null;
- }
-
- Size[] outputSizes;
-
- StreamConfigurationMap map =
- mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-
- if (map == null) {
- throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
- }
-
- outputSizes = Api23Impl.getHighResolutionOutputSizes(map, imageFormat);
-
- return outputSizes;
- }
-
- /**
- * Returns the target aspect ratio rational value.
- *
- * @param resolutionSelector the resolution selector of the use case.
- */
- @Nullable
- private Rational getTargetAspectRatioByResolutionSelector(
- @NonNull ResolutionSelector resolutionSelector) {
- Rational outputRatio = null;
-
- @AspectRatio.Ratio int aspectRatio = resolutionSelector.getPreferredAspectRatio();
- switch (aspectRatio) {
- case AspectRatio.RATIO_4_3:
- outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3
- : ASPECT_RATIO_3_4;
- break;
- case AspectRatio.RATIO_16_9:
- outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9
- : ASPECT_RATIO_9_16;
- break;
- case AspectRatio.RATIO_DEFAULT:
- break;
- default:
- Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
- }
- return outputRatio;
- }
-
- @Nullable
- static Size getTargetSizeByResolutionSelector(@NonNull ResolutionSelector resolutionSelector,
- int targetRotation, int sensorOrientation, int lensFacing) {
- Size targetSize = resolutionSelector.getPreferredResolution();
-
- // Calibrate targetSize by the target rotation value if it is set by the Android View
- // coordinate orientation.
- if (resolutionSelector.getSizeCoordinate() == SizeCoordinate.ANDROID_VIEW) {
- targetSize = flipSizeByRotation(targetSize, targetRotation, lensFacing,
- sensorOrientation);
- }
- return targetSize;
- }
-
- private static boolean isRotationNeeded(int targetRotation, int lensFacing,
- int sensorOrientation) {
- int relativeRotationDegrees =
- CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
-
- // Currently this assumes that a back-facing camera is always opposite to the screen.
- // This may not be the case for all devices, so in the future we may need to handle that
- // scenario.
- boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
-
- int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
- relativeRotationDegrees,
- sensorOrientation,
- isOppositeFacingScreen);
- return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
- }
-
- /**
- * Sorts the resolution candidate list according to the target aspect ratio and size settings.
- *
- * 1. The resolution candidate list will be grouped by aspect ratio.
- * 2. Each group only keeps one size which is not smaller than the target size.
- * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
- * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
- * 4. Concatenate all sizes as the result list
- */
- @NonNull
- private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
- @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
- @Nullable Size miniBoundingSize) {
- // Rearrange the supported size to put the ones with the same aspect ratio in the front
- // of the list and put others in the end from large to small. Some low end devices may
- // not able to get an supported resolution that match the preferred aspect ratio.
-
- // Group output sizes by aspect ratio.
- Map<Rational, List<Size>> aspectRatioSizeListMap =
- groupSizesByAspectRatio(resolutionCandidateList);
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- if (miniBoundingSize != null) {
- // Remove unnecessary larger sizes from each aspect ratio size list
- for (Rational key : aspectRatioSizeListMap.keySet()) {
- removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
- miniBoundingSize);
- }
- }
-
- // Sort the aspect ratio key set by the target aspect ratio.
- List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
- Rational fullFovRatio = mActiveArraySize != null ? new Rational(
- mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
- Collections.sort(aspectRatios,
- new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio, fullFovRatio));
-
- List<Size> resultList = new ArrayList<>();
-
- // Put available sizes into final result list by aspect ratio distance to target ratio.
- for (Rational rational : aspectRatios) {
- for (Size size : aspectRatioSizeListMap.get(rational)) {
- // A size may exist in multiple groups in mod16 condition. Keep only one in
- // the final list.
- if (!resultList.contains(size)) {
- resultList.add(size);
- }
- }
- }
-
- return resultList;
- }
-
- private boolean isBurstCaptureSupported() {
- int[] availableCapabilities =
- mCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
-
- if (availableCapabilities != null) {
- for (int capability : availableCapabilities) {
- if (capability
- == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // The following functions can be reused by the legacy resolution selection logic. The can be
- // changed as private function after the legacy resolution API is completely removed.
- //////////////////////////////////////////////////////////////////////////////////////////
-
- // Use target rotation to calibrate the size.
- @Nullable
- static Size flipSizeByRotation(@Nullable Size size, int targetRotation, int lensFacing,
- int sensorOrientation) {
- Size outputSize = size;
- // Calibrates the size with the display and sensor rotation degrees values.
- if (size != null && isRotationNeeded(targetRotation, lensFacing, sensorOrientation)) {
- outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
- }
- return outputSize;
- }
-
- static Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
- Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
-
- List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
-
- for (Rational aspectRatio: aspectRatioKeys) {
- aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
- }
-
- for (Size outputSize : sizes) {
- for (Rational key : aspectRatioSizeListMap.keySet()) {
- // Put the size into all groups that is matched in mod16 condition since a size
- // may match multiple aspect ratio in mod16 algorithm.
- if (hasMatchingAspectRatio(outputSize, key)) {
- aspectRatioSizeListMap.get(key).add(outputSize);
- }
- }
- }
-
- return aspectRatioSizeListMap;
- }
-
- /**
- * Returns the grouping aspect ratio keys of the input resolution list.
- *
- * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
- * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
- */
- @NonNull
- static List<Rational> getResolutionListGroupingAspectRatioKeys(
- @NonNull List<Size> resolutionCandidateList) {
- List<Rational> aspectRatios = new ArrayList<>();
-
- // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
- // additional items.
- aspectRatios.add(ASPECT_RATIO_4_3);
- aspectRatios.add(ASPECT_RATIO_16_9);
-
- // Tries to find the aspect ratio which the target size belongs to.
- for (Size size : resolutionCandidateList) {
- Rational newRatio = new Rational(size.getWidth(), size.getHeight());
- boolean aspectRatioFound = aspectRatios.contains(newRatio);
-
- // The checking size might be a mod16 size which can be mapped to an existing aspect
- // ratio group.
- if (!aspectRatioFound) {
- boolean hasMatchingAspectRatio = false;
- for (Rational aspectRatio : aspectRatios) {
- if (hasMatchingAspectRatio(size, aspectRatio)) {
- hasMatchingAspectRatio = true;
- break;
- }
- }
- if (!hasMatchingAspectRatio) {
- aspectRatios.add(newRatio);
- }
- }
- }
-
- return aspectRatios;
- }
-
- /**
- * Removes unnecessary sizes by target size.
- *
- * <p>If the target resolution is set, a size that is equal to or closest to the target
- * resolution will be selected. If the list includes more than one size equal to or larger
- * than the target resolution, only one closest size needs to be kept. The other larger sizes
- * can be removed so that they won't be selected to use.
- *
- * @param supportedSizesList The list should have been sorted in descending order.
- * @param miniBoundingSize The target size used to remove unnecessary sizes.
- */
- static void removeSupportedSizesByMiniBoundingSize(List<Size> supportedSizesList,
- Size miniBoundingSize) {
- if (supportedSizesList == null || supportedSizesList.isEmpty()) {
- return;
- }
-
- int indexBigEnough = -1;
- List<Size> removeSizes = new ArrayList<>();
-
- // Get the index of the item that is equal to or closest to the target size.
- for (int i = 0; i < supportedSizesList.size(); i++) {
- Size outputSize = supportedSizesList.get(i);
- if (outputSize.getWidth() >= miniBoundingSize.getWidth()
- && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
- // New big enough item closer to the target size is found. Adding the previous
- // one into the sizes list that will be removed.
- if (indexBigEnough >= 0) {
- removeSizes.add(supportedSizesList.get(indexBigEnough));
- }
-
- indexBigEnough = i;
- } else {
- // If duplicated miniBoundingSize items exist in the list, the size will be added
- // into the removeSizes list. Removes it from the removeSizes list to keep the
- // miniBoundingSize items in the final result list.
- if (indexBigEnough >= 0) {
- removeSizes.remove(supportedSizesList.get(indexBigEnough));
- }
- break;
- }
- }
-
- // Remove the unnecessary items that are larger than the item closest to the target size.
- supportedSizesList.removeAll(removeSizes);
- }
-
- static boolean isSensorLandscapeResolution(
- @NonNull CameraCharacteristicsCompat characteristicsCompat) {
- Size pixelArraySize =
- characteristicsCompat.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
-
- // Make the default value is true since usually the sensor resolution is landscape.
- return pixelArraySize == null || pixelArraySize.getWidth() >= pixelArraySize.getHeight();
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // The above functions can be reused by the legacy resolution selection logic. The can be
- // changed as private function after the legacy resolution API is completely removed.
- //////////////////////////////////////////////////////////////////////////////////////////
-
- @RequiresApi(23)
- private static class Api23Impl {
- private Api23Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static Size[] getHighResolutionOutputSizes(StreamConfigurationMap streamConfigurationMap,
- int format) {
- return streamConfigurationMap.getHighResolutionOutputSizes(format);
- }
- }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 5b19fe1..628d8be 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -18,38 +18,19 @@
import static android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.flipSizeByRotation;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getResolutionListGroupingAspectRatioKeys;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getTargetSizeByResolutionSelector;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.groupSizesByAspectRatio;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.isSensorLandscapeResolution;
-import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.removeSupportedSizesByMiniBoundingSize;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
-import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
-import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
-import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_ZERO;
-import static androidx.camera.core.internal.utils.SizeUtil.getArea;
import android.content.Context;
-import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
-import android.util.Pair;
import android.util.Range;
-import android.util.Rational;
import android.util.Size;
-import android.view.Surface;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
@@ -57,20 +38,14 @@
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
-import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
-import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraUnavailableException;
-import androidx.camera.core.Logger;
-import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceSizeDefinition;
import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.utils.AspectRatioUtil;
import androidx.camera.core.impl.utils.CompareSizesByArea;
import androidx.core.util.Preconditions;
@@ -101,19 +76,12 @@
private final ExtraSupportedSurfaceCombinationsContainer
mExtraSupportedSurfaceCombinationsContainer;
private final int mHardwareLevel;
- private final boolean mIsSensorLandscapeResolution;
private boolean mIsRawSupported = false;
private boolean mIsBurstCaptureSupported = false;
@VisibleForTesting
SurfaceSizeDefinition mSurfaceSizeDefinition;
- private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
@NonNull
private final DisplayInfoManager mDisplayInfoManager;
- private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
- private final Size mActiveArraySize;
- private final int mSensorOrientation;
- private final int mLensFacing;
- private final SupportedOutputSizesCollector mSupportedOutputSizesCollector;
SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
@NonNull CameraManagerCompat cameraManagerCompat,
@@ -131,7 +99,6 @@
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
mHardwareLevel = keyValue != null ? keyValue
: CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
- mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
} catch (CameraAccessExceptionCompat e) {
throw CameraUnavailableExceptionHelper.createFrom(e);
}
@@ -150,21 +117,12 @@
}
}
- Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
- mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
-
- mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
- mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
-
generateSupportedCombinationList();
if (context.getPackageManager().hasSystemFeature(FEATURE_CAMERA_CONCURRENT)) {
generateConcurrentSupportedCombinationList();
}
generateSurfaceSizeDefinition();
checkCustomization();
-
- mSupportedOutputSizesCollector = new SupportedOutputSizesCollector(mCameraId,
- mCharacteristics, mDisplayInfoManager);
}
String getCameraId() {
@@ -284,9 +242,11 @@
/**
* Finds the suggested stream specifications of the newly added UseCaseConfig.
*
- * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
- * @param attachedSurfaces the existing surfaces.
- * @param newUseCaseConfigs newly added UseCaseConfig.
+ * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise
+ * false.
+ * @param attachedSurfaces the existing surfaces.
+ * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output
+ * sizes map.
* @return the suggested stream specifications, which is a mapping from UseCaseConfig to the
* suggested stream specification.
* @throws IllegalArgumentException if the suggested solution for newUseCaseConfigs cannot be
@@ -297,7 +257,7 @@
Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecifications(
boolean isConcurrentCameraModeOn,
@NonNull List<AttachedSurfaceInfo> attachedSurfaces,
- @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
+ @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
// Refresh Preview Size based on current display configurations.
refreshPreviewSize();
List<SurfaceConfig> surfaceConfigs = new ArrayList<>();
@@ -305,6 +265,8 @@
surfaceConfigs.add(attachedSurface.getSurfaceConfig());
}
+ List<UseCaseConfig<?>> newUseCaseConfigs = new ArrayList<>(
+ newUseCaseConfigsSupportedSizeMap.keySet());
// Use the small size (640x480) for new use cases to check whether there is any possible
// supported combination first
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
@@ -347,7 +309,8 @@
// Collect supported output sizes for all use cases
for (Integer index : useCasesPriorityOrder) {
- List<Size> supportedOutputSizes = getSupportedOutputSizes(newUseCaseConfigs.get(index));
+ List<Size> supportedOutputSizes = newUseCaseConfigsSupportedSizeMap.get(
+ newUseCaseConfigs.get(index));
supportedOutputSizesList.add(supportedOutputSizes);
}
@@ -455,48 +418,6 @@
return suggestedStreamSpecMap;
}
- /**
- * Returns the target aspect ratio rational value.
- *
- * @param imageOutputConfig the image output config of the use case.
- * @param resolutionCandidateList the resolution candidate list which will be used to
- * determine the aspect ratio by target size when target
- * aspect ratio setting is not set.
- */
- private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig,
- @NonNull List<Size> resolutionCandidateList) {
- Rational outputRatio = null;
-
- if (imageOutputConfig.hasTargetAspectRatio()) {
- @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
- switch (aspectRatio) {
- case AspectRatio.RATIO_4_3:
- outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3
- : ASPECT_RATIO_3_4;
- break;
- case AspectRatio.RATIO_16_9:
- outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9
- : ASPECT_RATIO_9_16;
- break;
- case AspectRatio.RATIO_DEFAULT:
- break;
- default:
- Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
- }
- } else {
- // The legacy resolution API will use the aspect ratio of the target size to
- // be the fallback target aspect ratio value when the use case has no target
- // aspect ratio setting.
- Size targetSize = getTargetSize(imageOutputConfig);
- if (targetSize != null) {
- outputRatio = getAspectRatioGroupKeyOfTargetSize(targetSize,
- resolutionCandidateList);
- }
- }
-
- return outputRatio;
- }
-
private List<Integer> getUseCasesPriorityOrder(List<UseCaseConfig<?>> newUseCaseConfigs) {
List<Integer> priorityOrder = new ArrayList<>();
@@ -531,180 +452,6 @@
return priorityOrder;
}
- @NonNull
- @VisibleForTesting
- List<Size> getSupportedOutputSizes(@NonNull UseCaseConfig<?> config) {
- int imageFormat = config.getInputFormat();
- ImageOutputConfig imageOutputConfig = (ImageOutputConfig) config;
-
- List<Size> customOrderedResolutions = imageOutputConfig.getCustomOrderedResolutions(null);
- if (customOrderedResolutions != null) {
- return customOrderedResolutions;
- }
- ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
- if (resolutionSelector != null) {
- Size miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
-
- if (resolutionSelector.getPreferredResolution() != null) {
- miniBoundingSize = getTargetSizeByResolutionSelector(resolutionSelector,
- mDisplayInfoManager.getMaxSizeDisplay().getRotation(), mSensorOrientation,
- mLensFacing);
- }
-
- return mSupportedOutputSizesCollector.getSupportedOutputSizes(resolutionSelector,
- imageFormat, miniBoundingSize, config.isHigResolutionDisabled(false),
- getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig));
- }
-
- Size[] outputSizes = getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig);
- if (outputSizes == null) {
- outputSizes = getAllOutputSizesByFormat(imageFormat);
- }
-
- // Sort the result sizes. The Comparator result must be reversed to have a descending
- // order result.
- Arrays.sort(outputSizes, new CompareSizesByArea(true));
-
- List<Size> outputSizeCandidates = new ArrayList<>();
- Size maxSize = imageOutputConfig.getMaxResolution(null);
- Size maxOutputSizeByFormat = getMaxOutputSizeByFormat(imageFormat);
-
- // Set maxSize as the max resolution setting or the max supported output size for the
- // image format, whichever is smaller.
- if (maxSize == null || getArea(maxOutputSizeByFormat) < getArea(maxSize)) {
- maxSize = maxOutputSizeByFormat;
- }
-
- // Sort the output sizes. The Comparator result must be reversed to have a descending order
- // result.
- Arrays.sort(outputSizes, new CompareSizesByArea(true));
-
- Size targetSize = getTargetSize(imageOutputConfig);
- Size minSize = RESOLUTION_VGA;
- int defaultSizeArea = getArea(RESOLUTION_VGA);
- int maxSizeArea = getArea(maxSize);
- // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
- // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
- // TARGET_RESOLUTION).
- if (maxSizeArea < defaultSizeArea) {
- minSize = RESOLUTION_ZERO;
- } else if (targetSize != null && getArea(targetSize) < defaultSizeArea) {
- minSize = targetSize;
- }
-
- // Filter out the ones that exceed the maximum size and the minimum size. The output
- // sizes candidates list won't have duplicated items.
- for (Size outputSize : outputSizes) {
- if (getArea(outputSize) <= getArea(maxSize) && getArea(outputSize) >= getArea(minSize)
- && !outputSizeCandidates.contains(outputSize)) {
- outputSizeCandidates.add(outputSize);
- }
- }
-
- if (outputSizeCandidates.isEmpty()) {
- throw new IllegalArgumentException(
- "Can not get supported output size under supported maximum for the format: "
- + imageFormat);
- }
-
- Rational aspectRatio = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates);
-
- // Check the default resolution if the target resolution is not set
- targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;
-
- List<Size> supportedResolutions = new ArrayList<>();
- Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
-
- if (aspectRatio == null) {
- // If no target aspect ratio is set, all sizes can be added to the result list
- // directly. No need to sort again since the source list has been sorted previously.
- supportedResolutions.addAll(outputSizeCandidates);
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- if (targetSize != null) {
- removeSupportedSizesByMiniBoundingSize(supportedResolutions, targetSize);
- }
- } else {
- // Rearrange the supported size to put the ones with the same aspect ratio in the front
- // of the list and put others in the end from large to small. Some low end devices may
- // not able to get an supported resolution that match the preferred aspect ratio.
-
- // Group output sizes by aspect ratio.
- aspectRatioSizeListMap = groupSizesByAspectRatio(outputSizeCandidates);
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- if (targetSize != null) {
- // Remove unnecessary larger sizes from each aspect ratio size list
- for (Rational key : aspectRatioSizeListMap.keySet()) {
- removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
- targetSize);
- }
- }
-
- // Sort the aspect ratio key set by the target aspect ratio.
- List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
- Rational fullFovRatio = mActiveArraySize != null ? new Rational(
- mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
- Collections.sort(aspectRatios,
- new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio, fullFovRatio));
-
- // Put available sizes into final result list by aspect ratio distance to target ratio.
- for (Rational rational : aspectRatios) {
- for (Size size : aspectRatioSizeListMap.get(rational)) {
- // A size may exist in multiple groups in mod16 condition. Keep only one in
- // the final list.
- if (!supportedResolutions.contains(size)) {
- supportedResolutions.add(size);
- }
- }
- }
- }
-
- supportedResolutions = mResolutionCorrector.insertOrPrioritize(
- SurfaceConfig.getConfigType(config.getInputFormat()),
- supportedResolutions);
-
- return supportedResolutions;
- }
-
- @Nullable
- private Size getTargetSize(@NonNull ImageOutputConfig imageOutputConfig) {
- int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
- // Calibrate targetSize by the target rotation value.
- Size targetSize = imageOutputConfig.getTargetResolution(null);
- targetSize = flipSizeByRotation(targetSize, targetRotation, mLensFacing,
- mSensorOrientation);
- return targetSize;
- }
-
- /**
- * Returns the aspect ratio group key of the target size when grouping the input resolution
- * candidate list.
- *
- * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
- * also need to consider the mod 16 factor to find which aspect ratio of group the target size
- * might be put in. So that sizes of the group will be selected to use in the highest priority.
- */
- @Nullable
- private Rational getAspectRatioGroupKeyOfTargetSize(@Nullable Size targetSize,
- @NonNull List<Size> resolutionCandidateList) {
- if (targetSize == null) {
- return null;
- }
-
- List<Rational> aspectRatios = getResolutionListGroupingAspectRatioKeys(
- resolutionCandidateList);
-
- for (Rational aspectRatio: aspectRatios) {
- if (hasMatchingAspectRatio(targetSize, aspectRatio)) {
- return aspectRatio;
- }
- }
-
- return new Rational(targetSize.getWidth(), targetSize.getHeight());
- }
-
private List<List<Size>> getAllPossibleSizeArrangements(
List<List<Size>> supportedOutputSizesList) {
int totalArrangementsCount = 1;
@@ -758,50 +505,6 @@
return allPossibleSizeArrangements;
}
- @Nullable
- private Size[] getCustomizedSupportSizesFromConfig(int imageFormat,
- @NonNull ImageOutputConfig config) {
- Size[] outputSizes = null;
-
- // Try to retrieve customized supported resolutions from config.
- List<Pair<Integer, Size[]>> formatResolutionsPairList =
- config.getSupportedResolutions(null);
-
- if (formatResolutionsPairList != null) {
- for (Pair<Integer, Size[]> formatResolutionPair : formatResolutionsPairList) {
- if (formatResolutionPair.first == imageFormat) {
- outputSizes = formatResolutionPair.second;
- break;
- }
- }
- }
-
- return outputSizes;
- }
-
- @NonNull
- private Size[] getAllOutputSizesByFormat(int imageFormat) {
- Size[] outputs = mOutputSizesCache.get(imageFormat);
- if (outputs == null) {
- outputs = doGetAllOutputSizesByFormat(imageFormat);
- mOutputSizesCache.put(imageFormat, outputs);
- }
-
- return outputs;
- }
-
- @NonNull
- private Size[] doGetAllOutputSizesByFormat(int imageFormat) {
- StreamConfigurationMapCompat mapCompat = mCharacteristics.getStreamConfigurationMapCompat();
- Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
- if (outputSizes == null) {
- throw new IllegalArgumentException(
- "Can not get supported output size for the format: " + imageFormat);
- }
-
- return outputSizes;
- }
-
/**
* Get max supported output size for specific image format
*
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
index f22cb28..8505fdc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
@@ -109,7 +109,9 @@
public void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
if (cameraOperatingMode != mCameraOperatingMode) {
for (ConcurrentCameraModeListener listener : mConcurrentCameraModeListeners) {
- listener.notifyConcurrentCameraModeUpdated(cameraOperatingMode);
+ listener.onCameraOperatingModeUpdated(
+ mCameraOperatingMode,
+ cameraOperatingMode);
}
}
// Clear the cached camera selectors if concurrent mode is off.
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
index 12ffb92..4ea8c60 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
@@ -26,6 +26,7 @@
import androidx.camera.core.impl.CameraStateRegistry
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCoordinator
import androidx.lifecycle.Observer
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -43,9 +44,11 @@
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
+ private val cameraCoordinator = FakeCameraCoordinator()
+
/** Wrapper method that initializes the required test parameters, then runs the test's body. */
private fun runTest(body: (CameraStateMachine, StateObserver) -> Unit) {
- val registry = CameraStateRegistry(1)
+ val registry = CameraStateRegistry(cameraCoordinator, 1)
val stateMachine = CameraStateMachine(registry)
val stateObserver = StateObserver()
stateMachine.stateLiveData.observeForever(stateObserver)
@@ -113,6 +116,20 @@
}
@Test
+ fun shouldNotEmitNewState_whenInConfiguredState() =
+ runTest { stateMachine, stateObserver ->
+ stateMachine.updateState(CameraInternal.State.OPENING, null)
+ stateMachine.updateState(CameraInternal.State.OPEN, null)
+ stateMachine.updateState(CameraInternal.State.CONFIGURED, null)
+
+ stateObserver
+ .assertHasState(CameraState.create(Type.CLOSED))
+ .assertHasState(CameraState.create(Type.OPENING))
+ .assertHasState(CameraState.create(Type.OPEN))
+ .assertHasNoMoreStates()
+ }
+
+ @Test
fun shouldEmitNewState_whenErrorChanges() =
runTest { stateMachine, stateObserver ->
stateMachine.updateState(
@@ -143,12 +160,12 @@
@Test
fun shouldEmitOpeningState_whenCameraIsOpening_whileAnotherIsClosing() {
- val registry = CameraStateRegistry(1)
+ val registry = CameraStateRegistry(cameraCoordinator, 1)
val stateMachine = CameraStateMachine(registry)
// Create, open then start closing first camera
val camera1 = FakeCamera()
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), {})
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(), {}, {})
registry.tryOpenCamera(camera1)
registry.markCameraState(camera1, CameraInternal.State.OPEN)
registry.markCameraState(camera1, CameraInternal.State.CLOSING)
@@ -156,7 +173,7 @@
// Create and try to open second camera. Since the first camera is still closing, its
// internal state will move to PENDING_OPEN
val camera2 = FakeCamera()
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), {})
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(), {}, {})
registry.tryOpenCamera(camera2)
registry.markCameraState(camera2, CameraInternal.State.PENDING_OPEN)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
deleted file mode 100644
index 7761531..0000000
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
+++ /dev/null
@@ -1,1386 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.internal
-
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CameraManager
-import android.hardware.camera2.params.StreamConfigurationMap
-import android.media.MediaRecorder
-import android.os.Build
-import android.util.Pair
-import android.util.Size
-import android.view.Surface
-import android.view.WindowManager
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
-import androidx.camera.camera2.internal.compat.CameraManagerCompat
-import androidx.camera.core.AspectRatio
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraX
-import androidx.camera.core.CameraXConfig
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.Preview
-import androidx.camera.core.ResolutionSelector
-import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.ImageOutputConfig
-import androidx.camera.core.impl.SizeCoordinate
-import androidx.camera.core.impl.UseCaseConfigFactory
-import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.Configs
-import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.camera.testing.fakes.FakeCameraInfoInternal
-import androidx.camera.testing.fakes.FakeUseCaseConfig
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.TimeUnit
-import org.junit.After
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito
-import org.robolectric.ParameterizedRobolectricTestRunner
-import org.robolectric.Shadows
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowCameraCharacteristics
-import org.robolectric.shadows.ShadowCameraManager
-
-private const val FAKE_USE_CASE = 0
-private const val PREVIEW_USE_CASE = 1
-private const val IMAGE_CAPTURE_USE_CASE = 2
-private const val IMAGE_ANALYSIS_USE_CASE = 3
-private const val UNKNOWN_ASPECT_RATIO = -1
-private const val DEFAULT_CAMERA_ID = "0"
-private const val SENSOR_ORIENTATION_0 = 0
-private const val SENSOR_ORIENTATION_90 = 90
-private val LANDSCAPE_PIXEL_ARRAY_SIZE = Size(4032, 3024)
-private val PORTRAIT_PIXEL_ARRAY_SIZE = Size(3024, 4032)
-private val DISPLAY_SIZE = Size(720, 1280)
-private val DEFAULT_SUPPORTED_SIZES = arrayOf(
- Size(4032, 3024), // 4:3
- Size(3840, 2160), // 16:9
- Size(1920, 1440), // 4:3
- Size(1920, 1080), // 16:9
- Size(1280, 960), // 4:3
- Size(1280, 720), // 16:9
- Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
- Size(800, 450), // 16:9
- Size(640, 480), // 4:3
- Size(320, 240), // 4:3
- Size(320, 180), // 16:9
- Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
-)
-
-/** Robolectric test for [SupportedOutputSizesCollector] class */
-@RunWith(ParameterizedRobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class SupportedOutputSizesCollectorTest(
- private val sizeCoordinate: SizeCoordinate
-) {
- private val mockCamcorderProfileHelper = Mockito.mock(CamcorderProfileHelper::class.java)
- private lateinit var cameraManagerCompat: CameraManagerCompat
- private lateinit var cameraCharacteristicsCompat: CameraCharacteristicsCompat
- private lateinit var displayInfoManager: DisplayInfoManager
- private val context = ApplicationProvider.getApplicationContext<Context>()
- private var cameraFactory: FakeCameraFactory? = null
- private var useCaseConfigFactory: UseCaseConfigFactory? = null
-
- @Suppress("DEPRECATION") // defaultDisplay
- @Before
- fun setUp() {
- DisplayInfoManager.releaseInstance()
- val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(DISPLAY_SIZE.width)
- Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(DISPLAY_SIZE.height)
- Mockito.`when`(
- mockCamcorderProfileHelper.hasProfile(
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.anyInt()
- )
- ).thenReturn(true)
-
- displayInfoManager = DisplayInfoManager.getInstance(context)
- }
-
- @After
- fun tearDown() {
- CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio4x3() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_4_3
- )
-
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
-
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio16x9_inLegacyDevice() {
- setupCameraAndInitCameraX()
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList: List<Size> = if (Build.VERSION.SDK_INT == 21) {
- listOf(
- // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Non-matched items have been removed by OutputSizesCorrector due to
- // TargetAspectRatio quirk.
- )
- } else {
- listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- }
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_preferredResolution1920x1080_InLimitedDevice() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1920, 1080),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
- // after the preferred resolution.
- val expectedList =
- listOf(
- // Matched preferred resolution size will be put in first priority.
- Size(1920, 1080),
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_preferredResolution1920x1080_InLegacyDevice() {
- setupCameraAndInitCameraX()
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1920, 1080),
- sizeCoordinate = sizeCoordinate
- )
-
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
- // after the preferred resolution.
- val expectedList = if (Build.VERSION.SDK_INT == 21) {
- listOf(
- // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Non-matched items have been removed by OutputSizesCorrector due to
- // TargetAspectRatio quirk.
- )
- } else {
- // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest
- // priority just after the preferred resolution size.
- listOf(
- // Matched default preferred resolution size will be put in first priority.
- Size(1920, 1080),
- // Matched preferred AspectRatio items, sorted by area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched preferred default AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- )
- }
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- @Suppress("DEPRECATION") /* defaultDisplay */
- fun getSupportedOutputSizes_smallDisplay_withMaxResolution1920x1080() {
- // Sets up small display.
- val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(240)
- Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(320)
- displayInfoManager = DisplayInfoManager.getInstance(context)
-
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
-
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9,
- maxResolution = Size(1920, 1080)
- )
- // Max resolution setting will remove sizes larger than 1920x1080. The auto-resolution
- // mechanism will try to select the sizes which aspect ratio is nearest to the aspect ratio
- // of target resolution in priority. Therefore, sizes of aspect ratio 16/9 will be in front
- // of the returned sizes list and the list is sorted in descending order. Other items will
- // be put in the following that are sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
-
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_preferredResolution1800x1440NearTo4x3() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1800, 1440),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList =
- listOf(
- // No matched preferred resolution size found.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_preferredResolution1280x600NearTo16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1280, 600),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // No matched preferred resolution size found.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_maxResolution1280x720() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- maxResolution = Size(1280, 720)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_maxResolution720x1280() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- maxResolution = Size(720, 1280)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- defaultResolution = Size(1280, 720)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_defaultResolution1280x720_preferredResolution1920x1080() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- defaultResolution = Size(1280, 720),
- preferredResolution = Size(1920, 1080),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred resolution size will be put in first priority.
- Size(1920, 1080),
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1920, 1080),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // No matched preferred resolution size found.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(320, 240),
- sizeCoordinate = sizeCoordinate,
- maxResolution = Size(320, 180)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(Size(320, 180), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxSizeSmallerThanBigPreferredResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(3840, 2160),
- sizeCoordinate = sizeCoordinate,
- maxResolution = Size(1920, 1080)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // No matched preferred resolution size found after filtering by max resolution setting.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenNoSizeBetweenMaxSizeAndPreferredResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(320, 190),
- sizeCoordinate = sizeCoordinate,
- maxResolution = Size(320, 200)
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(Size(320, 180), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenPreferredResolutionSmallerThanAnySize() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(192, 144),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(Size(320, 240), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
- setupCameraAndInitCameraX(
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- maxResolution = Size(192, 144)
- )
- // All sizes will be filtered out by the max resolution 192x144 setting and an
- // IllegalArgumentException will be thrown.
- Assert.assertThrows(IllegalArgumentException::class.java) {
- getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- }
- }
-
- @Test
- fun getSupportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(296, 144),
- Size(256, 144)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(185, 90),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // No matched preferred resolution size found.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(320, 240),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(256, 144),
- Size(296, 144)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
- Size(768, 432),
- Size(640, 480),
- Size(640, 360),
- Size(480, 360),
- Size(384, 288)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredResolution = Size(1080, 2016),
- sizeCoordinate = sizeCoordinate
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // No matched preferred resolution size found.
- // Matched default preferred AspectRatio items, sorted by area size.
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480),
- Size(480, 360),
- Size(384, 288),
- // Mismatched default preferred AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(864, 480),
- Size(768, 432),
- Size(640, 360)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
- // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
- // phone device which majorly supports portrait output sizes.
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0,
- pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
- supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(960, 540),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(720, 1280),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(360, 480),
- Size(640, 480),
- Size(960, 540)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
- // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
- // tablet device which majorly supports portrait output sizes.
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_90,
- pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
- supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(960, 540),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- // Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in front of
- // the returned sizes list and the list is sorted in descending order. Other items will be
- // put in the following that are sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(720, 1280),
- // Mismatched preferred AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(360, 480),
- Size(640, 480),
- Size(960, 540)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTablet_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0,
- pixelArraySize = LANDSCAPE_PIXEL_ARRAY_SIZE
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0, supportedSizes = arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(540, 960),
- Size(640, 480),
- Size(480, 640),
- Size(480, 360)
- )
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480),
- Size(480, 360),
- Size(480, 640),
- Size(540, 960)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun getSupportedOutputSizes_whenHighResolutionIsEnabled_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- capabilities = intArrayOf(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
- ),
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
-
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9,
- highResolutionEnabled = true
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(8000, 4500),
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(8000, 6000),
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun highResolutionCanNotBeSelected_whenHighResolutionForceDisabled() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- capabilities = intArrayOf(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
- ),
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedOutputSizesCollector = SupportedOutputSizesCollector(
- DEFAULT_CAMERA_ID,
- cameraCharacteristicsCompat,
- displayInfoManager
- )
-
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9,
- highResolutionEnabled = true,
- highResolutionForceDisabled = true
- )
- val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
- val expectedList = listOf(
- // Matched preferred AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(320, 180),
- Size(256, 144),
- // Mismatched preferred AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- Size(320, 240)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- /**
- * Sets up camera according to the specified settings and initialize [CameraX].
- *
- * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
- * @param hardwareLevel the hardware level of the camera. Default value is
- * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
- * @param sensorOrientation the sensor orientation of the camera. Default value is
- * [SENSOR_ORIENTATION_90].
- * @param pixelArraySize the active pixel array size of the camera. Default value is
- * [LANDSCAPE_PIXEL_ARRAY_SIZE].
- * @param supportedSizes the supported sizes of the camera. Default value is
- * [DEFAULT_SUPPORTED_SIZES].
- * @param capabilities the capabilities of the camera. Default value is null.
- */
- private fun setupCameraAndInitCameraX(
- cameraId: String = DEFAULT_CAMERA_ID,
- hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
- sensorOrientation: Int = SENSOR_ORIENTATION_90,
- pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
- supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
- supportedHighResolutionSizes: Array<Size>? = null,
- capabilities: IntArray? = null
- ) {
- setupCamera(
- cameraId,
- hardwareLevel,
- sensorOrientation,
- pixelArraySize,
- supportedSizes,
- supportedHighResolutionSizes,
- capabilities
- )
-
- @CameraSelector.LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
- CameraCharacteristics.LENS_FACING_BACK
- )
- cameraManagerCompat = CameraManagerCompat.from(context)
- val cameraInfo = FakeCameraInfoInternal(
- cameraId,
- sensorOrientation,
- CameraCharacteristics.LENS_FACING_BACK
- )
-
- cameraFactory = FakeCameraFactory().apply {
- insertCamera(lensFacingEnum, cameraId) {
- FakeCamera(cameraId, null, cameraInfo)
- }
- }
-
- cameraCharacteristicsCompat = cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
-
- initCameraX()
- }
-
- /**
- * Initializes the [CameraX].
- */
- private fun initCameraX() {
- val surfaceManagerProvider =
- CameraDeviceSurfaceManager.Provider { context, _, availableCameraIds ->
- Camera2DeviceSurfaceManager(
- context,
- mockCamcorderProfileHelper,
- CameraManagerCompat.from([email protected]),
- availableCameraIds
- )
- }
- val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
- .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
- .setCameraFactoryProvider { _, _, _ -> cameraFactory!! }
- .build()
- val cameraX: CameraX = try {
- CameraXUtil.getOrCreateInstance(context) { cameraXConfig }.get()
- } catch (e: ExecutionException) {
- throw IllegalStateException("Unable to initialize CameraX for test.")
- } catch (e: InterruptedException) {
- throw IllegalStateException("Unable to initialize CameraX for test.")
- }
- useCaseConfigFactory = cameraX.defaultConfigFactory
- }
-
- /**
- * Gets the supported output sizes by the converted ResolutionSelector use case config which
- * will also be converted when a use case is bound to the lifecycle.
- */
- private fun getSupportedOutputSizes(
- supportedOutputSizesCollector: SupportedOutputSizesCollector,
- useCase: UseCase,
- cameraId: String = DEFAULT_CAMERA_ID,
- sensorOrientation: Int = SENSOR_ORIENTATION_90,
- useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
- ): List<Size?> {
- // Converts the use case config to new ResolutionSelector config
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- listOf(useCase),
- useCaseConfigFactory
- )
-
- val useCaseConfig = useCaseToConfigMap[useCase]!!
- val resolutionSelector = (useCaseConfig as ImageOutputConfig).resolutionSelector
- val imageFormat = useCaseConfig.inputFormat
- val isHighResolutionDisabled = useCaseConfig.isHigResolutionDisabled(false)
- val customizedSupportSizes = getCustomizedSupportSizesFromConfig(imageFormat, useCaseConfig)
- val miniBoundingSize = SupportedOutputSizesCollector.getTargetSizeByResolutionSelector(
- resolutionSelector,
- Surface.ROTATION_0,
- sensorOrientation,
- CameraCharacteristics.LENS_FACING_BACK
- ) ?: useCaseConfig.getDefaultResolution(null)
-
- return supportedOutputSizesCollector.getSupportedOutputSizes(
- resolutionSelector,
- imageFormat,
- miniBoundingSize,
- isHighResolutionDisabled,
- customizedSupportSizes
- )
- }
-
- companion object {
-
- /**
- * Sets up camera according to the specified settings.
- *
- * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
- * @param hardwareLevel the hardware level of the camera. Default value is
- * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
- * @param sensorOrientation the sensor orientation of the camera. Default value is
- * [SENSOR_ORIENTATION_90].
- * @param pixelArraySize the active pixel array size of the camera. Default value is
- * [LANDSCAPE_PIXEL_ARRAY_SIZE].
- * @param supportedSizes the supported sizes of the camera. Default value is
- * [DEFAULT_SUPPORTED_SIZES].
- * @param capabilities the capabilities of the camera. Default value is null.
- */
- @JvmStatic
- fun setupCamera(
- cameraId: String = DEFAULT_CAMERA_ID,
- hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
- sensorOrientation: Int = SENSOR_ORIENTATION_90,
- pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
- supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
- supportedHighResolutionSizes: Array<Size>? = null,
- capabilities: IntArray? = null
- ) {
- val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
- // Sets up the supported sizes
- Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt()))
- .thenReturn(supportedSizes)
- // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
- // output sizes need to be retrieved via SurfaceTexture.class.
- Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java))
- .thenReturn(supportedSizes)
- // This is setup for the test to determine RECORD size from StreamConfigurationMap
- Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java))
- .thenReturn(supportedSizes)
-
- // setup to return different minimum frame durations depending on resolution
- // minimum frame durations were designated only for the purpose of testing
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(4032, 3024))))
- .thenReturn(50000000L) // 20 fps, size maximum
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(3840, 2160))))
- .thenReturn(40000000L) // 25, size record
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1920, 1440))))
- .thenReturn(30000000L) // 30
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1920, 1080))))
- .thenReturn(28000000L) // 35
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1280, 960))))
- .thenReturn(25000000L) // 40
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(1280, 720))))
- .thenReturn(22000000L) // 45, size preview/display
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(960, 544))))
- .thenReturn(20000000L) // 50
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(800, 450))))
- .thenReturn(16666000L) // 60fps
-
- Mockito.`when`(it.getOutputMinFrameDuration(anyInt(), eq(Size(640, 480))))
- .thenReturn(16666000L) // 60fps
-
- // Sets up the supported high resolution sizes
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Mockito.`when`(it.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
- .thenReturn(supportedHighResolutionSizes)
- }
- }
-
- val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
- Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
- set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
- set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
- set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
- set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
- set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
- capabilities?.let {
- set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
- }
- }
-
- val cameraManager = ApplicationProvider.getApplicationContext<Context>()
- .getSystemService(Context.CAMERA_SERVICE) as CameraManager
- (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
- .addCamera(cameraId, characteristics)
- }
-
- /**
- * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
- * FakeUseCase by the legacy or new ResolutionSelector API according to the specified settings.
- *
- * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
- * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
- * @param preferredAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
- * and no target aspect ratio will be set to the created use case.
- * @param preferredResolution the preferred resolution setting which should be specified in the
- * camera sensor coordinate. The resolution will be transformed to set via
- * [ResolutionSelector.Builder.setPreferredResolutionByViewSize] if size coordinate is
- * [SizeCoordinate.ANDROID_VIEW]. Default is null.
- * @param maxResolution the max resolution setting. Default is null.
- * @param highResolutionEnabled the high resolution setting, Default is false.
- * @param highResolutionForceDisabled the high resolution force disabled setting, Default
- * is false. This will be set in the use case config to force disable high resolution.
- * @param defaultResolution the default resolution setting. Default is null.
- * @param supportedResolutions the customized supported resolutions. Default is null.
- */
- @JvmStatic
- fun createUseCaseByResolutionSelector(
- useCaseType: Int,
- preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
- preferredResolution: Size? = null,
- sizeCoordinate: SizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
- maxResolution: Size? = null,
- highResolutionEnabled: Boolean = false,
- highResolutionForceDisabled: Boolean = false,
- defaultResolution: Size? = null,
- supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
- customOrderedResolutions: List<Size>? = null,
- ): UseCase {
- val builder = when (useCaseType) {
- PREVIEW_USE_CASE -> Preview.Builder()
- IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
- IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
- else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
- }
-
- val resolutionSelectorBuilder = ResolutionSelector.Builder()
-
- if (preferredAspectRatio != UNKNOWN_ASPECT_RATIO) {
- resolutionSelectorBuilder.setPreferredAspectRatio(preferredAspectRatio)
- }
-
- preferredResolution?.let {
- if (sizeCoordinate == SizeCoordinate.CAMERA_SENSOR) {
- resolutionSelectorBuilder.setPreferredResolution(it)
- } else {
- val flippedResolution = Size(
- /* width= */ it.height,
- /* height= */ it.width
- )
- resolutionSelectorBuilder.setPreferredResolutionByViewSize(flippedResolution)
- }
- }
-
- maxResolution?.let { resolutionSelectorBuilder.setMaxResolution(it) }
- resolutionSelectorBuilder.setHighResolutionEnabled(highResolutionEnabled)
-
- builder.setResolutionSelector(resolutionSelectorBuilder.build())
- builder.setHighResolutionDisabled(highResolutionForceDisabled)
-
- defaultResolution?.let { builder.setDefaultResolution(it) }
- supportedResolutions?.let { builder.setSupportedResolutions(it) }
- customOrderedResolutions?.let { builder.setCustomOrderedResolutions(it) }
- return builder.build()
- }
-
- @JvmStatic
- fun getCustomizedSupportSizesFromConfig(
- imageFormat: Int,
- config: ImageOutputConfig
- ): Array<Size>? {
- var outputSizes: Array<Size>? = null
-
- // Try to retrieve customized supported resolutions from config.
- val formatResolutionsPairList = config.getSupportedResolutions(null)
- if (formatResolutionsPairList != null) {
- for (formatResolutionPair in formatResolutionsPairList) {
- if (formatResolutionPair.first == imageFormat) {
- outputSizes = formatResolutionPair.second
- break
- }
- }
- }
- return outputSizes
- }
-
- @JvmStatic
- @ParameterizedRobolectricTestRunner.Parameters(name = "sizeCoordinate = {0}")
- fun data() = listOf(
- SizeCoordinate.CAMERA_SENSOR,
- SizeCoordinate.ANDROID_VIEW
- )
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index 3ed7e19..7505ee1 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -35,7 +35,9 @@
import androidx.annotation.NonNull;
import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
import androidx.camera.core.CameraUnavailableException;
import androidx.camera.core.CameraXConfig;
@@ -45,8 +47,6 @@
import androidx.camera.testing.CameraXUtil;
import androidx.camera.testing.fakes.FakeCamera;
import androidx.camera.testing.fakes.FakeCameraFactory;
-import androidx.camera.testing.fakes.FakeUseCase;
-import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.test.core.app.ApplicationProvider;
import org.codehaus.plexus.util.ReflectionUtils;
@@ -152,12 +152,13 @@
// mSupportedSizes modified unexpectedly.
assertThat(Arrays.asList(mSupportedSizes)).containsAtLeastElementsIn(excludedSizes);
- // Make the fake use case have JPEG format since those sizes are excluded for JPEG format.
- final FakeUseCase useCase = new FakeUseCaseConfig.Builder()
- .setBufferFormat(ImageFormat.JPEG)
- .build();
- final List<Size> resultList = supportedSurfaceCombination.getSupportedOutputSizes(
- useCase.getCurrentConfig());
+ // These sizes should be excluded when retrieving output sizes via
+ // StreamConfigurationMapCompat#getOutputSizes()
+ CameraCharacteristicsCompat characteristicsCompat =
+ mCameraManagerCompat.getCameraCharacteristicsCompat(BACK_CAMERA_ID);
+ StreamConfigurationMapCompat mapCompat =
+ characteristicsCompat.getStreamConfigurationMapCompat();
+ final List<Size> resultList = Arrays.asList(mapCompat.getOutputSizes(ImageFormat.JPEG));
assertThat(resultList).containsNoneIn(excludedSizes);
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 51f2a79..ed4755d 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -1,4 +1,3 @@
-
/*
* Copyright 2022 The Android Open Source Project
*
@@ -20,78 +19,50 @@
import android.content.Context
import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
import android.graphics.ImageFormat
+import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.StreamConfigurationMap
import android.media.CamcorderProfile
import android.media.CamcorderProfile.QUALITY_1080P
import android.media.CamcorderProfile.QUALITY_2160P
import android.media.CamcorderProfile.QUALITY_480P
import android.media.CamcorderProfile.QUALITY_720P
+import android.media.MediaRecorder
import android.os.Build
-import android.util.Pair
import android.util.Range
-import android.util.Rational
import android.util.Size
-import android.view.Surface
import android.view.WindowManager
-import androidx.annotation.NonNull
import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.createUseCaseByResolutionSelector
-import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.setupCamera
-import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat
import androidx.camera.camera2.internal.compat.CameraManagerCompat
-import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector.LensFacing
-import androidx.camera.core.CameraUnavailableException
import androidx.camera.core.CameraX
import androidx.camera.core.CameraXConfig
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCase
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.MutableStateObservable
-import androidx.camera.core.impl.SizeCoordinate
-import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceConfig.ConfigSize
import androidx.camera.core.impl.SurfaceConfig.ConfigType
import androidx.camera.core.impl.UseCaseConfig
-import androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE
import androidx.camera.core.impl.UseCaseConfigFactory
-import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
-import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
-import androidx.camera.core.impl.utils.CompareSizesByArea
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1440P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.Configs
import androidx.camera.testing.EncoderProfilesUtil
-import androidx.camera.testing.SurfaceTextureProvider
-import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraFactory
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
import androidx.camera.testing.fakes.FakeUseCaseConfig
-import androidx.camera.video.FallbackStrategy
-import androidx.camera.video.MediaSpec
-import androidx.camera.video.Quality
-import androidx.camera.video.QualitySelector
-import androidx.camera.video.VideoCapture
-import androidx.camera.video.VideoOutput
-import androidx.camera.video.VideoOutput.SourceState
-import androidx.camera.video.VideoSpec
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import java.util.Arrays
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import org.codehaus.plexus.util.ReflectionUtils
@@ -103,30 +74,22 @@
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
-private const val FAKE_USE_CASE = 0
-private const val PREVIEW_USE_CASE = 1
-private const val IMAGE_CAPTURE_USE_CASE = 2
-private const val IMAGE_ANALYSIS_USE_CASE = 3
-private const val UNKNOWN_ROTATION = -1
-private const val UNKNOWN_ASPECT_RATIO = -1
private const val DEFAULT_CAMERA_ID = "0"
private const val EXTERNAL_CAMERA_ID = "0-external"
-private const val SENSOR_ORIENTATION_0 = 0
private const val SENSOR_ORIENTATION_90 = 90
-private val ASPECT_RATIO_16_9 = Rational(16, 9)
private val LANDSCAPE_PIXEL_ARRAY_SIZE = Size(4032, 3024)
-private val PORTRAIT_PIXEL_ARRAY_SIZE = Size(3024, 4032)
private val DISPLAY_SIZE = Size(720, 1280)
private val PREVIEW_SIZE = Size(1280, 720)
private val RECORD_SIZE = Size(3840, 2160)
private val MAXIMUM_SIZE = Size(4032, 3024)
private val LEGACY_VIDEO_MAXIMUM_SIZE = Size(1920, 1080)
-private val MOD16_SIZE = Size(960, 544)
private val DEFAULT_SUPPORTED_SIZES = arrayOf(
Size(4032, 3024), // 4:3
Size(3840, 2160), // 16:9
@@ -137,9 +100,6 @@
Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
Size(800, 450), // 16:9
Size(640, 480), // 4:3
- Size(320, 240), // 4:3
- Size(320, 180), // 16:9
- Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
)
/** Robolectric test for [SupportedSurfaceCombination] class */
@@ -169,101 +129,15 @@
private val context = ApplicationProvider.getApplicationContext<Context>()
private var cameraFactory: FakeCameraFactory? = null
private var useCaseConfigFactory: UseCaseConfigFactory? = null
-
- private val legacyUseCaseCreator = object : UseCaseCreator {
- override fun createUseCase(
- useCaseType: Int,
- targetRotation: Int,
- preferredAspectRatio: Int,
- preferredResolution: Size?,
- targetFrameRate: Range<Int>?,
- surfaceOccupancyPriority: Int,
- maxResolution: Size?,
- highResolutionEnabled: Boolean,
- defaultResolution: Size?,
- supportedResolutions: List<Pair<Int, Array<Size>>>?,
- customOrderedResolutions: List<Size>?,
- ): UseCase {
- return createUseCaseByLegacyApi(
- useCaseType,
- targetRotation,
- preferredAspectRatio,
- preferredResolution,
- targetFrameRate,
- surfaceOccupancyPriority,
- maxResolution,
- defaultResolution,
- supportedResolutions,
- customOrderedResolutions,
- )
- }
- }
-
- private val resolutionSelectorUseCaseCreator = object : UseCaseCreator {
- override fun createUseCase(
- useCaseType: Int,
- targetRotation: Int,
- preferredAspectRatio: Int,
- preferredResolution: Size?,
- targetFrameRate: Range<Int>?,
- surfaceOccupancyPriority: Int,
- maxResolution: Size?,
- highResolutionEnabled: Boolean,
- defaultResolution: Size?,
- supportedResolutions: List<Pair<Int, Array<Size>>>?,
- customOrderedResolutions: List<Size>?,
- ): UseCase {
- return createUseCaseByResolutionSelector(
- useCaseType,
- preferredAspectRatio,
- preferredResolution,
- sizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
- maxResolution,
- highResolutionEnabled,
- highResolutionForceDisabled = false,
- defaultResolution,
- supportedResolutions,
- customOrderedResolutions,
- )
- }
- }
-
- private val viewSizeResolutionSelectorUseCaseCreator = object : UseCaseCreator {
- override fun createUseCase(
- useCaseType: Int,
- targetRotation: Int,
- preferredAspectRatio: Int,
- preferredResolution: Size?,
- targetFrameRate: Range<Int>?,
- surfaceOccupancyPriority: Int,
- maxResolution: Size?,
- highResolutionEnabled: Boolean,
- defaultResolution: Size?,
- supportedResolutions: List<Pair<Int, Array<Size>>>?,
- customOrderedResolutions: List<Size>?,
- ): UseCase {
- return createUseCaseByResolutionSelector(
- useCaseType,
- preferredAspectRatio,
- preferredResolution,
- sizeCoordinate = SizeCoordinate.ANDROID_VIEW,
- maxResolution,
- highResolutionEnabled,
- highResolutionForceDisabled = false,
- defaultResolution,
- supportedResolutions,
- customOrderedResolutions
- )
- }
- }
+ private lateinit var cameraDeviceSurfaceManager: CameraDeviceSurfaceManager
@Suppress("DEPRECATION") // defaultDisplay
@Before
fun setUp() {
DisplayInfoManager.releaseInstance()
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(DISPLAY_SIZE.width)
- Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(DISPLAY_SIZE.height)
+ shadowOf(windowManager.defaultDisplay).setRealWidth(DISPLAY_SIZE.width)
+ shadowOf(windowManager.defaultDisplay).setRealHeight(DISPLAY_SIZE.height)
Mockito.`when`(
mockCamcorderProfileHelper.hasProfile(
ArgumentMatchers.anyInt(),
@@ -282,6 +156,12 @@
CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
}
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Surface combination support tests for guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
@Test
fun checkLegacySurfaceCombinationSupportedInLegacyDevice() {
setupCameraAndInitCameraX()
@@ -556,731 +436,11 @@
}
}
- @Test
- fun checkTargetAspectRatioInLegacyDevice_LegacyApi() {
- checkTargetAspectRatioInLegacyDevice(legacyUseCaseCreator)
- }
-
- @Test
- fun checkTargetAspectRatioInLegacyDevice_ResolutionSelector() {
- checkTargetAspectRatioInLegacyDevice(resolutionSelectorUseCaseCreator)
- }
-
- private fun checkTargetAspectRatioInLegacyDevice(useCaseCreator: UseCaseCreator) {
- setupCameraAndInitCameraX()
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val targetAspectRatio = ASPECT_RATIO_16_9
- val useCase = useCaseCreator.createUseCase(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
- val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- val selectedSize = suggestedStreamSpecMap[useCase]!!.resolution
- val resultAspectRatio = Rational(selectedSize.width, selectedSize.height)
- // The targetAspectRatio value will only be set to the same aspect ratio as maximum
- // supported jpeg size in Legacy + API 21 combination. For other combinations, it should
- // keep the original targetAspectRatio set for the use case.
- if (Build.VERSION.SDK_INT == 21) {
- // Checks targetAspectRatio and maxJpegAspectRatio, which is the ratio of maximum size
- // in the mSupportedSizes, are not equal to make sure this test case is valid.
- assertThat(targetAspectRatio).isNotEqualTo(maxJpegAspectRatio)
- assertThat(resultAspectRatio).isEqualTo(maxJpegAspectRatio)
- } else {
- // Checks no correction is needed.
- assertThat(resultAspectRatio).isEqualTo(targetAspectRatio)
- }
- }
-
- @Test
- fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_LegacyApi() {
- checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(legacyUseCaseCreator)
- }
-
- @Test
- fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_ResolutionSelector() {
- checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX()
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // The test case make sure the selected result is expected after the regular flow.
- val targetAspectRatio = ASPECT_RATIO_16_9
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- ) as Preview
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- Mockito.mock(
- SurfaceTextureCallback::class.java
- )
- )
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val imageAnalysis = useCaseCreator.createUseCase(
- IMAGE_ANALYSIS_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
- val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- preview,
- imageCapture, imageAnalysis
- )
- val previewResolution = suggestedStreamSpecMap[preview]!!.resolution
- val imageCaptureResolution = suggestedStreamSpecMap[imageCapture]!!.resolution
- val imageAnalysisResolution = suggestedStreamSpecMap[imageAnalysis]!!.resolution
- // The targetAspectRatio value will only be set to the same aspect ratio as maximum
- // supported jpeg size in Legacy + API 21 combination. For other combinations, it should
- // keep the original targetAspectRatio set for the use case.
- if (Build.VERSION.SDK_INT == 21) {
- // Checks targetAspectRatio and maxJpegAspectRatio, which is the ratio of maximum size
- // in the mSupportedSizes, are not equal to make sure this test case is valid.
- assertThat(targetAspectRatio).isNotEqualTo(maxJpegAspectRatio)
- assertThat(hasMatchingAspectRatio(previewResolution, maxJpegAspectRatio)).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- imageCaptureResolution,
- maxJpegAspectRatio
- )
- ).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- imageAnalysisResolution,
- maxJpegAspectRatio
- )
- ).isTrue()
- } else {
- // Checks no correction is needed.
- assertThat(
- hasMatchingAspectRatio(
- previewResolution,
- targetAspectRatio
- )
- ).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- imageCaptureResolution,
- targetAspectRatio
- )
- ).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- imageAnalysisResolution,
- targetAspectRatio
- )
- ).isTrue()
- }
- }
-
- @Test
- fun checkDefaultAspectRatioAndResolutionForMixedUseCase_LegacyApi() {
- checkDefaultAspectRatioAndResolutionForMixedUseCase(legacyUseCaseCreator)
- }
-
- @Test
- fun checkDefaultAspectRatioAndResolutionForMixedUseCase_ResolutionSelector() {
- checkDefaultAspectRatioAndResolutionForMixedUseCase(resolutionSelectorUseCaseCreator)
- }
-
- private fun checkDefaultAspectRatioAndResolutionForMixedUseCase(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- Mockito.mock(
- SurfaceTextureCallback::class.java
- )
- )
- )
- val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
- val imageAnalysis = useCaseCreator.createUseCase(IMAGE_ANALYSIS_USE_CASE)
-
- // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
- // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
- // default aspect ratio will be selected.
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination, preview,
- imageCapture, imageAnalysis
- )
- val previewSize = suggestedStreamSpecMap[preview]!!.resolution
- val imageCaptureSize = suggestedStreamSpecMap[imageCapture]!!.resolution
- val imageAnalysisSize = suggestedStreamSpecMap[imageAnalysis]!!.resolution
-
- val previewAspectRatio = Rational(previewSize.width, previewSize.height)
- val imageCaptureAspectRatio = Rational(imageCaptureSize.width, imageCaptureSize.height)
- val imageAnalysisAspectRatio = Rational(imageAnalysisSize.width, imageAnalysisSize.height)
-
- // Checks the default aspect ratio.
- assertThat(previewAspectRatio).isEqualTo(ASPECT_RATIO_4_3)
- assertThat(imageCaptureAspectRatio).isEqualTo(ASPECT_RATIO_4_3)
- assertThat(imageAnalysisAspectRatio).isEqualTo(ASPECT_RATIO_4_3)
-
- // Checks the default resolution.
- assertThat(imageAnalysisSize).isEqualTo(RESOLUTION_VGA)
- }
-
- @Test
- fun checkSmallSizesAreFilteredOutByDefaultSize480p() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- /* This test case is for b/139018208 that get small resolution 144x256 with below
- conditions:
- 1. The target aspect ratio is set to the screen size 1080 x 2220 (9:18.5).
- 2. The camera doesn't provide any 9:18.5 resolution and the size 144x256(9:16)
- is considered the 9:18.5 mod16 version.
- 3. There is no other bigger resolution matched the target aspect ratio.
- */
- val displayWidth = 1080
- val displayHeight = 2220
- val preview = createUseCaseByLegacyApi(
- PREVIEW_USE_CASE,
- targetResolution = Size(displayHeight, displayWidth)
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination, preview)
- // Checks the preconditions.
- val preconditionSize = Size(256, 144)
- val targetRatio = Rational(displayHeight, displayWidth)
- assertThat(listOf(*DEFAULT_SUPPORTED_SIZES)).contains(preconditionSize)
- DEFAULT_SUPPORTED_SIZES.forEach {
- assertThat(Rational(it.width, it.height)).isNotEqualTo(targetRatio)
- }
- // Checks the mechanism has filtered out the sizes which are smaller than default size 480p.
- val previewSize = suggestedStreamSpecMap[preview]
- assertThat(previewSize).isNotEqualTo(preconditionSize)
- }
-
- @Test
- fun checkAllSupportedSizesCanBeSelected_LegacyApi() {
- checkAllSupportedSizesCanBeSelected(legacyUseCaseCreator)
- }
-
- @Test
- fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_SensorSize() {
- checkAllSupportedSizesCanBeSelected(resolutionSelectorUseCaseCreator)
- }
-
- @Test
- fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_ViewSize() {
- checkAllSupportedSizesCanBeSelected(viewSizeResolutionSelectorUseCaseCreator)
- }
-
- private fun checkAllSupportedSizesCanBeSelected(useCaseCreator: UseCaseCreator) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // Sets each of mSupportedSizes as target resolution and also sets target rotation as
- // Surface.ROTATION to make it aligns the sensor direction and then exactly the same size
- // will be selected as the result. This test can also verify that size smaller than
- // 640x480 can be selected after set as target resolution.
- DEFAULT_SUPPORTED_SIZES.forEach {
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- Surface.ROTATION_90,
- preferredResolution = it
- )
- val suggestedStreamSpecMap =
- getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, imageCapture)
- assertThat(it).isEqualTo(suggestedStreamSpecMap[imageCapture]!!.resolution)
- }
- }
-
- @Test
- fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_LegacyApi() {
- // Sets target resolution as 1280x640, all supported resolutions will be put into
- // aspect ratio not matched list. Then, 1280x720 will be the nearest matched one.
- // Finally, checks whether 1280x720 is selected or not.
- checkCorrectAspectRatioNotMatchedSizeCanBeSelected(legacyUseCaseCreator, Size(1280, 720))
- }
-
- // 1280x640 is not included in the supported sizes list. So, the smallest size of the
- // default aspect ratio 4:3 which is 1280x960 will be finally selected.
- @Test
- fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_SensorSize() {
- checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
- resolutionSelectorUseCaseCreator,
- Size(1280, 960)
- )
- }
-
- @Test
- fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_ViewSize() {
- checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
- viewSizeResolutionSelectorUseCaseCreator,
- Size(1280, 960)
- )
- }
-
- private fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
- useCaseCreator: UseCaseCreator,
- expectedResult: Size
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
- // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
- // checks whether 1280x720 is selected or not.
- val resolution = Size(1280, 640)
- val useCase = useCaseCreator.createUseCase(
- FAKE_USE_CASE,
- Surface.ROTATION_90,
- preferredResolution = resolution
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- assertThat(suggestedStreamSpecMap[useCase]!!.resolution).isEqualTo(expectedResult)
- }
-
- @Test
- fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
- suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
- }
-
- @Test
- fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
- suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun suggestedStreamSpecsForMixedUseCaseNotSupportedInLegacyDevice(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val videoCapture = createVideoCapture()
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- // An IllegalArgumentException will be thrown because a LEGACY level device can't support
- // ImageCapture + VideoCapture + Preview
- assertThrows(IllegalArgumentException::class.java) {
- getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- videoCapture,
- preview
- )
- }
- }
-
- @Test
- fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
- suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
- }
-
- @Test
- fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
- suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun suggestedStreamSpecsForCustomizeResolutionsNotSupportedInLegacyDevice(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
- val previewResolutionsPairs = listOf(
- Pair.create(ImageFormat.PRIVATE, arrayOf(PREVIEW_SIZE))
- )
- val videoCapture: VideoCapture<TestVideoOutput> = createVideoCapture(Quality.UHD)
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- supportedResolutions = previewResolutionsPairs
- )
- // An IllegalArgumentException will be thrown because the VideoCapture requests to only
- // support a RECORD size but the configuration can't be supported on a LEGACY level device.
- assertThrows(IllegalArgumentException::class.java) {
- getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, videoCapture, preview)
- }
- }
-
- @Test
- fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_LegacyApi() {
- getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
- }
-
- @Test
- fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
- getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
- }
-
- private fun getsuggestedStreamSpecsForMixedUseCaseInLimitedDevice(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val videoCapture = createVideoCapture(Quality.HIGHEST)
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- videoCapture,
- preview
- )
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
- assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
- assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
- }
-
- // For the use case in b/230651237,
- // QualitySelector.from(Quality.UHD, FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD).
- // VideoCapture should have higher priority to choose size than ImageCapture.
- @Test
- @Throws(CameraUnavailableException::class)
- fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
- getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
- legacyUseCaseCreator)
- }
-
- @Test
- @Throws(CameraUnavailableException::class)
- fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
- getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun getsuggestedStreamSpecsInFullDevice_videoHasHigherPriorityThanImage(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val videoCapture = createVideoCapture(QualitySelector.from(
- Quality.UHD,
- FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
- ))
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- videoCapture,
- preview
- )
- // There are two possible combinations in Full level device
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD) => should be applied
- // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
- assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(RECORD_SIZE)
- assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(RECORD_SIZE)
- assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
- }
-
- @Test
- fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_LegacyApi() {
- imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
- legacyUseCaseCreator)
- }
-
- @Test
- fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_ResolutionSelector() {
- imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_4_3 // mMaximumSize(4032x3024) is 4:3
- )
- val videoCapture = createVideoCapture(
- QualitySelector.fromOrderedList(
- listOf<Quality>(Quality.HD, Quality.FHD, Quality.UHD)
- )
- )
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- videoCapture,
- preview
- )
- // There are two possible combinations in Full level device
- // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
- // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM) => should be applied
- assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(MAXIMUM_SIZE)
- // Quality.HD
- assertThat(suggestedStreamSpecMap[videoCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
- assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
- }
-
- @Test
- fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_LegacyApi() {
- getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
- legacyUseCaseCreator,
- DISPLAY_SIZE
- )
- }
-
- @Test
- fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
- getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
- resolutionSelectorUseCaseCreator,
- PREVIEW_SIZE
- )
- }
-
- @Test
- fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
- getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
- viewSizeResolutionSelectorUseCaseCreator,
- PREVIEW_SIZE
- )
- }
-
- private fun getsuggestedStreamSpecsWithSameSupportedListForDifferentUseCases(
- useCaseCreator: UseCaseCreator,
- preferredResolution: Size
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
- supportedSizes = arrayOf(
- Size(4032, 3024), // 4:3
- Size(3840, 2160), // 16:9
- Size(1920, 1440), // 4:3
- Size(1920, 1080), // 16:9
- Size(1280, 960), // 4:3
- Size(1280, 720), // 16:9
- Size(1280, 720), // duplicate the size since Nexus 5X emulator has the case.
- Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
- Size(800, 450), // 16:9
- Size(640, 480), // 4:3
- Size(320, 240), // 4:3
- Size(320, 180), // 16:9
- Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- /* This test case is for b/132603284 that divide by zero issue crash happened in below
- conditions:
- 1. There are duplicated two 1280x720 supported sizes for ImageCapture and Preview.
- 2. supportedOutputSizes for ImageCapture and Preview in
- SupportedSurfaceCombination#getAllPossibleSizeArrangements are the same.
- */
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredResolution = preferredResolution
- )
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredResolution = preferredResolution
- )
- val imageAnalysis = useCaseCreator.createUseCase(
- IMAGE_ANALYSIS_USE_CASE,
- preferredResolution = preferredResolution
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- imageAnalysis,
- preview
- )
- assertThat(suggestedStreamSpecMap[imageCapture]!!.resolution).isEqualTo(PREVIEW_SIZE)
- assertThat(suggestedStreamSpecMap[imageAnalysis]!!.resolution).isEqualTo(PREVIEW_SIZE)
- assertThat(suggestedStreamSpecMap[preview]!!.resolution).isEqualTo(PREVIEW_SIZE)
- }
-
- @Test
- fun setTargetAspectRatioForMixedUseCases_LegacyApi() {
- setTargetAspectRatioForMixedUseCases(legacyUseCaseCreator)
- }
-
- @Test
- fun setTargetAspectRatioForMixedUseCases_ResolutionSelector() {
- setTargetAspectRatioForMixedUseCases(resolutionSelectorUseCaseCreator)
- }
-
- private fun setTargetAspectRatioForMixedUseCases(useCaseCreator: UseCaseCreator) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val imageAnalysis = useCaseCreator.createUseCase(
- IMAGE_ANALYSIS_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- preview,
- imageCapture,
- imageAnalysis
- )
- assertThat(
- hasMatchingAspectRatio(
- suggestedStreamSpecMap[preview]!!.resolution,
- ASPECT_RATIO_16_9
- )
- ).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- suggestedStreamSpecMap[imageCapture]!!.resolution,
- ASPECT_RATIO_16_9
- )
- ).isTrue()
- assertThat(
- hasMatchingAspectRatio(
- suggestedStreamSpecMap[imageAnalysis]!!.resolution,
- ASPECT_RATIO_16_9
- )
- ).isTrue()
- }
-
- @Test
- fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_LegacyApi() {
- getsuggestedStreamSpecsForCustomizedSupportedResolutions(legacyUseCaseCreator)
- }
-
- @Test
- fun getsuggestedStreamSpecsForCustomizedSupportedResolutions_ResolutionSelector() {
- getsuggestedStreamSpecsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
- }
-
- private fun getsuggestedStreamSpecsForCustomizedSupportedResolutions(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val formatResolutionsPairList = arrayListOf<Pair<Int, Array<Size>>>().apply {
- add(Pair.create(ImageFormat.JPEG, arrayOf(RESOLUTION_VGA)))
- add(Pair.create(ImageFormat.YUV_420_888, arrayOf(RESOLUTION_VGA)))
- add(Pair.create(ImageFormat.PRIVATE, arrayOf(RESOLUTION_VGA)))
- }
- // Sets use cases customized supported resolutions to 640x480 only.
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- supportedResolutions = formatResolutionsPairList
- )
- val videoCapture = createVideoCapture(Quality.SD)
- val preview = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- supportedResolutions = formatResolutionsPairList
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- imageCapture,
- videoCapture,
- preview
- )
- // Checks all resolutions in suggested stream specs will become 640x480.
- assertThat(suggestedStreamSpecMap[imageCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
- assertThat(suggestedStreamSpecMap[videoCapture]?.resolution).isEqualTo(RESOLUTION_VGA)
- assertThat(suggestedStreamSpecMap[preview]?.resolution).isEqualTo(RESOLUTION_VGA)
- }
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Surface config transformation tests
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
@Test
fun transformSurfaceConfigWithYUVAnalysisSize() {
@@ -1484,6 +644,947 @@
assertThat(surfaceConfig).isEqualTo(expectedSurfaceConfig)
}
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for LEGACY-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singlePrivStream_inLegacyDevice() {
+ val privUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singleJpegStream_inLegacyDevice() {
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSize_singleYuvStream_inLegacyDevice() {
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusJpeg_inLegacyDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW)
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusJpeg_inLegacyDevice() {
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLegacyDevice() {
+ val privUseCase1 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, PREVIEW_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLegacyDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusJpeg_inLegacyDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+
+ /**
+ * Unsupported PRIV + JPEG + PRIV for legacy level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLegacyDevice() {
+ val privUseCase1 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val privUseCas2 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RESOLUTION_VGA)
+ put(jpegUseCase, RESOLUTION_VGA)
+ put(privUseCas2, RESOLUTION_VGA)
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(useCaseExpectedResultMap)
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for LIMITED-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLimitedDevice() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCas2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RECORD_SIZE)
+ put(privUseCas2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLimitedDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, RECORD_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, RECORD_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/RECORD + JPEG/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpeg_inLimitedDevice() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RECORD_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ put(jpegUseCase, RECORD_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusJpeg_inLimitedDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, RECORD_SIZE)
+ put(jpegUseCase, RECORD_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusJpeg_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, PREVIEW_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ /**
+ * Unsupported YUV + PRIV + YUV for limited level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLimitedDevice() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, RESOLUTION_VGA)
+ put(privUseCase, RESOLUTION_VGA)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for FULL-level guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inFullDevice() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, MAXIMUM_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inFullDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inFullDevice() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, MAXIMUM_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW + JPEG/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpeg_inFullDevice() {
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(jpegUseCase, MAXIMUM_SIZE)
+ put(privUseCase1, PREVIEW_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/VGA + PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusPrivPlusYuv_inFullDevice() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase1, MAXIMUM_SIZE)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * YUV/VGA + YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusYuv_inFullDevice() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase3 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, MAXIMUM_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ put(yuvUseCase3, RESOLUTION_VGA)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ )
+ }
+
+ /**
+ * Unsupported PRIV + PRIV + YUV + RAW for full level devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inFullDevice() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, RESOLUTION_VGA)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(yuvUseCase, RESOLUTION_VGA)
+ put(rawUseCase, RESOLUTION_VGA)
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for Level-3 guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/VGA + YUV/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusYuvPlusRaw_inLevel3Device() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, PREVIEW_SIZE)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(yuvUseCase, MAXIMUM_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/VGA + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusJpegPlusRaw_inLevel3Device() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, PREVIEW_SIZE)
+ put(privUseCase2, RESOLUTION_VGA)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+
+ /**
+ * Unsupported PRIV + YUV + YUV + RAW for level-3 devices
+ */
+ @Test
+ fun throwsException_unsupportedConfiguration_inLevel3Device() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, RESOLUTION_VGA)
+ put(yuvUseCase1, RESOLUTION_VGA)
+ put(yuvUseCase2, RESOLUTION_VGA)
+ put(rawUseCase, RESOLUTION_VGA)
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
+ )
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for Burst-capability guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * PRIV/PREVIEW + PRIV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPriv_inLimitedDevice_withBurstCapability() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, MAXIMUM_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuv_inLimitedDevice_withBurstCapability() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuv_inLimitedDevice_withBurstCapability() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, MAXIMUM_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+ )
+ )
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for RAW-capability guaranteed configurations
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * RAW/MAX
+ */
+ @Test
+ fun canSelectCorrectSizes_singleRawStream_inLimitedDevice_withRawCapability() {
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + PRIV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusPrivPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase1 = createUseCase(CaptureType.VIDEO_CAPTURE) // PRIV
+ val privUseCase2 = createUseCase(CaptureType.PREVIEW) // PRIV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase1, PREVIEW_SIZE)
+ put(privUseCase2, PREVIEW_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + YUV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusYuvPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(yuvUseCase, PREVIEW_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + YUV/PREVIEW + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusYuvPlusRAW_inLimitedDevice_withRawCapability() {
+ val yuvUseCase1 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val yuvUseCase2 = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase1, PREVIEW_SIZE)
+ put(yuvUseCase2, PREVIEW_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * PRIV/PREVIEW + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_privPlusJpegPlusRAW_inLimitedDevice_withRawCapability() {
+ val privUseCase = createUseCase(CaptureType.PREVIEW) // PRIV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(privUseCase, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ /**
+ * YUV/PREVIEW + JPEG/MAXIMUM + RAW/MAXIMUM
+ */
+ @Test
+ fun canSelectCorrectSizes_yuvPlusJpegPlusRAW_inLimitedDevice_withRawCapability() {
+ val yuvUseCase = createUseCase(CaptureType.IMAGE_ANALYSIS) // YUV
+ val jpegUseCase = createUseCase(CaptureType.IMAGE_CAPTURE) // JPEG
+ val rawUseCase = createRawUseCase() // RAW
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(yuvUseCase, PREVIEW_SIZE)
+ put(jpegUseCase, MAXIMUM_SIZE)
+ put(rawUseCase, MAXIMUM_SIZE)
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ capabilities = intArrayOf(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
+ )
+ }
+
+ private fun getSuggestedSpecsAndVerify(
+ useCasesExpectedResultMap: Map<UseCase, Size>,
+ attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
+ hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ capabilities: IntArray? = null,
+ compareWithAtMost: Boolean = false
+ ) {
+ setupCameraAndInitCameraX(
+ hardwareLevel = hardwareLevel,
+ capabilities = capabilities
+ )
+ val supportedSurfaceCombination = SupportedSurfaceCombination(
+ context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+ )
+
+ val useCaseConfigMap = getUseCaseToConfigMap(useCasesExpectedResultMap.keys.toList())
+ val useCaseConfigToOutputSizesMap =
+ getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
+ val suggestedStreamSpecs = supportedSurfaceCombination.getSuggestedStreamSpecifications(
+ false,
+ attachedSurfaceInfoList,
+ useCaseConfigToOutputSizesMap
+ )
+
+ useCasesExpectedResultMap.keys.forEach {
+ val resultSize = suggestedStreamSpecs[useCaseConfigMap[it]]!!.resolution
+ val expectedSize = useCasesExpectedResultMap[it]!!
+ if (!compareWithAtMost) {
+ assertThat(resultSize).isEqualTo(expectedSize)
+ } else {
+ assertThat(sizeIsAtMost(resultSize, expectedSize)).isTrue()
+ }
+ }
+ }
+
+ private fun getUseCaseToConfigMap(useCases: List<UseCase>): Map<UseCase, UseCaseConfig<*>> {
+ val useCaseConfigMap = mutableMapOf<UseCase, UseCaseConfig<*>>().apply {
+ useCases.forEach {
+ put(it, it.currentConfig)
+ }
+ }
+ return useCaseConfigMap
+ }
+
+ private fun getUseCaseConfigToOutputSizesMap(
+ useCaseConfigs: List<UseCaseConfig<*>>
+ ): Map<UseCaseConfig<*>, List<Size>> {
+ val resultMap = mutableMapOf<UseCaseConfig<*>, List<Size>>().apply {
+ useCaseConfigs.forEach {
+ put(it, DEFAULT_SUPPORTED_SIZES.toList())
+ }
+ }
+
+ return resultMap
+ }
+
+ /**
+ * Helper function that returns whether size is <= maxSize
+ *
+ */
+ private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
+ return (size.height * size.width) <= (maxSize.height * maxSize.width)
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Resolution selection tests for FPS settings
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
+ @Test
+ fun getSupportedOutputSizes_single_valid_targetFPS() {
+ // a valid target means the device is capable of that fps
+ val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(25, 30))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase, Size(3840, 2160))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_single_invalid_targetFPS() {
+ // an invalid target means the device would neve be able to reach that fps
+ val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(65, 70))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ put(useCase, Size(800, 450))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_multiple_targetFPS_first_is_larger() {
+ // a valid target means the device is capable of that fps
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
+ val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(15, 25))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // both selected size should be no larger than 1920 x 1445
+ put(useCase1, Size(1920, 1445))
+ put(useCase2, Size(1920, 1445))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_multiple_targetFPS_first_is_smaller() {
+ // a valid target means the device is capable of that fps
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
+ val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(45, 50))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // both selected size should be no larger than 1920 x 1440
+ put(useCase1, Size(1920, 1440))
+ put(useCase2, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_multiple_targetFPS_intersect() {
+ // first and second new use cases have target fps that intersect each other
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 40))
+ val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(35, 45))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // effective target fps becomes 35-40
+ // both selected size should be no larger than 1920 x 1080
+ put(useCase1, Size(1920, 1080))
+ put(useCase2, Size(1920, 1080))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_multiple_cases_first_has_targetFPS() {
+ // first new use case has a target fps, second new use case does not
+ val useCase1 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
+ val useCase2 = createUseCase(CaptureType.PREVIEW)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // both selected size should be no larger than 1920 x 1440
+ put(useCase1, Size(1920, 1440))
+ put(useCase2, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_multiple_cases_second_has_targetFPS() {
+ // second new use case does not have a target fps, first new use case does not
+ val useCase1 = createUseCase(CaptureType.PREVIEW)
+ val useCase2 = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // both selected size should be no larger than 1920 x 1440
+ put(useCase1, Size(1920, 1440))
+ put(useCase2, Size(1920, 1440))
+ }
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_attached_with_targetFPS_no_new_targetFPS() {
+ // existing surface with target fps + new use case without a target fps
+ val useCase = createUseCase(CaptureType.PREVIEW)
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // size should be no larger than 1280 x 960
+ put(useCase, Size(1280, 960))
+ }
+ // existing surface w/ target fps
+ val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+ SurfaceConfig.create(
+ ConfigType.JPEG,
+ ConfigSize.PREVIEW
+ ), ImageFormat.JPEG,
+ Size(1280, 720), Range(40, 50)
+ )
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
+ // existing surface with target fps + new use case with target fps that does not intersect
+ val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(30, 35))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // size of new surface should be no larger than 1280 x 960
+ put(useCase, Size(1280, 960))
+ }
+ // existing surface w/ target fps
+ val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+ SurfaceConfig.create(
+ ConfigType.JPEG,
+ ConfigSize.PREVIEW
+ ), ImageFormat.JPEG,
+ Size(1280, 720), Range(40, 50)
+ )
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
+ // existing surface with target fps + new use case with target fps that intersect each other
+ val useCase = createUseCase(CaptureType.PREVIEW, targetFrameRate = Range<Int>(45, 50))
+ val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+ // size of new surface should be no larger than 1280 x 720
+ put(useCase, Size(1280, 720))
+ }
+ // existing surface w/ target fps
+ val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+ SurfaceConfig.create(
+ ConfigType.JPEG,
+ ConfigSize.PREVIEW
+ ), ImageFormat.JPEG,
+ Size(1280, 720), Range(40, 50)
+ )
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+ hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ compareWithAtMost = true
+ )
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Other tests
+ //
+ // //////////////////////////////////////////////////////////////////////////////////////////
+
@Test
fun getMaximumSizeForImageFormat() {
setupCameraAndInitCameraX()
@@ -1498,1043 +1599,6 @@
}
@Test
- fun isAspectRatioMatchWithSupportedMod16Resolution_LegacyApi() {
- isAspectRatioMatchWithSupportedMod16Resolution(legacyUseCaseCreator)
- }
-
- @Test
- fun isAspectRatioMatchWithSupportedMod16Resolution_ResolutionSelector() {
- isAspectRatioMatchWithSupportedMod16Resolution(resolutionSelectorUseCaseCreator)
- }
-
- private fun isAspectRatioMatchWithSupportedMod16Resolution(useCaseCreator: UseCaseCreator) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = useCaseCreator.createUseCase(
- FAKE_USE_CASE,
- Surface.ROTATION_90,
- preferredAspectRatio = AspectRatio.RATIO_16_9,
- preferredResolution = MOD16_SIZE
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MOD16_SIZE)
- }
-
- @Test
- fun sortByCompareSizesByArea_canSortSizesCorrectly() {
- val sizes = arrayOfNulls<Size>(DEFAULT_SUPPORTED_SIZES.size)
- // Generates a unsorted array from mSupportedSizes.
- val centerIndex = DEFAULT_SUPPORTED_SIZES.size / 2
- // Puts 2nd half sizes in the front
- for (i in centerIndex until DEFAULT_SUPPORTED_SIZES.size) {
- sizes[i - centerIndex] = DEFAULT_SUPPORTED_SIZES[i]
- }
- // Puts 1st half sizes inversely in the tail
- for (j in centerIndex - 1 downTo 0) {
- sizes[DEFAULT_SUPPORTED_SIZES.size - j - 1] = DEFAULT_SUPPORTED_SIZES[j]
- }
- // The testing sizes array will be equal to mSupportedSizes after sorting.
- Arrays.sort(sizes, CompareSizesByArea(true))
- assertThat(listOf(*sizes)).isEqualTo(listOf(*DEFAULT_SUPPORTED_SIZES))
- }
-
- @Test
- fun getSupportedOutputSizes_noConfigSettings() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(FAKE_USE_CASE)
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. No any aspect ratio related setting. The returned sizes list will be sorted in
- // descending order.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- Size(4032, 3024),
- Size(3840, 2160),
- Size(1920, 1440),
- Size(1920, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio4x3() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_4_3
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 4/3 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_aspectRatio16x9_inLegacyDevice() {
- setupCameraAndInitCameraX()
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed.
- val expectedList: List<Size> = if (Build.VERSION.SDK_INT == 21) {
- // Sizes with the same aspect ratio as maximum JPEG resolution will be in front of
- // the returned sizes list and the list is sorted in descending order. Other items
- // will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- listOf(
- // Matched the same AspectRatio as maximum JPEG items, sorted by aspect ratio
- // delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- // Non-matched items have been removed by OutputSizesCorrector due to
- // TargetAspectRatio quirk.
- )
- } else {
- // Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that
- // are sorted by aspect ratio delta and then area size.
- listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- }
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolution1080x1920InRotation0_InLimitedDevice() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetResolution = Size(1080, 1920)
- )
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // target resolution will be calibrated by default target rotation 0 degree. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 16/9 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolution1080x1920InRotation0_InLegacyDevice() {
- setupCameraAndInitCameraX()
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetResolution = Size(1080, 1920)
- )
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList: List<Size> = if (Build.VERSION.SDK_INT == 21) {
- // Sizes with the same aspect ratio as maximum JPEG resolution will be in front of
- // the returned sizes list and the list is sorted in descending order. Other items
- // will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- listOf(
- // Matched the same AspectRatio as maximum JPEG items, sorted by aspect ratio
- // delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- // Non-matched items have been removed by OutputSizesCorrector due to
- // TargetAspectRatio quirk.
- )
- } else {
- // The target resolution will be calibrated by default target rotation 0 degree. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is
- // nearest to the aspect ratio of target resolution in priority. Therefore, sizes of
- // aspect ratio 16/9 will be in front of the returned sizes list and the list is
- // sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- }
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolutionLargerThan640x480() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(1280, 960)
- )
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Target resolution larger than 640x480 won't overwrite
- // minimum size setting. Sizes smaller than 640x480 will be removed. The auto-resolution
- // mechanism will try to select the sizes which aspect ratio is nearest to the aspect
- // ratio of target resolution in priority. Therefore, sizes of aspect ratio 4/3 will be
- // in front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1280, 960),
- Size(640, 480),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolutionSmallerThan640x480() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(320, 240)
- )
- // Unnecessary big enough sizes will be removed from the result list. Minimum size will
- // be overwritten as 320x240. Sizes smaller than 320x240 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 4/3 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(320, 240),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_maxResolutionSmallerThan640x480() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- maxResolution = Size(320, 240)
- )
- // Minimum size bound will be removed due to small max resolution setting.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = Arrays.asList(
- *arrayOf(
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolution1800x1440NearTo4x3() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(1800, 1440)
- )
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Size 1800x1440 is near to 4/3
- // therefore, sizes of aspect ratio 4/3 will be in front of the returned sizes list and
- // the list is sorted in descending order.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Sizes of 4/3 are near to aspect ratio of 1800/1440
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480),
- // Sizes of 16/9 are far to aspect ratio of 1800/1440
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_targetResolution1280x600NearTo16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(1280, 600)
- )
- // Unnecessary big enough sizes will be removed from the result list. There is default
- // minimum size 640x480 setting. Sizes smaller than 640x480 will also be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Size 1280x600 is near to 16/9,
- // therefore, sizes of aspect ratio 16/9 will be in front of the returned sizes list and
- // the list is sorted in descending order.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Sizes of 16/9 are near to aspect ratio of 1280/600
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Sizes of 4/3 are far to aspect ratio of 1280/600
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_maxResolution1280x720() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- maxResolution = Size(1280, 720)
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 or
- // larger than 1280x720 will be removed. The returned sizes list will be sorted in
- // descending order.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(1280, 720), Size(960, 544), Size(800, 450), Size(640, 480))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_setCustomOrderedResolutions() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val customOrderedResolutions = listOf(
- Size(640, 480),
- Size(1280, 720),
- Size(1920, 1080),
- Size(3840, 2160),
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- customOrderedResolutions = customOrderedResolutions,
- maxResolution = Size(1920, 1440),
- defaultResolution = Size(1280, 720),
- supportedResolutions = listOf(
- Pair.create(
- ImageFormat.PRIVATE, arrayOf(
- Size(800, 450),
- Size(640, 480),
- Size(320, 240),
- )
- )
- )
- )
- // Custom ordered resolutions is fully respected, meaning it will not be sorted or filtered
- // by other configurations such as max/default/target/supported resolutions.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- assertThat(resultList).containsExactlyElementsIn(customOrderedResolutions).inOrder()
- }
-
- @Test
- fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_LegacyApi() {
- previewCanSelectResolutionLargerThanDisplay_withMaxResolution(legacyUseCaseCreator)
- }
-
- @Test
- fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_ResolutionSelector() {
- previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // The max resolution is expressed in the sensor coordinate.
- val useCase = useCaseCreator.createUseCase(
- PREVIEW_USE_CASE,
- maxResolution = MAXIMUM_SIZE
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- // Checks mMaximumSize is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(MAXIMUM_SIZE)
- }
-
- @Test
- fun getSupportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- defaultResolution = Size(1280, 720)
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. If there is no target resolution setting, it will be overwritten by default
- // resolution as 1280x720. Unnecessary big enough sizes will also be removed. The
- // returned sizes list will be sorted in descending order.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(1280, 720), Size(960, 544), Size(800, 450), Size(640, 480))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_defaultResolution1280x720_targetResolution1920x1080() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- defaultResolution = Size(1280, 720),
- targetResolution = Size(1920, 1080)
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. There is target resolution 1920x1080, it won't be overwritten by default
- // resolution 1280x720. Unnecessary big enough sizes will also be removed. Sizes of
- // aspect ratio 16/9 will be in front of the returned sizes list and the list is sorted
- // in descending order. Other items will be put in the following that are sorted by
- // aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(1920, 1080)
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. There is target resolution 1920x1080 (16:9). Even 640x480 does not match 16:9
- // requirement, it will still be returned to use.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(640, 480))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxSizeSmallerThanDefaultMiniSize() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- maxResolution = Size(320, 240)
- )
- // There is default minimum size 640x480 setting. Originally, sizes smaller than 640x480
- // will be removed. Due to maximal size bound is smaller than the default minimum size
- // bound and it is also smaller than 640x480, the default minimum size bound will be
- // ignored. Then, sizes equal to or smaller than 320x240 will be kept in the result list.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(320, 240), Size(320, 180), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(320, 240),
- maxResolution = Size(320, 180)
- )
- // The default minimum size 640x480 will be overwritten by the target resolution 320x240.
- // Originally, sizes smaller than 320x240 will be removed. Due to maximal size bound is
- // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
- // size bound will be ignored. Then, sizes equal to or smaller than 320x180 will be kept
- // in the result list.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(320, 180), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenBothMaxAndTargetResolutionsSmallerThan640x480() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(320, 180),
- maxResolution = Size(320, 240)
- )
- // The default minimum size 640x480 will be overwritten by the target resolution 320x180.
- // Originally, sizes smaller than 320x180 will be removed. Due to maximal size bound is
- // smaller than the minimum size bound and it is also smaller than 640x480, the minimum
- // size bound will be ignored. Then, all sizes equal to or smaller than 320x320 will be
- // kept in the result list.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(320, 180), Size(256, 144), Size(320, 240))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxSizeSmallerThanBigTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(3840, 2160),
- maxResolution = Size(1920, 1080)
- )
- // Because the target size 3840x2160 is larger than 640x480, it won't overwrite the
- // default minimum size 640x480. Sizes smaller than 640x480 will be removed. The
- // auto-resolution mechanism will try to select the sizes which aspect ratio is nearest
- // to the aspect ratio of target resolution in priority. Therefore, sizes of aspect ratio
- // 16/9 will be in front of the returned sizes list and the list is sorted in descending
- // order. Other items will be put in the following that are sorted by aspect ratio delta
- // and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenNoSizeBetweenMaxSizeAndTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(320, 190),
- maxResolution = Size(320, 200)
- )
- // The default minimum size 640x480 will be overwritten by the target resolution 320x190.
- // Originally, sizes smaller than 320x190 will be removed. Due to there is no available
- // size between the maximal size and the minimum size bound and the maximal size is
- // smaller than 640x480, the default minimum size bound will be ignored. Then, sizes
- // equal to or smaller than 320x200 will be kept in the result list.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(320, 180), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenTargetResolutionSmallerThanAnySize() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(192, 144)
- )
- // The default minimum size 640x480 will be overwritten by the target resolution 192x144.
- // Because 192x144 is smaller than any size in the supported list, no one will be
- // filtered out by it. The result list will only keep one big enough size of aspect ratio
- // 4:3 and 16:9.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(320, 240), Size(256, 144))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
- setupCameraAndInitCameraX(
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- maxResolution = Size(192, 144)
- )
- // All sizes will be filtered out by the max resolution 192x144 setting and an
- // IllegalArgumentException will be thrown.
- assertThrows(IllegalArgumentException::class.java) {
- getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- }
- }
-
- @Test
- fun getSupportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(640, 480),
- Size(320, 240),
- Size(320, 180),
- Size(296, 144),
- Size(256, 144)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- targetResolution = Size(185, 90)
- )
- // The default minimum size 640x480 will be overwritten by the target resolution 185x90
- // (18.5:9). If mod 16 calculation is not ignored for the sizes smaller than 640x480, the
- // size 256x144 will be considered to match 18.5:9 and then become the first item in the
- // result list. After ignoring mod 16 calculation for small sizes, 256x144 will still be
- // kept as a 16:9 resolution as the result.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(Size(296, 144), Size(256, 144), Size(320, 240))
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
- Size(768, 432),
- Size(640, 480),
- Size(640, 360),
- Size(480, 360),
- Size(384, 288)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetResolution = Size(1080, 2016)
- )
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- Size(1920, 1080),
- Size(1280, 720),
- Size(864, 480),
- Size(768, 432),
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
- // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
- // phone device which majorly supports portrait output sizes.
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0,
- pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
- supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(1280, 720),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
- // front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(720, 1280),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(640, 480),
- Size(1280, 720)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
- // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
- // tablet device which majorly supports portrait output sizes.
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_90,
- pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
- supportedSizes = arrayOf(
- Size(1080, 1920),
- Size(1080, 1440),
- Size(960, 1280),
- Size(720, 1280),
- Size(1280, 720),
- Size(480, 640),
- Size(640, 480),
- Size(360, 480)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in
- // front of the returned sizes list and the list is sorted in descending order. Other
- // items will be put in the following that are sorted by aspect ratio delta and then area
- // size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1080, 1920),
- Size(720, 1280),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1080, 1440),
- Size(960, 1280),
- Size(480, 640),
- Size(640, 480),
- Size(1280, 720)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTablet_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0,
- pixelArraySize = LANDSCAPE_PIXEL_ARRAY_SIZE
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1280, 720),
- Size(960, 544),
- Size(800, 450),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(4032, 3024),
- Size(1920, 1440),
- Size(1280, 960),
- Size(640, 480)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
- fun getSupportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- sensorOrientation = SENSOR_ORIENTATION_0, supportedSizes = arrayOf(
- Size(1920, 1080),
- Size(1440, 1080),
- Size(1280, 960),
- Size(1280, 720),
- Size(720, 1280),
- Size(640, 480),
- Size(480, 640),
- Size(480, 360)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val useCase = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetAspectRatio = AspectRatio.RATIO_16_9
- )
- // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
- // removed. Sizes of aspect ratio 16/9 will be in front of the returned sizes list and the
- // list is sorted in descending order. Other items will be put in the following that are
- // sorted by aspect ratio delta and then area size.
- val resultList = getSupportedOutputSizes(supportedSurfaceCombination, useCase)
- val expectedList = listOf(
- // Matched AspectRatio items, sorted by area size.
- Size(1920, 1080),
- Size(1280, 720),
- // Mismatched AspectRatio items, sorted by aspect ratio delta then area size.
- Size(1440, 1080),
- Size(1280, 960),
- Size(640, 480),
- Size(480, 640),
- Size(720, 1280)
- )
- assertThat(resultList).isEqualTo(expectedList)
- }
-
- @Test
fun determineRecordSizeFromStreamConfigurationMap() {
// Setup camera with non-integer camera Id
setupCameraAndInitCameraX(cameraId = EXTERNAL_CAMERA_ID)
@@ -2549,652 +1613,6 @@
)
}
- @Test
- fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_LegacyApi() {
- canGet640x480_whenAnotherGroupMatchedInMod16Exists(legacyUseCaseCreator)
- }
-
- @Test
- fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_SensorSize() {
- canGet640x480_whenAnotherGroupMatchedInMod16Exists(resolutionSelectorUseCaseCreator)
- }
-
- @Test
- fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_ViewSize() {
- canGet640x480_whenAnotherGroupMatchedInMod16Exists(viewSizeResolutionSelectorUseCaseCreator)
- }
-
- private fun canGet640x480_whenAnotherGroupMatchedInMod16Exists(useCaseCreator: UseCaseCreator) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(
- Size(4000, 3000),
- Size(3840, 2160),
- Size(1920, 1080),
- Size(1024, 738), // This will create a 512/269 aspect ratio group that
- // 640x480 will be considered to match in mod16 condition.
- Size(800, 600),
- Size(640, 480),
- Size(320, 240)
- )
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // Sets the target resolution as 640x480 with target rotation as ROTATION_90 because the
- // sensor orientation is 90.
- val useCase = useCaseCreator.createUseCase(
- FAKE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- preferredResolution = RESOLUTION_VGA
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- // Checks 640x480 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(RESOLUTION_VGA)
- }
-
- @Test
- fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_LegacyApi() {
- canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(legacyUseCaseCreator)
- }
-
- @Test
- fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_ResolutionSelector() {
- canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
- resolutionSelectorUseCaseCreator
- )
- }
-
- private fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedSizes = arrayOf(Size(480, 480))
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- // Sets the max resolution as 720x1280
- val useCase = useCaseCreator.createUseCase(
- FAKE_USE_CASE,
- maxResolution = DISPLAY_SIZE
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
- // Checks 480x480 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(480, 480))
- }
-
- @Test
- fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_LegacyApi() {
- previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
- legacyUseCaseCreator, PREVIEW_SIZE
- )
- }
-
- // For the ResolutionSelector API, RECORD_SIZE can't be used because it exceeds
- // PREVIEW_SIZE. Therefore, the logic will fallback to select a 4:3 PREVIEW_SIZE. Then,
- // 640x480 will be selected.
- @Test
- fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_SensorSize() {
- previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
- resolutionSelectorUseCaseCreator, RESOLUTION_VGA
- )
- }
-
- @Test
- fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_ViewSize() {
- previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
- viewSizeResolutionSelectorUseCaseCreator, RESOLUTION_VGA
- )
- }
-
- private fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
- useCaseCreator: UseCaseCreator,
- expectedResult: Size
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- Mockito.mock(
- SurfaceTextureCallback::class.java
- )
- )
- )
- // ImageCapture has no explicit target resolution setting
- val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
- // A LEGACY-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
- //
- // A LIMITED-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
- //
- // Even there is a RECORD size target resolution setting for ImageAnalysis, ImageCapture
- // will still have higher priority to have a MAXIMUM size resolution if the app doesn't
- // explicitly specify a RECORD size target resolution to ImageCapture.
- val imageAnalysis = useCaseCreator.createUseCase(
- IMAGE_ANALYSIS_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- preferredResolution = RECORD_SIZE
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- preview,
- imageCapture,
- imageAnalysis
- )
- assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(expectedResult)
- }
-
- @Test
- fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_LegacyApi() {
- imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
- legacyUseCaseCreator
- )
- }
-
- @Test
- fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_SensorSize() {
- imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
- resolutionSelectorUseCaseCreator
- )
- }
-
- @Test
- fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_ViewSize() {
- imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
- viewSizeResolutionSelectorUseCaseCreator
- )
- }
-
- private fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
- useCaseCreator: UseCaseCreator
- ) {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
- val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
- preview.setSurfaceProvider(
- CameraXExecutors.directExecutor(),
- SurfaceTextureProvider.createSurfaceTextureProvider(
- Mockito.mock(
- SurfaceTextureCallback::class.java
- )
- )
- )
- // ImageCapture has no explicit RECORD size target resolution setting
- val imageCapture = useCaseCreator.createUseCase(
- IMAGE_CAPTURE_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- preferredResolution = RECORD_SIZE
- )
- // A LEGACY-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
- //
- // A LIMITED-level above device supports the following configuration.
- // PRIV/PREVIEW + YUV/RECORD + JPEG/RECORD
- //
- // A RECORD can be selected for ImageAnalysis if the ImageCapture has a explicit RECORD
- // size target resolution setting. It means that the application know the trade-off and
- // the ImageAnalysis has higher priority to get a larger resolution than ImageCapture.
- val imageAnalysis = useCaseCreator.createUseCase(
- IMAGE_ANALYSIS_USE_CASE,
- targetRotation = Surface.ROTATION_90,
- preferredResolution = RECORD_SIZE
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- preview,
- imageCapture,
- imageAnalysis
- )
- assertThat(suggestedStreamSpecMap[imageAnalysis]?.resolution).isEqualTo(RECORD_SIZE)
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun highResolutionIsSelected_whenHighResolutionIsEnabled() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- capabilities = intArrayOf(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
- ),
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
-
- // Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 6000))
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun highResolutionIsNotSelected_whenHighResolutionIsEnabled_withoutBurstCaptureCapability() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
-
- // Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun highResolutionIsNotSelected_whenHighResolutionIsNotEnabled_targetResolution8000x6000() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- capabilities = intArrayOf(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
- ),
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase =
- createUseCaseByResolutionSelector(FAKE_USE_CASE, preferredResolution = Size(8000, 6000))
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
-
- // Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(4032, 3024))
- }
-
- @Config(minSdk = Build.VERSION_CODES.M)
- @Test
- fun highResolutionIsSelected_whenHighResolutionIsEnabled_aspectRatio16x9() {
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
- capabilities = intArrayOf(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
- ),
- supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase = createUseCaseByResolutionSelector(
- FAKE_USE_CASE,
- preferredAspectRatio = AspectRatio.RATIO_16_9,
- highResolutionEnabled = true
- )
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false, supportedSurfaceCombination, useCase)
-
- // Checks 8000x6000 is final selected for the use case.
- assertThat(suggestedStreamSpecMap[useCase]?.resolution).isEqualTo(Size(8000, 4500))
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_single_valid_targetFPS() {
- // a valid target means the device is capable of that fps
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- // use case with target fps
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(25, 30)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1
- )
- // single selected size should be equal to 3840 x 2160
- assertThat(suggestedStreamSpecMap[useCase1]!!.resolution).isEqualTo(Size(3840, 2160))
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_single_invalid_targetFPS() {
- // an invalid target means the device would neve be able to reach that fps
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- // use case with target fps
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(65, 70)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1
- )
- // single selected size should be equal to 3840 x 2160
- assertThat(suggestedStreamSpecMap[useCase1]!!.resolution).isEqualTo(Size(800, 450))
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_multiple_targetFPS_first_is_larger() {
- // a valid target means the device is capable of that fps
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 35),
- surfaceOccupancyPriority = 1
- )
-
- val useCase2 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(15, 25)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- useCase2
- )
- // both selected size should be no larger than 1920 x 1080
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1445)))
- .isTrue()
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1445)))
- .isTrue()
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_multiple_targetFPS_first_is_smaller() {
- // a valid target means the device is capable of that fps
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 35),
- surfaceOccupancyPriority = 1
- )
-
- val useCase2 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(45, 50)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- useCase2
- )
- // both selected size should be no larger than 1920 x 1440
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
- .isTrue()
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
- .isTrue()
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_multiple_targetFPS_intersect() {
- // first and second new use cases have target fps that intersect each other
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 40),
- surfaceOccupancyPriority = 1
- )
-
- val useCase2 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(35, 45)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- useCase2
- )
- // effective target fps becomes 35-40
- // both selected size should be no larger than 1920 x 1080
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1080)))
- .isTrue()
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1080)))
- .isTrue()
- }
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_multiple_cases_first_has_targetFPS() {
- // first new use case has a target fps, second new use case does not
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 35),
- surfaceOccupancyPriority = 1
- )
-
- val useCase2 = createUseCaseByLegacyApi(
- FAKE_USE_CASE
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- useCase2
- )
- // both selected size should be no larger than 1920 x 1440
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
- .isTrue()
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
- .isTrue()
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_multiple_cases_second_has_targetFPS() {
- // second new use case does not have a target fps, first new use case does not
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE
- )
- val useCase2 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 35)
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- useCase2
- )
- // both selected size should be no larger than 1920 x 1440
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1920, 1440)))
- .isTrue()
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase2]!!.resolution, Size(1920, 1440)))
- .isTrue()
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_attached_with_targetFPS_no_new_targetFPS() {
- // existing surface with target fps + new use case without a target fps
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- // existing surface w/ target fps
- val attachedSurfaceInfo = AttachedSurfaceInfo.create(
- SurfaceConfig.create(
- ConfigType.JPEG,
- ConfigSize.PREVIEW
- ), ImageFormat.JPEG,
- Size(1280, 720), Range(40, 50)
- )
-
- // new use case with no target fps
- val useCase1 = createUseCaseByLegacyApi(FAKE_USE_CASE)
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- attachedSurfaces = listOf(attachedSurfaceInfo)
- )
- // size should be no larger than 1280 x 960
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 960)))
- .isTrue()
- }
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
- // existing surface with target fps + new use case with target fps that does not intersect
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- // existing surface w/ target fps
- val attachedSurfaceInfo = AttachedSurfaceInfo.create(
- SurfaceConfig.create(
- ConfigType.JPEG,
- ConfigSize.PREVIEW
- ), ImageFormat.JPEG,
- Size(1280, 720), Range(40, 50)
- )
-
- // new use case with target fps
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(30, 35)
-
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- attachedSurfaces = listOf(attachedSurfaceInfo)
- )
- // size of new surface should be no larger than 1280 x 960
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 960)))
- .isTrue()
- }
-
- @Test
- @Throws(CameraUnavailableException::class, CameraAccessExceptionCompat::class)
- fun getSupportedOutputSizes_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
- // existing surface with target fps + new use case with target fps that intersect each other
- setupCameraAndInitCameraX(
- hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
- )
- val supportedSurfaceCombination = SupportedSurfaceCombination(
- context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
- )
-
- // existing surface w/ target fps
- val attachedSurfaceInfo = AttachedSurfaceInfo.create(
- SurfaceConfig.create(
- ConfigType.JPEG,
- ConfigSize.PREVIEW
- ), ImageFormat.JPEG,
- Size(1280, 720), Range(40, 50)
- )
-
- // new use case with target fps
- val useCase1 = createUseCaseByLegacyApi(
- FAKE_USE_CASE,
- targetFrameRate = Range<Int>(45, 50)
-
- )
-
- val suggestedStreamSpecMap = getSuggestedStreamSpecMap(
- false,
- supportedSurfaceCombination,
- useCase1,
- attachedSurfaces = listOf(attachedSurfaceInfo)
- )
- // size of new surface should be no larger than 1280 x 720
- assertThat(sizeIsAtMost(suggestedStreamSpecMap[useCase1]!!.resolution, Size(1280, 720)))
- .isTrue()
- }
-
- /**
- * Helper function that returns whether size is <= maxSize
- *
- */
- private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
- return (size.height * size.width) <= (maxSize.height * maxSize.width)
- }
-
/**
* Sets up camera according to the specified settings and initialize [CameraX].
*
@@ -3255,17 +1673,134 @@
}
/**
+ * Sets up camera according to the specified settings.
+ *
+ * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
+ * @param hardwareLevel the hardware level of the camera. Default value is
+ * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
+ * @param sensorOrientation the sensor orientation of the camera. Default value is
+ * [SENSOR_ORIENTATION_90].
+ * @param pixelArraySize the active pixel array size of the camera. Default value is
+ * [LANDSCAPE_PIXEL_ARRAY_SIZE].
+ * @param supportedSizes the supported sizes of the camera. Default value is
+ * [DEFAULT_SUPPORTED_SIZES].
+ * @param capabilities the capabilities of the camera. Default value is null.
+ */
+ fun setupCamera(
+ cameraId: String = DEFAULT_CAMERA_ID,
+ hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ sensorOrientation: Int = SENSOR_ORIENTATION_90,
+ pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
+ supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+ supportedHighResolutionSizes: Array<Size>? = null,
+ capabilities: IntArray? = null
+ ) {
+ val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
+ // Sets up the supported sizes
+ Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt()))
+ .thenReturn(supportedSizes)
+ // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
+ // output sizes need to be retrieved via SurfaceTexture.class.
+ Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java))
+ .thenReturn(supportedSizes)
+ // This is setup for the test to determine RECORD size from StreamConfigurationMap
+ Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java))
+ .thenReturn(supportedSizes)
+
+ // setup to return different minimum frame durations depending on resolution
+ // minimum frame durations were designated only for the purpose of testing
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(4032, 3024))
+ ))
+ .thenReturn(50000000L) // 20 fps, size maximum
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(3840, 2160))
+ ))
+ .thenReturn(40000000L) // 25, size record
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(1920, 1440))
+ ))
+ .thenReturn(30000000L) // 30
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(1920, 1080))
+ ))
+ .thenReturn(28000000L) // 35
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(1280, 960))
+ ))
+ .thenReturn(25000000L) // 40
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(1280, 720))
+ ))
+ .thenReturn(22000000L) // 45, size preview/display
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(960, 544))
+ ))
+ .thenReturn(20000000L) // 50
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(800, 450))
+ ))
+ .thenReturn(16666000L) // 60fps
+
+ Mockito.`when`(it.getOutputMinFrameDuration(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.eq(Size(640, 480))
+ ))
+ .thenReturn(16666000L) // 60fps
+
+ // Sets up the supported high resolution sizes
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Mockito.`when`(it.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
+ .thenReturn(supportedHighResolutionSizes)
+ }
+ }
+
+ val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+ Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
+ set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
+ set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
+ set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
+ set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
+ set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+ capabilities?.let {
+ set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
+ }
+ }
+
+ val cameraManager = ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
+ .addCamera(cameraId, characteristics)
+ }
+
+ /**
* Initializes the [CameraX].
*/
private fun initCameraX() {
val surfaceManagerProvider =
CameraDeviceSurfaceManager.Provider { context, _, availableCameraIds ->
- Camera2DeviceSurfaceManager(
+ cameraDeviceSurfaceManager = Camera2DeviceSurfaceManager(
context,
mockCamcorderProfileHelper,
CameraManagerCompat.from([email protected]),
availableCameraIds
)
+ cameraDeviceSurfaceManager
}
val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
@@ -3298,7 +1833,9 @@
removeAt(index)
}
if (!supportedSurfaceCombination.checkSupported(
- isConcurrentCameraModeOn, subConfigurationList)) {
+ isConcurrentCameraModeOn, subConfigurationList
+ )
+ ) {
return false
}
}
@@ -3306,164 +1843,27 @@
return true
}
- /**
- * Gets the suggested resolution map by the converted ResolutionSelector use case config which
- * will also be converted when a use case is bound to the lifecycle.
- */
- private fun getSuggestedStreamSpecMap(
- isConcurrentCameraModeOn: Boolean,
- supportedSurfaceCombination: SupportedSurfaceCombination,
- vararg useCases: UseCase,
- attachedSurfaces: List<AttachedSurfaceInfo>? = null,
- cameraFactory: CameraFactory = this.cameraFactory!!,
- cameraId: String = DEFAULT_CAMERA_ID,
- useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
- ): Map<UseCase, StreamSpec?> {
- // Generates the use case to new ResolutionSelector use case config map
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory.getCamera(cameraId).cameraInfoInternal,
- listOf(*useCases),
- useCaseConfigFactory
- )
- // Uses the use case config list to get suggested stream specs
- val useCaseConfigStreamSpecMap = supportedSurfaceCombination
- .getSuggestedStreamSpecifications(
- isConcurrentCameraModeOn,
- attachedSurfaces ?: emptyList(),
- mutableListOf<UseCaseConfig<*>?>().apply { addAll(useCaseToConfigMap.values) }
- )
- val useCaseStreamSpecMap = mutableMapOf<UseCase, StreamSpec?>()
- // Maps the use cases to the suggestion resolutions
- for (useCase in useCases) {
- useCaseStreamSpecMap[useCase] = useCaseConfigStreamSpecMap[useCaseToConfigMap[useCase]]
- }
- return useCaseStreamSpecMap
- }
-
- /**
- * Gets the supported output sizes by the converted ResolutionSelector use case config which
- * will also be converted when a use case is bound to the lifecycle.
- */
- private fun getSupportedOutputSizes(
- supportedSurfaceCombination: SupportedSurfaceCombination,
- useCase: UseCase,
- cameraId: String = DEFAULT_CAMERA_ID,
- useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
- ): List<Size?> {
- // Converts the use case config to new ResolutionSelector config
- val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
- listOf(useCase),
- useCaseConfigFactory
- )
- return supportedSurfaceCombination.getSupportedOutputSizes(useCaseToConfigMap[useCase]!!)
- }
-
- /**
- * Creates [Preview], [ImageCapture], [ImageAnalysis] or FakeUseCase according to the specified
- * settings.
- *
- * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis] and FakeUseCase should
- * be created.
- * @param targetRotation the target rotation setting. Default is UNKNOWN_ROTATION and no target
- * rotation will be set to the created use case.
- * @param targetAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
- * and no target aspect ratio will be set to the created use case.
- * @param targetResolution the target resolution setting which should still be specified in the
- * legacy API approach. The size should be expressed in the coordinate frame after rotating the
- * supported sizes by the target rotation. Default is null.
- * @param maxResolution the max resolution setting. Default is null.
- * @param defaultResolution the default resolution setting. Default is null.
- * @param supportedResolutions the customized supported resolutions. Default is null.
- * @param customOrderedResolutions the custom ordered resolutions. Default is null.
- */
- private fun createUseCaseByLegacyApi(
- useCaseType: Int,
- targetRotation: Int = UNKNOWN_ROTATION,
- targetAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
- targetResolution: Size? = null,
- targetFrameRate: Range<Int>? = null,
- surfaceOccupancyPriority: Int = -1,
- maxResolution: Size? = null,
- defaultResolution: Size? = null,
- supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
- customOrderedResolutions: List<Size>? = null,
+ private fun createUseCase(
+ captureType: CaptureType,
+ targetFrameRate: Range<Int>? = null
): UseCase {
- val builder = when (useCaseType) {
- PREVIEW_USE_CASE -> Preview.Builder()
- IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
- IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
- else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+ val builder = FakeUseCaseConfig.Builder(captureType, when (captureType) {
+ CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
+ CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
+ else -> INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ })
+ targetFrameRate?.let {
+ builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
}
- if (targetRotation != UNKNOWN_ROTATION) {
- builder.setTargetRotation(targetRotation)
- }
- if (targetAspectRatio != UNKNOWN_ASPECT_RATIO) {
- builder.setTargetAspectRatio(targetAspectRatio)
- }
- if (surfaceOccupancyPriority >= 0) {
- builder.setSurfaceOccupancyPriority(surfaceOccupancyPriority)
- }
- builder.mutableConfig.insertOption(OPTION_TARGET_FRAME_RATE, targetFrameRate)
- targetResolution?.let { builder.setTargetResolution(it) }
- maxResolution?.let { builder.setMaxResolution(it) }
- defaultResolution?.let { builder.setDefaultResolution(it) }
- supportedResolutions?.let { builder.setSupportedResolutions(it) }
- customOrderedResolutions?.let { builder.setCustomOrderedResolutions(it) }
return builder.build()
}
- /** Creates a VideoCapture with a default QualitySelector */
- private fun createVideoCapture(): VideoCapture<TestVideoOutput> {
- return createVideoCapture(VideoSpec.QUALITY_SELECTOR_AUTO)
- }
-
- /** Creates a VideoCapture with one ore more specific Quality */
- private fun createVideoCapture(vararg quality: Quality): VideoCapture<TestVideoOutput> {
- return createVideoCapture(QualitySelector.fromOrderedList(listOf(*quality)))
- }
-
- /** Creates a VideoCapture with a customized QualitySelector */
- private fun createVideoCapture(qualitySelector: QualitySelector):
- VideoCapture<TestVideoOutput> {
- val mediaSpec = MediaSpec.builder().configureVideo {
- it.setQualitySelector(
- qualitySelector
- )
- }.build()
- val videoOutput = TestVideoOutput()
- videoOutput.mediaSpecObservable.setState(mediaSpec)
- return VideoCapture.withOutput(videoOutput)
- }
-
- /** A fake implementation of VideoOutput */
- private class TestVideoOutput : VideoOutput {
- var mediaSpecObservable: MutableStateObservable<MediaSpec> =
- MutableStateObservable.withInitialState(MediaSpec.builder().build())
- var surfaceRequest: SurfaceRequest? = null
- var sourceState: SourceState? = null
- override fun onSurfaceRequested(@NonNull request: SurfaceRequest) {
- surfaceRequest = request
- }
- override fun getMediaSpec() = mediaSpecObservable
- override fun onSourceStateChanged(@NonNull sourceState: SourceState) {
- this.sourceState = sourceState
- }
- }
-
- private interface UseCaseCreator {
- fun createUseCase(
- useCaseType: Int,
- targetRotation: Int = UNKNOWN_ROTATION,
- preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
- preferredResolution: Size? = null,
- targetFrameRate: Range<Int>? = null,
- surfaceOccupancyPriority: Int = -1,
- maxResolution: Size? = null,
- highResolutionEnabled: Boolean = false,
- defaultResolution: Size? = null,
- supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
- customOrderedResolutions: List<Size>? = null,
- ): UseCase
+ private fun createRawUseCase(): UseCase {
+ val builder = FakeUseCaseConfig.Builder()
+ builder.mutableConfig.insertOption(
+ UseCaseConfig.OPTION_INPUT_FORMAT,
+ ImageFormat.RAW_SENSOR
+ )
+ return builder.build()
}
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
index 4427cbc..c485a90 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
@@ -33,6 +33,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
+import org.mockito.Mockito.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
@@ -120,15 +121,17 @@
val listener = mock(CameraCoordinator.ConcurrentCameraModeListener::class.java)
cameraCoordinator.addListener(listener)
cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
- verify(listener).notifyConcurrentCameraModeUpdated(CAMERA_OPERATING_MODE_CONCURRENT)
+ verify(listener).onCameraOperatingModeUpdated(
+ CAMERA_OPERATING_MODE_UNSPECIFIED, CAMERA_OPERATING_MODE_CONCURRENT)
cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
- verify(listener).notifyConcurrentCameraModeUpdated(CAMERA_OPERATING_MODE_SINGLE)
+ verify(listener).onCameraOperatingModeUpdated(
+ CAMERA_OPERATING_MODE_CONCURRENT, CAMERA_OPERATING_MODE_SINGLE)
reset(listener)
cameraCoordinator.removeListener(listener)
cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
- verify(listener, never()).notifyConcurrentCameraModeUpdated(
- CAMERA_OPERATING_MODE_CONCURRENT)
+ verify(listener, never()).onCameraOperatingModeUpdated(
+ anyInt(), anyInt())
}
private class FakeCameraManagerImpl : CameraManagerCompat.CameraManagerCompatImpl {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index eda23ef..287d3d4a 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -21,8 +21,11 @@
import android.util.Rational
import android.util.Size
import android.view.Surface
+import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
import androidx.camera.core.concurrent.CameraCoordinator
-import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.SessionConfig
@@ -43,20 +46,18 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
@SmallTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 21)
class UseCaseTest {
- private var mockCameraInternal: CameraInternal? = null
+ private lateinit var fakeCamera: FakeCamera
+ private lateinit var fakeFrontCamera: FakeCamera
@Before
fun setup() {
- mockCameraInternal = Mockito.mock(
- CameraInternal::class.java
- )
+ fakeCamera = FakeCamera()
+ fakeFrontCamera = FakeCamera(null, FakeCameraInfoInternal(0, LENS_FACING_FRONT))
}
@Test
@@ -92,46 +93,42 @@
@Test
fun removeListener() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
- testUseCase.unbindFromCamera(mockCameraInternal!!)
+ testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.unbindFromCamera(fakeCamera)
testUseCase.notifyActive()
- Mockito.verify(mockCameraInternal, Mockito.never())!!.onUseCaseActive(
- ArgumentMatchers.any(
- UseCase::class.java
- )
- )
+ assertThat(fakeCamera.useCaseActiveHistory).isEmpty()
}
@Test
fun notifyActiveState() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null)
testUseCase.notifyActive()
- Mockito.verify(mockCameraInternal, Mockito.times(1))!!.onUseCaseActive(testUseCase)
+ assertThat(fakeCamera.useCaseActiveHistory[0]).isEqualTo(testUseCase)
}
@Test
fun notifyInactiveState() {
val testUseCase = createFakeUseCase()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null)
testUseCase.notifyInactive()
- Mockito.verify(mockCameraInternal, Mockito.times(1))!!.onUseCaseInactive(testUseCase)
+ assertThat(fakeCamera.useCaseInactiveHistory[0]).isEqualTo(testUseCase)
}
@Test
fun notifyUpdatedSettings() {
val testUseCase = FakeUseCase()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null)
testUseCase.notifyUpdated()
- Mockito.verify(mockCameraInternal, Mockito.times(1))!!.onUseCaseUpdated(testUseCase)
+ assertThat(fakeCamera.useCaseUpdateHistory[0]).isEqualTo(testUseCase)
}
@Test
fun notifyResetUseCase() {
val testUseCase = FakeUseCase()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
+ testUseCase.bindToCamera(fakeCamera, null, null)
testUseCase.notifyReset()
- Mockito.verify(mockCameraInternal, Mockito.times(1))!!.onUseCaseReset(testUseCase)
+ assertThat(fakeCamera.useCaseResetHistory[0]).isEqualTo(testUseCase)
}
@Test
@@ -150,8 +147,8 @@
val testUseCase = FakeUseCase()
testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
assertThat(testUseCase.attachedSurfaceResolution).isNotNull()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
- testUseCase.unbindFromCamera(mockCameraInternal!!)
+ testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.attachedSurfaceResolution).isNull()
}
@@ -160,8 +157,8 @@
val testUseCase = FakeUseCase()
testUseCase.updateSuggestedStreamSpec(TEST_STREAM_SPEC)
assertThat(testUseCase.attachedStreamSpec).isNotNull()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
- testUseCase.unbindFromCamera(mockCameraInternal!!)
+ testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.attachedStreamSpec).isNull()
}
@@ -170,8 +167,8 @@
val testUseCase = FakeUseCase()
testUseCase.setViewPortCropRect(Rect(0, 0, 640, 480))
assertThat(testUseCase.viewPortCropRect).isNotNull()
- testUseCase.bindToCamera(mockCameraInternal!!, null, null)
- testUseCase.unbindFromCamera(mockCameraInternal!!)
+ testUseCase.bindToCamera(fakeCamera, null, null)
+ testUseCase.unbindFromCamera(fakeCamera)
assertThat(testUseCase.viewPortCropRect).isNull()
}
@@ -262,13 +259,50 @@
assertThat(resolutionInfo!!.cropRect).isEqualTo(Rect(0, 60, 640, 420))
}
+ @Test
+ fun defaultMirrorModeIsOff() {
+ val fakeUseCase = createFakeUseCase()
+ assertThat(fakeUseCase.mirrorModeInternal).isEqualTo(MIRROR_MODE_OFF)
+ }
+
+ @Test
+ fun canGetSetMirrorMode() {
+ val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_ON)
+ assertThat(fakeUseCase.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON)
+ }
+
+ @Test
+ fun setMirrorModeOff_isMirroringRequiredIsFalse() {
+ val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_OFF)
+ assertThat(fakeUseCase.isMirroringRequired(fakeCamera)).isFalse()
+ assertThat(fakeUseCase.isMirroringRequired(fakeFrontCamera)).isFalse()
+ }
+
+ @Test
+ fun setMirrorModeOn_isMirroringRequiredIsTrue() {
+ val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_ON)
+ assertThat(fakeUseCase.isMirroringRequired(fakeCamera)).isTrue()
+ assertThat(fakeUseCase.isMirroringRequired(fakeFrontCamera)).isTrue()
+ }
+
+ @Test
+ fun setMirrorModeFrontOn_isMirroringRequiredDependsOnCamera() {
+ val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_FRONT_ON)
+ assertThat(fakeUseCase.isMirroringRequired(fakeCamera)).isFalse()
+ assertThat(fakeUseCase.isMirroringRequired(fakeFrontCamera)).isTrue()
+ }
+
private fun createFakeUseCase(
- targetRotation: Int = Surface.ROTATION_0
+ targetRotation: Int = Surface.ROTATION_0,
+ mirrorMode: Int? = null,
): FakeUseCase {
return FakeUseCase(
FakeUseCaseConfig.Builder()
.setTargetName("UseCase")
.setTargetRotation(targetRotation)
+ .apply {
+ mirrorMode?.let { setMirrorMode(it) }
+ }
.useCaseConfig
)
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 977c007..8a7e1ee 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -1258,6 +1258,18 @@
}
/**
+ * setMirrorMode is not supported on ImageAnalysis.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
+ throw new UnsupportedOperationException("setMirrorMode is not supported.");
+ }
+
+ /**
* Sets the resolution of the intended target from this configuration.
*
* <p>The target resolution attempts to establish a minimum bound for the image resolution.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2511c3d..8e209fe 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -2761,6 +2761,18 @@
}
/**
+ * setMirrorMode is not supported on ImageCapture.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
+ throw new UnsupportedOperationException("setMirrorMode is not supported.");
+ }
+
+ /**
* Sets the intended output target resolution.
*
* <p>The target resolution attempts to establish a minimum bound for the image resolution.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
new file mode 100644
index 0000000..a4ca7f7
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.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.camera.core;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+// TODO: to public API
+/**
+ * The mirror mode.
+ *
+ * @hide
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class MirrorMode {
+ /** No mirror effect will be applied. */
+ public static final int MIRROR_MODE_OFF = 0;
+
+ /** The mirror effect is always applied. */
+ public static final int MIRROR_MODE_ON = 1;
+
+ /**
+ * The mirroring effect is applied only when the lens facing of the associated camera is
+ * {@link CameraSelector#LENS_FACING_FRONT}.
+ */
+ public static final int MIRROR_MODE_FRONT_ON = 2;
+
+ private MirrorMode() {
+ }
+
+ /**
+ * @hide
+ */
+ @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_FRONT_ON})
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @interface Mirror {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index dfd629a..f5006b3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -17,10 +17,12 @@
package androidx.camera.core;
import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MIRROR_MODE;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.PreviewConfig.OPTION_BACKGROUND_EXECUTOR;
import static androidx.camera.core.impl.PreviewConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
@@ -265,7 +267,7 @@
new Matrix(),
camera.getHasTransform(),
requireNonNull(getCropRect(streamSpec.getResolution())),
- getRelativeRotation(camera, camera.isFrontFacing()),
+ getRelativeRotation(camera, isMirroringRequired(camera)),
shouldMirror(camera));
mCameraEdge.addOnInvalidatedListener(this::notifyReset);
SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(mCameraEdge);
@@ -303,7 +305,7 @@
// Since PreviewView cannot mirror, we will always mirror preview stream during buffer
// copy. If there has been a buffer copy, it means it's already mirrored. Otherwise,
// mirror it for the front camera.
- return camera.getHasTransform() && camera.isFrontFacing();
+ return camera.getHasTransform() && isMirroringRequired(camera);
}
/**
@@ -398,12 +400,12 @@
if (mNode == null) {
surfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(
cropRect,
- getRelativeRotation(cameraInternal, cameraInternal.isFrontFacing()),
+ getRelativeRotation(cameraInternal, isMirroringRequired(cameraInternal)),
getAppTargetRotation(),
cameraInternal.getHasTransform()));
} else {
mCameraEdge.setRotationDegrees(
- getRelativeRotation(cameraInternal, cameraInternal.isFrontFacing()));
+ getRelativeRotation(cameraInternal, isMirroringRequired(cameraInternal)));
}
}
}
@@ -742,6 +744,7 @@
public static final class Defaults implements ConfigProvider<PreviewConfig> {
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 2;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
+ private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_FRONT_ON;
private static final PreviewConfig DEFAULT_CONFIG;
@@ -786,6 +789,7 @@
}
setTargetClass(Preview.class);
+ mutableConfig.insertOption(OPTION_MIRROR_MODE, Defaults.DEFAULT_MIRROR_MODE);
}
/**
@@ -967,6 +971,18 @@
}
/**
+ * setMirrorMode is not supported on Preview.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
+ throw new UnsupportedOperationException("setMirrorMode is not supported.");
+ }
+
+ /**
* Sets the resolution of the intended target from this configuration.
*
* <p>The target resolution attempts to establish a minimum bound for the preview
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index f5e76b1..465e198 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -16,6 +16,9 @@
package androidx.camera.core;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.camera.core.processing.TargetUtils.isSuperset;
import static androidx.core.util.Preconditions.checkArgument;
@@ -325,6 +328,39 @@
}
/**
+ * Returns the mirror mode.
+ *
+ * <p>If mirror mode is not set, defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @MirrorMode.Mirror
+ protected int getMirrorModeInternal() {
+ return ((ImageOutputConfig) mCurrentConfig).getMirrorMode(MIRROR_MODE_OFF);
+ }
+
+ /**
+ * Returns if the mirroring is required with the associated camera.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public boolean isMirroringRequired(@NonNull CameraInternal camera) {
+ int mirrorMode = getMirrorModeInternal();
+ switch (mirrorMode) {
+ case MIRROR_MODE_OFF:
+ return false;
+ case MIRROR_MODE_ON:
+ return true;
+ case MIRROR_MODE_FRONT_ON:
+ return camera.isFrontFacing();
+ default:
+ throw new AssertionError("Unknown mirrorMode: " + mirrorMode);
+ }
+ }
+
+ /**
* Returns the target rotation set by apps explicitly.
*
* @return The rotation of the intended target.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
index 31b6bfc..7b37c34 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
@@ -88,10 +88,10 @@
/**
* Returns paired camera id in concurrent mode.
*
- * <p>The paired camera id dictionary is constructed when {@link CameraCoordinator#init()} is
- * called. This internal API is used to look up paired camera id when coordinating device
- * open and session config in {@link CameraStateRegistry}. Currently only dual cameras will
- * be supported in concurrent mode.
+ * <p>The paired camera id dictionary is constructed when constructor is called. This
+ * internal API is used to look up paired camera id when coordinating device open and session
+ * config in {@link CameraStateRegistry}. Currently only dual cameras will be supported in
+ * concurrent mode.
*
* @param cameraId camera id.
* @return The paired camera id if exists or null if paired camera not exists.
@@ -132,12 +132,13 @@
/**
* Interface for concurrent camera mode update.
*
- * <p>Everytime user sets concurrent mode, the observer will be notified and update related
- * states or parameters accordingly. E.g. in
- * {@link CameraStateRegistry}, we will update the number of max
- * allowed cameras if concurrent mode is set.
+ * <p>Everytime user changes {@link CameraOperatingMode}, the observer will be notified and
+ * update related states or parameters accordingly. E.g. in {@link CameraStateRegistry}, we
+ * will update the number of max allowed cameras.
*/
interface ConcurrentCameraModeListener {
- void notifyConcurrentCameraModeUpdated(@CameraOperatingMode int cameraOperatingMode);
+ void onCameraOperatingModeUpdated(
+ @CameraOperatingMode int prevMode,
+ @CameraOperatingMode int currMode);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index 66e9cf6..48bd32c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -87,12 +87,16 @@
/**
* Retrieves a map of suggested stream specifications for the given list of use cases.
*
- * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise false.
- * @param cameraId the camera id of the camera device used by the use cases
- * @param existingSurfaces list of surfaces already configured and used by the camera. The
- * stream specifications for these surface can not change.
- * @param newUseCaseConfigs list of configurations of the use cases that will be given a
- * suggested stream specification
+ * @param isConcurrentCameraModeOn true if concurrent camera mode is on, otherwise
+ * false.
+ * @param cameraId the camera id of the camera device used by the
+ * use cases
+ * @param existingSurfaces list of surfaces already configured and used by
+ * the camera. The stream specifications for these
+ * surface can not change.
+ * @param newUseCaseConfigsSupportedSizeMap map of configurations of the use cases to the
+ * supported output sizes list that will be given a
+ * suggested stream specification
* @return map of suggested stream specifications for given use cases
* @throws IllegalStateException if not initialized
* @throws IllegalArgumentException if {@code newUseCaseConfigs} is an empty list, if
@@ -105,5 +109,5 @@
boolean isConcurrentCameraModeOn,
@NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
- @NonNull List<UseCaseConfig<?>> newUseCaseConfigs);
+ @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
index 76831a0..a80242d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
@@ -67,6 +67,16 @@
*/
OPEN(/*holdsCameraSlot=*/true),
/**
+ * Camera is open and capture session is configured. This state is only used for concurrent
+ * camera.
+ *
+ * <p>In concurrent mode, CONFIGURED refers to camera is opened and capture session is
+ * configured, to differentiate from OPEN, which refers to camera device is opened but
+ * capture session is not configured yet. External users will only see OPEN state, no
+ * matter the internal state is CONFIGURED or OPEN.
+ */
+ CONFIGURED(/*holdsCameraSlot=*/true),
+ /**
* Camera is in the process of closing.
*
* <p>This is a transient state.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraStateRegistry.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraStateRegistry.java
index cea9370..a2c17cc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraStateRegistry.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraStateRegistry.java
@@ -16,6 +16,8 @@
package androidx.camera.core.impl;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
+
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -23,6 +25,8 @@
import androidx.annotation.WorkerThread;
import androidx.camera.core.Camera;
import androidx.camera.core.Logger;
+import androidx.camera.core.concurrent.CameraCoordinator;
+import androidx.camera.core.concurrent.CameraCoordinator.CameraOperatingMode;
import androidx.core.util.Preconditions;
import java.util.HashMap;
@@ -39,13 +43,23 @@
* there is a slot available to open a camera.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class CameraStateRegistry {
+public final class CameraStateRegistry implements CameraCoordinator.ConcurrentCameraModeListener {
private static final String TAG = "CameraStateRegistry";
+
+ /**
+ * Only two cameras are allowed in concurrent mode now.
+ */
+ private static final int MAX_ALLOWED_CONCURRENT_CAMERAS_IN_SINGLE_MODE = 1;
+ private static final int MAX_ALLOWED_CONCURRENT_CAMERAS_IN_CONCURRENT_MODE = 2;
+
private final StringBuilder mDebugString = new StringBuilder();
private final Object mLock = new Object();
- private final int mMaxAllowedOpenedCameras;
+ private int mMaxAllowedOpenedCameras;
+
+ @GuardedBy("mLock")
+ private final CameraCoordinator mCameraCoordinator;
@GuardedBy("mLock")
private final Map<Camera, CameraRegistration> mCameraStates = new HashMap<>();
@GuardedBy("mLock")
@@ -55,11 +69,15 @@
/**
* Creates a new registry with a limit of {@code maxAllowedOpenCameras} allowed to be opened.
*
+ * @param cameraCoordinator The camera coordinator for conucurrent cameras.
* @param maxAllowedOpenedCameras The limit for number of simultaneous open cameras.
*/
- public CameraStateRegistry(int maxAllowedOpenedCameras) {
+ public CameraStateRegistry(
+ @NonNull CameraCoordinator cameraCoordinator,
+ int maxAllowedOpenedCameras) {
mMaxAllowedOpenedCameras = maxAllowedOpenedCameras;
- synchronized ("mLock") {
+ synchronized (mLock) {
+ mCameraCoordinator = cameraCoordinator;
mAvailableCameras = mMaxAllowedOpenedCameras;
}
}
@@ -77,14 +95,20 @@
* {@link CameraInternal.State#RELEASED} state.
*
* @param camera The camera to register.
+ * @param notifyExecutor The executor to notify camera device opened or capture session
+ * configured.
+ * @param onOpenAvailableListener The listener for camera device open available.
+ * @param onConfigureAvailableListener The listener for camera capture session configure
+ * available.
*/
public void registerCamera(@NonNull Camera camera, @NonNull Executor notifyExecutor,
- @NonNull OnOpenAvailableListener cameraAvailableListener) {
+ @NonNull OnConfigureAvailableListener onConfigureAvailableListener,
+ @NonNull OnOpenAvailableListener onOpenAvailableListener) {
synchronized (mLock) {
Preconditions.checkState(!mCameraStates.containsKey(camera), "Camera is "
+ "already registered: " + camera);
- mCameraStates.put(camera,
- new CameraRegistration(null, notifyExecutor, cameraAvailableListener));
+ mCameraStates.put(camera, new CameraRegistration(null, notifyExecutor,
+ onConfigureAvailableListener, onOpenAvailableListener));
}
}
@@ -95,9 +119,12 @@
* open, then this will return {@code false}, and the caller should not attempt to open the
* camera. Instead, the caller should mark its state as
* {@link CameraInternal.State#PENDING_OPEN} with
- * {@link #markCameraState(Camera, CameraInternal.State)}, and the listener registered with
- * {@link #registerCamera(Camera, Executor, OnOpenAvailableListener)} will be notified when a
- * camera becomes available. At that time, the caller should attempt to call this method again.
+ * {@link #markCameraState(Camera, CameraInternal.State)} and the listener
+ * registered with {@link #registerCamera(Camera, Executor,OnConfigureAvailableListener,
+ * OnOpenAvailableListener)} will be notified when a camera becomes available. At that
+ * time, the caller should attempt to call this method again.
+ *
+ * @param camera The camera instance.
*
* @return {@code true} if it is safe to open the camera. If this returns {@code true}, it is
* assumed the camera is now in an {@link CameraInternal.State#OPENING} state, and the
@@ -138,6 +165,34 @@
}
/**
+ * Checks if opening capture session is allowed in concurrent camera mode.
+ *
+ * @param cameraId The camera id.
+ * @param pairedCameraId The paired camera id.
+ *
+ * @return True if it is safe to open the capture session, otherwise false.
+ */
+ public boolean tryOpenCaptureSession(
+ @NonNull String cameraId,
+ @Nullable String pairedCameraId) {
+ synchronized (mLock) {
+ if (mCameraCoordinator.getCameraOperatingMode() != CAMERA_OPERATING_MODE_CONCURRENT) {
+ return true;
+ }
+ CameraInternal.State selfState = getCameraRegistration(cameraId) != null
+ ? getCameraRegistration(cameraId).getState() : null;
+ CameraInternal.State pairedState =
+ (pairedCameraId != null && getCameraRegistration(pairedCameraId) != null)
+ ? getCameraRegistration(pairedCameraId).getState() : null;
+ boolean isSelfAvailable = CameraInternal.State.OPEN.equals(selfState)
+ || CameraInternal.State.CONFIGURED.equals(selfState);
+ boolean isPairAvailable = CameraInternal.State.OPEN.equals(pairedState)
+ || CameraInternal.State.CONFIGURED.equals(pairedState);
+ return isSelfAvailable && isPairAvailable;
+ }
+ }
+
+ /**
* Mark the state of a registered camera.
*
* <p>This is used to track the states of all cameras in order to determine how many cameras
@@ -146,7 +201,9 @@
* @param camera Registered camera whose state is being set
* @param state New state of the registered camera
*/
- public void markCameraState(@NonNull Camera camera, @NonNull CameraInternal.State state) {
+ public void markCameraState(
+ @NonNull Camera camera,
+ @NonNull CameraInternal.State state) {
markCameraState(camera, state, true);
}
@@ -170,7 +227,8 @@
*/
public void markCameraState(@NonNull Camera camera, @NonNull CameraInternal.State state,
boolean notifyImmediately) {
- Map<Camera, CameraRegistration> camerasToNotify = null;
+ Map<Camera, CameraRegistration> camerasToNotifyOpen = null;
+ CameraRegistration cameraToNotifyConfigure = null;
synchronized (mLock) {
CameraInternal.State previousState = null;
int previousAvailableCameras = mAvailableCameras;
@@ -185,31 +243,67 @@
return;
}
+ // In concurrent mode, if state transits to CONFIGURED, need to notify paired camera
+ // to configure capture session.
+ if (mCameraCoordinator.getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT
+ && state == CameraInternal.State.CONFIGURED) {
+ String cameraId = ((CameraInfoInternal) camera.getCameraInfo()).getCameraId();
+ String pairedCameraId = mCameraCoordinator.getPairedConcurrentCameraId(cameraId);
+ if (pairedCameraId != null) {
+ cameraToNotifyConfigure = getCameraRegistration(pairedCameraId);
+ }
+ }
+
if (previousAvailableCameras < 1 && mAvailableCameras > 0) {
// Cameras are now available, notify ALL cameras in a PENDING_OPEN state.
- camerasToNotify = new HashMap<>();
+ camerasToNotifyOpen = new HashMap<>();
for (Map.Entry<Camera, CameraRegistration> entry : mCameraStates.entrySet()) {
if (entry.getValue().getState() == CameraInternal.State.PENDING_OPEN) {
- camerasToNotify.put(entry.getKey(), entry.getValue());
+ camerasToNotifyOpen.put(entry.getKey(), entry.getValue());
}
}
} else if (state == CameraInternal.State.PENDING_OPEN && mAvailableCameras > 0) {
// This camera entered a PENDING_OPEN state while there are available cameras,
// only notify the single camera.
- camerasToNotify = new HashMap<>();
- camerasToNotify.put(camera, mCameraStates.get(camera));
+ camerasToNotifyOpen = new HashMap<>();
+ camerasToNotifyOpen.put(camera, mCameraStates.get(camera));
}
// Omit notifying this camera if `notifyImmediately` is false
- if (camerasToNotify != null && !notifyImmediately) {
- camerasToNotify.remove(camera);
+ if (camerasToNotifyOpen != null && !notifyImmediately) {
+ camerasToNotifyOpen.remove(camera);
}
}
// Notify pending cameras unlocked.
- if (camerasToNotify != null) {
- for (CameraRegistration registration : camerasToNotify.values()) {
- registration.notifyListener();
+ if (camerasToNotifyOpen != null) {
+ for (CameraRegistration registration : camerasToNotifyOpen.values()) {
+ registration.notifyOnOpenAvailableListener();
+ }
+ }
+
+ // Notify paired camera to configure for concurrent camera
+ if (cameraToNotifyConfigure != null) {
+ cameraToNotifyConfigure.notifyOnConfigureAvailableListener();
+ }
+ }
+
+ @Override
+ public void onCameraOperatingModeUpdated(
+ @CameraOperatingMode int prevMode,
+ @CameraOperatingMode int currMode) {
+ synchronized (mLock) {
+ mMaxAllowedOpenedCameras = (currMode == CAMERA_OPERATING_MODE_CONCURRENT)
+ ? MAX_ALLOWED_CONCURRENT_CAMERAS_IN_CONCURRENT_MODE
+ : MAX_ALLOWED_CONCURRENT_CAMERAS_IN_SINGLE_MODE;
+ boolean isConcurrentCameraModeOn =
+ prevMode != CAMERA_OPERATING_MODE_CONCURRENT
+ && currMode == CAMERA_OPERATING_MODE_CONCURRENT;
+ boolean isConcurrentCameraModeOff =
+ prevMode == CAMERA_OPERATING_MODE_CONCURRENT
+ && currMode != CAMERA_OPERATING_MODE_CONCURRENT;
+ if (isConcurrentCameraModeOn || isConcurrentCameraModeOff) {
+ recalculateAvailableCameras();
}
}
}
@@ -217,7 +311,7 @@
// Unregisters the given camera and returns the state before being unregistered
@GuardedBy("mLock")
@Nullable
- private CameraInternal.State unregisterCamera(Camera camera) {
+ private CameraInternal.State unregisterCamera(@NonNull Camera camera) {
CameraRegistration registration = mCameraStates.remove(camera);
if (registration != null) {
recalculateAvailableCameras();
@@ -309,6 +403,18 @@
}
}
+ @Nullable
+ @GuardedBy("mLock")
+ private CameraRegistration getCameraRegistration(@NonNull String targetCameraId) {
+ for (Camera camera : mCameraStates.keySet()) {
+ String cameraId = ((CameraInfoInternal) camera.getCameraInfo()).getCameraId();
+ if (targetCameraId.equals(cameraId)) {
+ return mCameraStates.get(camera);
+ }
+ }
+ return null;
+ }
+
/**
* A listener that is notified when a camera slot becomes available for opening.
*/
@@ -325,17 +431,32 @@
void onOpenAvailable();
}
+ /**
+ * A listener that is notified when capture session is available to config. It is used in
+ * concurrent camera mode when all of the cameras are opened.
+ */
+ public interface OnConfigureAvailableListener {
+ /**
+ * Called when a camera slot becomes available for configuring.
+ */
+ void onConfigureAvailable();
+ }
+
private static class CameraRegistration {
private CameraInternal.State mState;
private final Executor mNotifyExecutor;
- private final OnOpenAvailableListener mCameraAvailableListener;
+ private final OnConfigureAvailableListener mOnConfigureAvailableListener;
+ private final OnOpenAvailableListener mOnOpenAvailableListener;
- CameraRegistration(@Nullable CameraInternal.State initialState,
+ CameraRegistration(
+ @Nullable CameraInternal.State initialState,
@NonNull Executor notifyExecutor,
- @NonNull OnOpenAvailableListener cameraAvailableListener) {
+ @NonNull OnConfigureAvailableListener onConfigureAvailableListener,
+ @NonNull OnOpenAvailableListener onOpenAvailableListener) {
mState = initialState;
mNotifyExecutor = notifyExecutor;
- mCameraAvailableListener = cameraAvailableListener;
+ mOnConfigureAvailableListener = onConfigureAvailableListener;
+ mOnOpenAvailableListener = onOpenAvailableListener;
}
CameraInternal.State setState(@Nullable CameraInternal.State state) {
@@ -348,11 +469,19 @@
return mState;
}
- void notifyListener() {
+ void notifyOnConfigureAvailableListener() {
try {
- mNotifyExecutor.execute(mCameraAvailableListener::onOpenAvailable);
+ mNotifyExecutor.execute(mOnConfigureAvailableListener::onConfigureAvailable);
} catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to notify camera.", e);
+ Logger.e(TAG, "Unable to notify camera to configure.", e);
+ }
+ }
+
+ void notifyOnOpenAvailableListener() {
+ try {
+ mNotifyExecutor.execute(mOnOpenAvailableListener::onOpenAvailable);
+ } catch (RejectedExecutionException e) {
+ Logger.e(TAG, "Unable to notify camera to open.", e);
}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index 3a2cbfb..a2168c3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -28,6 +28,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.AspectRatio;
+import androidx.camera.core.MirrorMode;
import androidx.camera.core.ResolutionSelector;
import java.lang.annotation.Retention;
@@ -72,6 +73,12 @@
Option.create("camerax.core.imageOutput.appTargetRotation", int.class);
/**
+ * Option: camerax.core.imageOutput.mirrorMode
+ */
+ Option<Integer> OPTION_MIRROR_MODE =
+ Option.create("camerax.core.imageOutput.mirrorMode", int.class);
+
+ /**
* Option: camerax.core.imageOutput.targetResolution
*/
Option<Size> OPTION_TARGET_RESOLUTION =
@@ -183,6 +190,18 @@
}
/**
+ * Retrieves the mirror mode of the target intending to use from this configuration.
+ *
+ * @param valueIfMissing The value to return if this configuration option has not been set.
+ * @return The stored value or <code>valueIfMissing</code> if the value does not exist in
+ * this configuration.
+ */
+ @MirrorMode.Mirror
+ default int getMirrorMode(int valueIfMissing) {
+ return retrieveOption(ImageOutputConfig.OPTION_MIRROR_MODE, valueIfMissing);
+ }
+
+ /**
* Retrieves the resolution of the target intending to use from this configuration.
*
* @return The stored value, if it exists in this configuration.
@@ -392,6 +411,18 @@
B setTargetRotation(@RotationValue int rotation);
/**
+ * Sets the mirror mode of the intended target for images from this configuration.
+ *
+ * <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
+ * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
+ *
+ * @param mirrorMode The mirror mode of the intended target.
+ * @return The current Builder.
+ */
+ @NonNull
+ B setMirrorMode(@MirrorMode.Mirror int mirrorMode);
+
+ /**
* Sets the resolution of the intended target from this configuration.
*
* <p>It is not allowed to set both target aspect ratio and target resolution on the same
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 789f991..784a21f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -16,14 +16,16 @@
package androidx.camera.core.internal;
-import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.processing.TargetUtils.getNumberOfTargets;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
-import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -241,7 +243,6 @@
* Updates the states based the new app UseCases.
*/
void updateUseCases(@NonNull Collection<UseCase> appUseCases) {
- // TODO(b/265820449): set applyStreamSharing to true if Effects requires it..
updateUseCases(appUseCases, /*applyStreamSharing*/false);
}
@@ -260,8 +261,8 @@
// Calculate camera UseCases and keep the result in local variables in case they don't
// meet the stream combination rules.
UseCase placeholderForExtensions = calculatePlaceholderForExtensions(appUseCases);
- StreamSharing streamSharing = applyStreamSharing
- ? createOrReuseStreamSharing(appUseCases) : null;
+ StreamSharing streamSharing = createOrReuseStreamSharing(appUseCases,
+ applyStreamSharing);
Collection<UseCase> cameraUseCases =
calculateCameraUseCases(appUseCases, placeholderForExtensions, streamSharing);
@@ -289,6 +290,10 @@
// resolution. Throw exception here if (applyStreamSharing == false), both video
// and preview are used and preview resolution is lower than user configuration.
} catch (IllegalArgumentException exception) {
+ // TODO(b/270187871): instead of catch and retry, we can check UseCase
+ // combination directly with #isUseCasesCombinationSupported(). However
+ // calculateSuggestedStreamSpecs() is currently slow. We will do it after it's
+ // optimized
// Only allow StreamSharing for non-concurrent mode.
if (!applyStreamSharing && hasNoExtension()
&& mCameraCoordinator.getCameraOperatingMode()
@@ -304,7 +309,7 @@
// Update properties.
updateViewPort(suggestedStreamSpecMap, cameraUseCases);
- updateEffects(mEffects, appUseCases);
+ updateEffects(mEffects, cameraUseCases, appUseCases);
// Detach unused UseCases.
for (UseCase useCase : cameraUseCasesToDetach) {
@@ -349,16 +354,38 @@
* Returns {@link UseCase}s qualified for {@link StreamSharing}.
*/
@NonNull
- private Set<UseCase> getStreamSharingChildren(@NonNull Collection<UseCase> appUseCases) {
- Set<UseCase> useCases = new HashSet<>();
+ private Set<UseCase> getStreamSharingChildren(@NonNull Collection<UseCase> appUseCases,
+ boolean forceSharingToPreviewAndVideo) {
+ Set<UseCase> children = new HashSet<>();
+ int sharingTargets = getSharingTargets(forceSharingToPreviewAndVideo);
for (UseCase useCase : appUseCases) {
checkArgument(!isStreamSharing(useCase), "Only support one level of sharing for now.");
- if (isPrivateInputFormat(useCase)) {
- // Add UseCase if the input format is PRIVATE(Preview and VideoCapture).
- useCases.add(useCase);
+ if (useCase.isEffectTargetsSupported(sharingTargets)) {
+ children.add(useCase);
}
}
- return useCases;
+ return children;
+ }
+
+ @CameraEffect.Targets
+ private int getSharingTargets(boolean forceSharingToPreviewAndVideo) {
+ synchronized (mLock) {
+ // Find the only effect that has more than one targets.
+ CameraEffect sharingEffect = null;
+ for (CameraEffect effect : mEffects) {
+ if (getNumberOfTargets(effect.getTargets()) > 1) {
+ checkState(sharingEffect == null, "Can only have one sharing effect.");
+ sharingEffect = effect;
+ }
+ }
+ int sharingTargets = sharingEffect == null ? 0 : sharingEffect.getTargets();
+
+ // Share stream to preview and video capture if the device requires it.
+ if (forceSharingToPreviewAndVideo) {
+ sharingTargets |= PREVIEW | VIDEO_CAPTURE;
+ }
+ return sharingTargets;
+ }
}
/**
@@ -366,14 +393,13 @@
*
* <p> Returns the existing {@link StreamSharing} if the children have not changed.
* Otherwise, create a new {@link StreamSharing} and return.
- *
- * <p> Currently, only {@link UseCase} with {@link ImageFormat#PRIVATE} can be
- * {@link StreamSharing} children({@link Preview} and VideoCapture).
*/
@Nullable
- private StreamSharing createOrReuseStreamSharing(@NonNull Collection<UseCase> appUseCases) {
+ private StreamSharing createOrReuseStreamSharing(@NonNull Collection<UseCase> appUseCases,
+ boolean forceSharingToPreviewAndVideo) {
synchronized (mLock) {
- Set<UseCase> newChildren = getStreamSharingChildren(appUseCases);
+ Set<UseCase> newChildren = getStreamSharingChildren(appUseCases,
+ forceSharingToPreviewAndVideo);
if (newChildren.size() < 2) {
// No need to share the stream for 1 or less children.
return null;
@@ -525,9 +551,14 @@
suggestedStreamSpecs.put(useCase, useCase.getAttachedStreamSpec());
}
+ Rect sensorRect = ((CameraControlInternal) getCameraControl()).getSensorRect();
+ SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
+ (CameraInfoInternal) getCameraInfo(), rectToSize(sensorRect));
+
// Calculate resolution for new use cases.
if (!newUseCases.isEmpty()) {
Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
+ Map<UseCaseConfig<?>, List<Size>> configToSupportedSizesMap = new HashMap<>();
for (UseCase useCase : newUseCases) {
ConfigPair configPair = configPairMap.get(useCase);
// Combine with default configuration.
@@ -535,6 +566,9 @@
useCase.mergeConfigs(cameraInfoInternal, configPair.mExtendedConfig,
configPair.mCameraConfig);
configToUseCaseMap.put(combinedUseCaseConfig, useCase);
+ configToSupportedSizesMap.put(combinedUseCaseConfig,
+ supportedOutputSizesSorter.getSortedSupportedOutputSizes(
+ combinedUseCaseConfig));
}
// Get suggested stream specifications and update the use case session configuration
@@ -542,7 +576,7 @@
mCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
isConcurrentCameraModeOn,
cameraId, existingSurfaces,
- new ArrayList<>(configToUseCaseMap.keySet()));
+ configToSupportedSizesMap);
for (Map.Entry<UseCaseConfig<?>, UseCase> entry : configToUseCaseMap.entrySet()) {
suggestedStreamSpecs.put(entry.getValue(),
@@ -554,17 +588,40 @@
@VisibleForTesting
static void updateEffects(@NonNull List<CameraEffect> effects,
+ @NonNull Collection<UseCase> cameraUseCases,
+ @NonNull Collection<UseCase> appUseCases) {
+ // Match camera UseCases first. Apply the effect early in the pipeline if possible.
+ List<CameraEffect> unusedEffects = setEffectsOnUseCases(effects, cameraUseCases);
+
+ // Match unused effects with app only UseCases.
+ List<UseCase> appOnlyUseCases = new ArrayList<>(appUseCases);
+ appOnlyUseCases.removeAll(cameraUseCases);
+ unusedEffects = setEffectsOnUseCases(unusedEffects, appOnlyUseCases);
+
+ if (unusedEffects.size() > 0) {
+ Logger.w(TAG, "Unused effects: " + unusedEffects);
+ }
+ }
+
+ /**
+ * Sets effects on the given {@link UseCase} list and returns unused effects.
+ */
+ @NonNull
+ private static List<CameraEffect> setEffectsOnUseCases(@NonNull List<CameraEffect> effects,
@NonNull Collection<UseCase> useCases) {
+ List<CameraEffect> unusedEffects = new ArrayList<>(effects);
for (UseCase useCase : useCases) {
useCase.setEffect(null);
for (CameraEffect effect : effects) {
if (useCase.isEffectTargetsSupported(effect.getTargets())) {
checkState(useCase.getEffect() == null,
- useCase + " already has effect " + effect);
+ useCase + " already has effect" + useCase.getEffect());
useCase.setEffect(effect);
+ unusedEffects.remove(effect);
}
}
}
+ return unusedEffects;
}
private void updateViewPort(@NonNull Map<UseCase, StreamSpec> suggestedStreamSpecMap,
@@ -847,13 +904,6 @@
return useCase instanceof ImageCapture;
}
- private boolean isPrivateInputFormat(@NonNull UseCase useCase) {
- UseCaseConfig<?> mergedConfig = useCase.mergeConfigs(
- mCameraInternal.getCameraInfoInternal(), null,
- useCase.getDefaultConfig(true, mUseCaseConfigFactory));
- return mergedConfig.getInputFormat() == INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
- }
-
private Preview createExtraPreview() {
Preview preview = new Preview.Builder().setTargetName("Preview-Extra").build();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
new file mode 100644
index 0000000..b8e80ef
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal;
+
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+
+import android.util.Pair;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class used to sort the supported output sizes according to the use case configs
+ */
+@RequiresApi(21)
+class SupportedOutputSizesSorter {
+ private static final String TAG = "SupportedOutputSizesCollector";
+ private final CameraInfoInternal mCameraInfoInternal;
+ private final Size mActiveArraySize;
+ private final boolean mIsSensorLandscapeResolution;
+ private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
+
+ SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
+ @NonNull Size activeArraySize) {
+ mCameraInfoInternal = cameraInfoInternal;
+ mActiveArraySize = activeArraySize;
+ mIsSensorLandscapeResolution = mActiveArraySize.getWidth() >= mActiveArraySize.getHeight();
+ mSupportedOutputSizesSorterLegacy =
+ new SupportedOutputSizesSorterLegacy(cameraInfoInternal, activeArraySize);
+ }
+
+ @NonNull
+ List<Size> getSortedSupportedOutputSizes(@NonNull UseCaseConfig<?> useCaseConfig) {
+ ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
+ List<Size> customOrderedResolutions = imageOutputConfig.getCustomOrderedResolutions(null);
+
+ // Directly returns the custom ordered resolutions list if it is set.
+ if (customOrderedResolutions != null) {
+ return customOrderedResolutions;
+ }
+
+ // Retrieves the resolution candidate list according to the use case config if
+ List<Size> resolutionCandidateList = getResolutionCandidateList(useCaseConfig);
+
+ ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
+
+ if (resolutionSelector == null) {
+ return mSupportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ resolutionCandidateList, useCaseConfig);
+ } else {
+ Size miniBoundingSize = resolutionSelector.getPreferredResolution();
+ if (miniBoundingSize == null) {
+ miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
+ }
+ return sortSupportedOutputSizesByResolutionSelector(resolutionCandidateList,
+ resolutionSelector, miniBoundingSize);
+ }
+ }
+
+ @NonNull
+ private List<Size> getResolutionCandidateList(@NonNull UseCaseConfig<?> useCaseConfig) {
+ int imageFormat = useCaseConfig.getInputFormat();
+ // Tries to get the custom supported resolutions list if it is set
+ List<Size> resolutionCandidateList = getCustomizedSupportedResolutionsFromConfig(
+ imageFormat, (ImageOutputConfig) useCaseConfig);
+
+ // Tries to get the supported output sizes from the CameraInfoInternal if both custom
+ // ordered and supported resolutions lists are not set.
+ if (resolutionCandidateList == null) {
+ resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
+ }
+
+ return resolutionCandidateList;
+ }
+
+ /**
+ * Retrieves the customized supported resolutions from the use case config.
+ *
+ * <p>In some cases, the use case might not be able to use all the supported output sizes
+ * retrieved from the stream configuration map. For example, extensions is enabled. These
+ * sizes can be set in the use case config by
+ * {@link ImageOutputConfig.Builder#setSupportedResolutions(List)}. SupportedOutputSizesSorter
+ * should use the customized supported resolutions to run the sort/filter logic if it is set.
+ */
+ @Nullable
+ private List<Size> getCustomizedSupportedResolutionsFromConfig(int imageFormat,
+ @NonNull ImageOutputConfig config) {
+ Size[] outputSizes = null;
+
+ // Try to retrieve customized supported resolutions from config.
+ List<Pair<Integer, Size[]>> formatResolutionsPairList =
+ config.getSupportedResolutions(null);
+
+ if (formatResolutionsPairList != null) {
+ for (Pair<Integer, Size[]> formatResolutionPair : formatResolutionsPairList) {
+ if (formatResolutionPair.first == imageFormat) {
+ outputSizes = formatResolutionPair.second;
+ break;
+ }
+ }
+ }
+
+ return outputSizes == null ? null : Arrays.asList(outputSizes);
+ }
+
+ /**
+ * Sorts the resolution candidate list by the following steps:
+ *
+ * 1. Filters out the candidate list according to the max resolution.
+ * 2. Sorts the candidate list according to ResolutionSelector strategies.
+ */
+ @NonNull
+ private List<Size> sortSupportedOutputSizesByResolutionSelector(
+ @NonNull List<Size> resolutionCandidateList,
+ @NonNull ResolutionSelector resolutionSelector,
+ @Nullable Size miniBoundingSize) {
+ if (resolutionCandidateList.isEmpty()) {
+ return resolutionCandidateList;
+ }
+
+ List<Size> descendingSizeList = new ArrayList<>(resolutionCandidateList);
+
+ // Sort the result sizes. The Comparator result must be reversed to have a descending
+ // order result.
+ Collections.sort(descendingSizeList, new CompareSizesByArea(true));
+
+ // 1. Filters out the candidate list according to the min size bound and max resolution.
+ List<Size> filteredSizeList = filterOutResolutionCandidateListByMaxResolutionSetting(
+ descendingSizeList, resolutionSelector);
+
+ // 2. Sorts the candidate list according to the rules of new Resolution API.
+ return sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
+ filteredSizeList, resolutionSelector, miniBoundingSize);
+
+ }
+
+ /**
+ * Filters out the resolution candidate list by the max resolution setting.
+ *
+ * The input size list should have been sorted in descending order.
+ */
+ private static List<Size> filterOutResolutionCandidateListByMaxResolutionSetting(
+ @NonNull List<Size> resolutionCandidateList,
+ @NonNull ResolutionSelector resolutionSelector) {
+ // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
+ // selection logic should depend on ResolutionSelector's settings.
+ Size maxResolution = resolutionSelector.getMaxResolution();
+
+ if (maxResolution == null) {
+ return resolutionCandidateList;
+ }
+
+ // Filter out the resolution candidate list by the max resolution. Sizes that any edge
+ // exceeds the max resolution will be filtered out.
+ List<Size> resultList = new ArrayList<>();
+ for (Size outputSize : resolutionCandidateList) {
+ if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
+ resultList.add(outputSize);
+ }
+ }
+
+ if (resultList.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Resolution candidate list is empty after filtering out by the settings!");
+ }
+
+ return resultList;
+ }
+
+ /**
+ * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
+ *
+ * The list will be sorted by the following order:
+ * 1. size of preferred resolution
+ * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
+ * preferred resolution.
+ * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
+ * size in descending order of resolution area size.
+ * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
+ * area size.
+ */
+ @NonNull
+ private List<Size> sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
+ @NonNull List<Size> resolutionCandidateList,
+ @NonNull ResolutionSelector resolutionSelector,
+ @Nullable Size miniBoundingSize) {
+ Rational aspectRatio = getTargetAspectRatioRationalValue(
+ resolutionSelector.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
+ Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
+ + " value.");
+
+ Size targetSize = resolutionSelector.getPreferredResolution();
+ List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
+ resolutionCandidateList, aspectRatio, miniBoundingSize);
+
+ // Moves the target size to the first position if it exists in the resolution candidate
+ // list.
+ if (resultList.contains(targetSize)) {
+ resultList.remove(targetSize);
+ resultList.add(0, targetSize);
+ }
+
+ return resultList;
+ }
+
+ /**
+ * Sorts the resolution candidate list according to the target aspect ratio and size settings.
+ *
+ * 1. The resolution candidate list will be grouped by aspect ratio.
+ * 2. Moves the smallest size larger than the mini bounding size to the first position for each
+ * aspect ratio sizes group.
+ * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
+ * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
+ * 4. Concatenate all sizes as the result list
+ */
+ @NonNull
+ private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
+ @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
+ @Nullable Size miniBoundingSize) {
+ // Rearrange the supported size to put the ones with the same aspect ratio in the front
+ // of the list and put others in the end from large to small. Some low end devices may
+ // not able to get an supported resolution that match the preferred aspect ratio.
+
+ // Group output sizes by aspect ratio.
+ Map<Rational, List<Size>> aspectRatioSizeListMap =
+ groupSizesByAspectRatio(resolutionCandidateList);
+
+ // If the target resolution is set, use it to remove unnecessary larger sizes.
+ if (miniBoundingSize != null) {
+ // Sorts sizes from each aspect ratio size list
+ for (Rational key : aspectRatioSizeListMap.keySet()) {
+ List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
+ aspectRatioSizeListMap.get(key), miniBoundingSize);
+ aspectRatioSizeListMap.put(key, sortedResult);
+ }
+ }
+
+ // Sort the aspect ratio key set by the target aspect ratio.
+ List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+ Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+ mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
+ Collections.sort(aspectRatios,
+ new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatio, fullFovRatio));
+
+ List<Size> resultList = new ArrayList<>();
+
+ // Put available sizes into final result list by aspect ratio distance to target ratio.
+ for (Rational rational : aspectRatios) {
+ for (Size size : aspectRatioSizeListMap.get(rational)) {
+ // A size may exist in multiple groups in mod16 condition. Keep only one in
+ // the final list.
+ if (!resultList.contains(size)) {
+ resultList.add(size);
+ }
+ }
+ }
+
+ return resultList;
+ }
+
+ /**
+ * Returns the target aspect ratio rational value according to the ResolutionSelector settings.
+ */
+ @Nullable
+ static Rational getTargetAspectRatioRationalValue(@AspectRatio.Ratio int aspectRatio,
+ boolean isSensorLandscapeResolution) {
+ Rational outputRatio = null;
+
+ switch (aspectRatio) {
+ case AspectRatio.RATIO_4_3:
+ outputRatio = isSensorLandscapeResolution ? ASPECT_RATIO_4_3
+ : ASPECT_RATIO_3_4;
+ break;
+ case AspectRatio.RATIO_16_9:
+ outputRatio = isSensorLandscapeResolution ? ASPECT_RATIO_16_9
+ : ASPECT_RATIO_9_16;
+ break;
+ case AspectRatio.RATIO_DEFAULT:
+ break;
+ default:
+ Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
+ }
+
+ return outputRatio;
+ }
+
+ /**
+ * Returns the grouping aspect ratio keys of the input resolution list.
+ *
+ * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+ * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+ */
+ @NonNull
+ static List<Rational> getResolutionListGroupingAspectRatioKeys(
+ @NonNull List<Size> resolutionCandidateList) {
+ List<Rational> aspectRatios = new ArrayList<>();
+
+ // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+ // additional items.
+ aspectRatios.add(ASPECT_RATIO_4_3);
+ aspectRatios.add(ASPECT_RATIO_16_9);
+
+ // Tries to find the aspect ratio which the target size belongs to.
+ for (Size size : resolutionCandidateList) {
+ Rational newRatio = new Rational(size.getWidth(), size.getHeight());
+ boolean aspectRatioFound = aspectRatios.contains(newRatio);
+
+ // The checking size might be a mod16 size which can be mapped to an existing aspect
+ // ratio group.
+ if (!aspectRatioFound) {
+ boolean hasMatchingAspectRatio = false;
+ for (Rational aspectRatio : aspectRatios) {
+ if (hasMatchingAspectRatio(size, aspectRatio)) {
+ hasMatchingAspectRatio = true;
+ break;
+ }
+ }
+ if (!hasMatchingAspectRatio) {
+ aspectRatios.add(newRatio);
+ }
+ }
+ }
+
+ return aspectRatios;
+ }
+
+ /**
+ * Removes unnecessary sizes by target size.
+ *
+ * <p>If the target resolution is set, a size that is equal to or closest to the target
+ * resolution will be selected. If the list includes more than one size equal to or larger
+ * than the target resolution, only one closest size needs to be kept. The other larger sizes
+ * can be removed so that they won't be selected to use.
+ *
+ * @param supportedSizesList The list should have been sorted in descending order.
+ * @param miniBoundingSize The target size used to remove unnecessary sizes.
+ */
+ static List<Size> sortSupportedSizesByMiniBoundingSize(@NonNull List<Size> supportedSizesList,
+ @NonNull Size miniBoundingSize) {
+ if (supportedSizesList.isEmpty()) {
+ return supportedSizesList;
+ }
+
+ List<Size> resultList = new ArrayList<>();
+
+ // Get the index of the item that is equal to or closest to the target size.
+ for (int i = 0; i < supportedSizesList.size(); i++) {
+ Size outputSize = supportedSizesList.get(i);
+ if (outputSize.getWidth() >= miniBoundingSize.getWidth()
+ && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
+ // The supportedSizesList is in descending order. Checking and put the
+ // mini-bounding-above size at position 0 so that the smallest larger resolution
+ // will be put in the first position finally.
+ resultList.add(0, outputSize);
+ } else {
+ // Appends the remaining smaller sizes in descending order.
+ resultList.add(outputSize);
+ }
+ }
+
+ return resultList;
+ }
+
+ static Map<Rational, List<Size>> groupSizesByAspectRatio(@NonNull List<Size> sizes) {
+ Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
+
+ List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
+
+ for (Rational aspectRatio : aspectRatioKeys) {
+ aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
+ }
+
+ for (Size outputSize : sizes) {
+ for (Rational key : aspectRatioSizeListMap.keySet()) {
+ // Put the size into all groups that is matched in mod16 condition since a size
+ // may match multiple aspect ratio in mod16 algorithm.
+ if (hasMatchingAspectRatio(outputSize, key)) {
+ aspectRatioSizeListMap.get(key).add(outputSize);
+ }
+ }
+ }
+
+ return aspectRatioSizeListMap;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
new file mode 100644
index 0000000..674a6a4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal;
+
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+import static androidx.camera.core.internal.SupportedOutputSizesSorter.getResolutionListGroupingAspectRatioKeys;
+import static androidx.camera.core.internal.SupportedOutputSizesSorter.groupSizesByAspectRatio;
+import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByMiniBoundingSize;
+import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
+import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_ZERO;
+import static androidx.camera.core.internal.utils.SizeUtil.getArea;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CameraOrientationUtil;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class used to sort the supported output sizes according to the legacy use case configs
+ */
+@RequiresApi(21)
+class SupportedOutputSizesSorterLegacy {
+ private static final String TAG = "SupportedOutputSizesCollector";
+ private final int mSensorOrientation;
+ private final int mLensFacing;
+ private final Size mActiveArraySize;
+ private final boolean mIsSensorLandscapeResolution;
+
+ SupportedOutputSizesSorterLegacy(@NonNull CameraInfoInternal cameraInfoInternal,
+ @NonNull Size activeArraySize) {
+ mSensorOrientation = cameraInfoInternal.getSensorRotationDegrees();
+ mLensFacing = cameraInfoInternal.getLensFacing();
+ mActiveArraySize = activeArraySize;
+ mIsSensorLandscapeResolution = mActiveArraySize.getWidth() >= mActiveArraySize.getHeight();
+ }
+
+ /**
+ * Sorts the resolution candidate list by the following steps:
+ *
+ * 1. Filters out the candidate list according to the mini and max resolution.
+ * 2. Sorts the candidate list according to legacy target aspect ratio or resolution settings.
+ */
+ @NonNull
+ List<Size> sortSupportedOutputSizes(
+ @NonNull List<Size> resolutionCandidateList,
+ @NonNull UseCaseConfig<?> useCaseConfig) {
+ if (resolutionCandidateList.isEmpty()) {
+ return resolutionCandidateList;
+ }
+
+ List<Size> descendingSizeList = new ArrayList<>(resolutionCandidateList);
+
+ // Sort the result sizes. The Comparator result must be reversed to have a descending
+ // order result.
+ Collections.sort(descendingSizeList, new CompareSizesByArea(true));
+
+ List<Size> filteredSizeList = new ArrayList<>();
+ ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
+ Size maxSize = imageOutputConfig.getMaxResolution(null);
+ Size maxSupportedOutputSize = descendingSizeList.get(0);
+
+ // Set maxSize as the max resolution setting or the max supported output size for the
+ // image format, whichever is smaller.
+ if (maxSize == null || getArea(maxSupportedOutputSize) < getArea(maxSize)) {
+ maxSize = maxSupportedOutputSize;
+ }
+
+ Size targetSize = getTargetSize(imageOutputConfig);
+ Size minSize = RESOLUTION_VGA;
+ int defaultSizeArea = getArea(RESOLUTION_VGA);
+ int maxSizeArea = getArea(maxSize);
+ // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
+ // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
+ // TARGET_RESOLUTION).
+ if (maxSizeArea < defaultSizeArea) {
+ minSize = RESOLUTION_ZERO;
+ } else if (targetSize != null && getArea(targetSize) < defaultSizeArea) {
+ minSize = targetSize;
+ }
+
+ // Filter out the ones that exceed the maximum size and the minimum size. The output
+ // sizes candidates list won't have duplicated items.
+ for (Size outputSize : descendingSizeList) {
+ if (getArea(outputSize) <= getArea(maxSize) && getArea(outputSize) >= getArea(minSize)
+ && !filteredSizeList.contains(outputSize)) {
+ filteredSizeList.add(outputSize);
+ }
+ }
+
+ if (filteredSizeList.isEmpty()) {
+ throw new IllegalArgumentException(
+ "All supported output sizes are filtered out according to current resolution "
+ + "selection settings.");
+ }
+
+ Rational aspectRatio = getTargetAspectRatioByLegacyApi(imageOutputConfig, filteredSizeList);
+
+ // Check the default resolution if the target resolution is not set
+ targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;
+
+ List<Size> resultSizeList = new ArrayList<>();
+ Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
+
+ if (aspectRatio == null) {
+ // If no target aspect ratio is set, all sizes can be added to the result list
+ // directly. No need to sort again since the source list has been sorted previously.
+ resultSizeList.addAll(filteredSizeList);
+
+ // If the target resolution is set, use it to sort the sizes list.
+ if (targetSize != null) {
+ resultSizeList = sortSupportedSizesByMiniBoundingSize(resultSizeList, targetSize);
+ }
+ } else {
+ // Rearrange the supported size to put the ones with the same aspect ratio in the front
+ // of the list and put others in the end from large to small. Some low end devices may
+ // not able to get an supported resolution that match the preferred aspect ratio.
+
+ // Group output sizes by aspect ratio.
+ aspectRatioSizeListMap = groupSizesByAspectRatio(filteredSizeList);
+
+ // If the target resolution is set, use it to remove unnecessary larger sizes.
+ if (targetSize != null) {
+ // Remove unnecessary larger sizes from each aspect ratio size list
+ for (Rational key : aspectRatioSizeListMap.keySet()) {
+ List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
+ aspectRatioSizeListMap.get(key), targetSize);
+ aspectRatioSizeListMap.put(key, sortedResult);
+ }
+ }
+
+ // Sort the aspect ratio key set by the target aspect ratio.
+ List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+ Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+ mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
+ Collections.sort(aspectRatios,
+ new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatio, fullFovRatio));
+
+ // Put available sizes into final result list by aspect ratio distance to target ratio.
+ for (Rational rational : aspectRatios) {
+ for (Size size : aspectRatioSizeListMap.get(rational)) {
+ // A size may exist in multiple groups in mod16 condition. Keep only one in
+ // the final list.
+ if (!resultSizeList.contains(size)) {
+ resultSizeList.add(size);
+ }
+ }
+ }
+ }
+
+ return resultSizeList;
+ }
+
+ /**
+ * Returns the target aspect ratio rational value according to the legacy API settings.
+ */
+ private Rational getTargetAspectRatioByLegacyApi(@NonNull ImageOutputConfig imageOutputConfig,
+ @NonNull List<Size> resolutionCandidateList) {
+ Rational outputRatio = null;
+
+ if (imageOutputConfig.hasTargetAspectRatio()) {
+ @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
+ outputRatio = SupportedOutputSizesSorter.getTargetAspectRatioRationalValue(aspectRatio,
+ mIsSensorLandscapeResolution);
+ } else {
+ // The legacy resolution API will use the aspect ratio of the target size to
+ // be the fallback target aspect ratio value when the use case has no target
+ // aspect ratio setting.
+ Size targetSize = getTargetSize(imageOutputConfig);
+ if (targetSize != null) {
+ outputRatio = getAspectRatioGroupKeyOfTargetSize(targetSize,
+ resolutionCandidateList);
+ }
+ }
+
+ return outputRatio;
+ }
+
+ @Nullable
+ private Size getTargetSize(@NonNull ImageOutputConfig imageOutputConfig) {
+ int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
+ // Calibrate targetSize by the target rotation value.
+ Size targetSize = imageOutputConfig.getTargetResolution(null);
+ targetSize = flipSizeByRotation(targetSize, targetRotation, mLensFacing,
+ mSensorOrientation);
+ return targetSize;
+ }
+
+ /**
+ * Returns the aspect ratio group key of the target size when grouping the input resolution
+ * candidate list.
+ *
+ * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
+ * also need to consider the mod 16 factor to find which aspect ratio of group the target size
+ * might be put in. So that sizes of the group will be selected to use in the highest priority.
+ */
+ @Nullable
+ private static Rational getAspectRatioGroupKeyOfTargetSize(@Nullable Size targetSize,
+ @NonNull List<Size> resolutionCandidateList) {
+ if (targetSize == null) {
+ return null;
+ }
+
+ List<Rational> aspectRatios = getResolutionListGroupingAspectRatioKeys(
+ resolutionCandidateList);
+
+ for (Rational aspectRatio : aspectRatios) {
+ if (hasMatchingAspectRatio(targetSize, aspectRatio)) {
+ return aspectRatio;
+ }
+ }
+
+ return new Rational(targetSize.getWidth(), targetSize.getHeight());
+ }
+
+ // Use target rotation to calibrate the size.
+ @Nullable
+ private static Size flipSizeByRotation(@Nullable Size size, int targetRotation, int lensFacing,
+ int sensorOrientation) {
+ Size outputSize = size;
+ // Calibrates the size with the display and sensor rotation degrees values.
+ if (size != null && isRotationNeeded(targetRotation, lensFacing, sensorOrientation)) {
+ outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
+ }
+ return outputSize;
+ }
+
+ private static boolean isRotationNeeded(int targetRotation, int lensFacing,
+ int sensorOrientation) {
+ int relativeRotationDegrees =
+ CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
+
+ // Currently this assumes that a back-facing camera is always opposite to the screen.
+ // This may not be the case for all devices, so in the future we may need to handle that
+ // scenario.
+ boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
+
+ int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
+ relativeRotationDegrees,
+ sensorOrientation,
+ isOppositeFacingScreen);
+ return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 90a518b..623c019 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -345,19 +345,19 @@
* The target {@link UseCase} of the output stream.
*/
@CameraEffect.Targets
- abstract int getTargets();
+ public abstract int getTargets();
/**
* The format of the output stream.
*/
@CameraEffect.Formats
- abstract int getFormat();
+ public abstract int getFormat();
/**
* How the input should be cropped.
*/
@NonNull
- abstract Rect getCropRect();
+ public abstract Rect getCropRect();
/**
* The stream should scale to this size after cropping and rotating.
@@ -365,12 +365,12 @@
* <p>The input stream should be scaled to match this size after cropping and rotating
*/
@NonNull
- abstract Size getSize();
+ public abstract Size getSize();
/**
* The whether the stream should be mirrored.
*/
- abstract boolean getMirroring();
+ public abstract boolean getMirroring();
/**
* Creates an {@link OutConfig} instance from the input edge.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/TargetUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/TargetUtils.java
index 32b95f9..fde6f4c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/TargetUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/TargetUtils.java
@@ -37,6 +37,18 @@
}
/**
+ * Returns the number of targets in the given target mask by counting the number of 1s.
+ */
+ public static int getNumberOfTargets(int targets) {
+ int count = 0;
+ while (targets != 0) {
+ count += (targets & 1);
+ targets >>= 1;
+ }
+ return count;
+ }
+
+ /**
* Returns true if subset ⊆ superset.
*/
public static boolean isSuperset(int superset, int subset) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index b6cd753..e124da4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -164,7 +164,7 @@
// stream without changing it. Later we will update it to allow
// cropping/down-sampling to better match children UseCase config.
int target = useCase instanceof Preview ? PREVIEW : VIDEO_CAPTURE;
- boolean mirroring = useCase instanceof Preview && isFrontFacing();
+ boolean mirroring = useCase.isMirroringRequired(this);
outConfigs.put(useCase, OutConfig.of(
target,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, // TODO: use JPEG for ImageCapture
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 372e879..60482af 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -16,6 +16,9 @@
package androidx.camera.core;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
@@ -157,6 +160,17 @@
}
@Test
+ public void defaultMirrorModeIsOff() {
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+ assertThat(imageAnalysis.getMirrorModeInternal()).isEqualTo(MIRROR_MODE_OFF);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void setMirrorMode_throwException() {
+ new ImageAnalysis.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON);
+ }
+
+ @Test
public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_legacyApi() {
assertThat(getMergedImageAnalysisConfig(APP_RESOLUTION, ANALYZER_RESOLUTION, -1,
false).getTargetResolution()).isEqualTo(APP_RESOLUTION);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 54b70d0..5df4693 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -32,6 +32,8 @@
import androidx.camera.core.ImageCapture.ImageCaptureRequest
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
+import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CaptureConfig
@@ -162,6 +164,17 @@
}
@Test
+ fun defaultMirrorModeIsOff() {
+ val imageCapture = ImageCapture.Builder().build()
+ assertThat(imageCapture.mirrorModeInternal).isEqualTo(MIRROR_MODE_OFF)
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun setMirrorMode_throwException() {
+ ImageCapture.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+ }
+
+ @Test
fun metadataNotSet_createsNewMetadataInstance() {
val options = ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
options.metadata.isReversedHorizontal = true
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 18f429a..1c0be28 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -30,6 +30,7 @@
import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
+import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
import androidx.camera.core.SurfaceRequest.TransformationInfo
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CameraThreadConfig
@@ -201,6 +202,17 @@
}
@Test
+ fun defaultMirrorModeIsFrontOn() {
+ val preview = Preview.Builder().build()
+ assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_FRONT_ON)
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun setMirrorMode_throwException() {
+ Preview.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+ }
+
+ @Test
fun setTargetRotation_rotationIsChanged() {
// Arrange.
val preview = Preview.Builder().setTargetRotation(Surface.ROTATION_0).build()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraStateRegistryTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraStateRegistryTest.java
index 61009d2..181f5fc 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraStateRegistryTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraStateRegistryTest.java
@@ -21,11 +21,17 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.camera.core.Camera;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.concurrent.CameraCoordinator;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.testing.fakes.FakeCameraCoordinator;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,31 +39,40 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.util.HashMap;
+
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
public final class CameraStateRegistryTest {
- private static final CameraStateRegistry.OnOpenAvailableListener NO_OP_LISTENER = () -> {
- };
+ private static final CameraStateRegistry.OnOpenAvailableListener NO_OP_OPEN_LISTENER =
+ () -> {};
+ private static final CameraStateRegistry.OnConfigureAvailableListener NO_OP_CONFIGURE_LISTENER =
+ () -> {};
+
+ private final FakeCameraCoordinator mCameraCoordinator = new FakeCameraCoordinator();
@Test
public void tryOpenSucceeds_whenNoCamerasOpen() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera = mock(Camera.class);
+ String cameraId = "0";
+ Camera camera = createMockedCamera(cameraId, mCameraCoordinator, null);
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
assertThat(registry.tryOpenCamera(camera)).isTrue();
}
@Test(expected = RuntimeException.class)
public void cameraMustBeRegistered_beforeMarkingState() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera = mock(Camera.class);
+ String cameraId = "0";
+ Camera camera = createMockedCamera(cameraId, mCameraCoordinator, null);
registry.markCameraState(camera, CameraInternal.State.CLOSED);
}
@@ -65,9 +80,10 @@
@Test(expected = RuntimeException.class)
public void cameraMustBeRegistered_beforeTryOpen() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera = mock(Camera.class);
+ String cameraId = "0";
+ Camera camera = createMockedCamera(cameraId, mCameraCoordinator, null);
registry.tryOpenCamera(camera);
}
@@ -75,44 +91,58 @@
@Test(expected = RuntimeException.class)
public void cameraCannotBeRegisteredMultipleTimes() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera = mock(Camera.class);
+ String cameraId = "0";
+ Camera camera = createMockedCamera(cameraId, mCameraCoordinator, null);
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
}
@Test
public void markingReleased_unregistersCamera() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera = mock(Camera.class);
+ String cameraId = "0";
+ Camera camera = createMockedCamera(cameraId, mCameraCoordinator, null);
// Register the camera
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
// Mark the camera as released. This should unregister the camera.
registry.markCameraState(camera, CameraInternal.State.RELEASED);
// Should now be able to register the camera again since it was unregistered
// Should not throw.
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
}
@Test
public void tryOpenFails_whenNoCamerasAvailable() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
- Camera camera3 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ String camera3Id = "3";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+ Camera camera3 = createMockedCamera(camera3Id, mCameraCoordinator, null);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera3, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera3, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
// Take the only available camera.
registry.tryOpenCamera(camera1);
@@ -128,13 +158,19 @@
@Test
public void tryOpenSucceeds_forMultipleCameras() {
// Allow for two cameras to be open simultaneously
- CameraStateRegistry registry = new CameraStateRegistry(2);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 2);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
// Open first camera
boolean tryOpen1 = registry.tryOpenCamera(camera1);
@@ -148,11 +184,12 @@
@Test
public void tryOpenSucceeds_whenNoCamerasAvailable_butCameraIsAlreadyOpen() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
Camera camera = mock(Camera.class);
- registry.registerCamera(camera, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
// Try to open the same camera twice
registry.tryOpenCamera(camera);
@@ -162,13 +199,19 @@
@Test
public void closingCameras_freesUpCameraForOpen() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
// Open the first camera.
registry.tryOpenCamera(camera1);
@@ -187,13 +230,19 @@
@Test
public void pendingOpen_isNotCountedAsOpen() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
registry.markCameraState(camera1, CameraInternal.State.PENDING_OPEN);
@@ -204,15 +253,23 @@
@Test
public void cameraInPendingOpenState_isNotifiedWhenCameraBecomesAvailable() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- CameraStateRegistry.OnOpenAvailableListener mockListener =
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
mock(CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), mockListener);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
// Open first camera
registry.tryOpenCamera(camera1);
@@ -224,25 +281,36 @@
// Close first camera
registry.markCameraState(camera1, CameraInternal.State.CLOSED);
- verify(mockListener).onOpenAvailable();
+ verify(mockOpenListener).onOpenAvailable();
+ verify(mockConfigureListener, never()).onConfigureAvailable();
}
@Test
public void cameraInPendingOpenState_isNotImmediatelyNotifiedWhenCameraBecomesAvailable() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
// Set up first camera
- Camera camera1 = mock(Camera.class);
- CameraStateRegistry.OnOpenAvailableListener mockListener1 = mock(
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener1 = mock(
CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), mockListener1);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener1 = mock(
+ CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ mockConfigureListener1, mockOpenListener1);
// Set up second camera
- Camera camera2 = mock(Camera.class);
- CameraStateRegistry.OnOpenAvailableListener mockListener2 =
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener2 =
mock(CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), mockListener2);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener2 = mock(
+ CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener2, mockOpenListener2);
// Update state of both cameras to PENDING_OPEN, but omit notifying second camera of
// available camera slot
@@ -250,8 +318,8 @@
registry.markCameraState(camera2, CameraInternal.State.PENDING_OPEN, false);
// Verify only first camera is notified of available camera slot
- verify(mockListener1).onOpenAvailable();
- verify(mockListener2, never()).onOpenAvailable();
+ verify(mockOpenListener1).onOpenAvailable();
+ verify(mockOpenListener2, never()).onOpenAvailable();
// Open then close first camera
registry.tryOpenCamera(camera1);
@@ -259,7 +327,8 @@
registry.markCameraState(camera1, CameraInternal.State.CLOSED);
// Verify second camera is notified of available camera slot for opening
- verify(mockListener2).onOpenAvailable();
+ verify(mockOpenListener2).onOpenAvailable();
+ verify(mockConfigureListener2, never()).onConfigureAvailable();
}
// Checks whether a camera in a pending open state is notified when one of 2 slots becomes
@@ -267,17 +336,27 @@
@Test
public void cameraInPendingOpenState_isNotifiedWhenCameraBecomesAvailable_multipleSlots() {
// Allow for two cameras to be open simultaneously
- CameraStateRegistry registry = new CameraStateRegistry(2);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 2);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
- Camera camera3 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ String camera3Id = "3";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+ Camera camera3 = createMockedCamera(camera3Id, mCameraCoordinator, null);
- CameraStateRegistry.OnOpenAvailableListener mockListener =
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
mock(CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera3, CameraXExecutors.directExecutor(), mockListener);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera3, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
// Open first camera
registry.tryOpenCamera(camera1);
@@ -293,21 +372,30 @@
// Close first camera
registry.markCameraState(camera1, CameraInternal.State.CLOSED);
- verify(mockListener).onOpenAvailable();
+ verify(mockOpenListener).onOpenAvailable();
+ verify(mockConfigureListener, never()).onConfigureAvailable();
}
@Test
public void cameraInClosedState_isNotNotifiedWhenCameraBecomesAvailable() {
// Only allow a single open camera at a time
- CameraStateRegistry registry = new CameraStateRegistry(1);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- CameraStateRegistry.OnOpenAvailableListener mockListener =
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
mock(CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), mockListener);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
// Open first camera
registry.tryOpenCamera(camera1);
@@ -319,21 +407,30 @@
// Close first camera
registry.markCameraState(camera1, CameraInternal.State.CLOSED);
- verify(mockListener, never()).onOpenAvailable();
+ verify(mockOpenListener, never()).onOpenAvailable();
+ verify(mockConfigureListener, never()).onConfigureAvailable();
}
@Test
public void cameraInOpenState_isNotNotifiedWhenCameraBecomesAvailable() {
// Allow for two cameras to be open simultaneously
- CameraStateRegistry registry = new CameraStateRegistry(2);
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 2);
- Camera camera1 = mock(Camera.class);
- Camera camera2 = mock(Camera.class);
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
- CameraStateRegistry.OnOpenAvailableListener mockListener =
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
mock(CameraStateRegistry.OnOpenAvailableListener.class);
- registry.registerCamera(camera1, CameraXExecutors.directExecutor(), NO_OP_LISTENER);
- registry.registerCamera(camera2, CameraXExecutors.directExecutor(), mockListener);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
// Open first camera
registry.tryOpenCamera(camera1);
@@ -347,6 +444,180 @@
registry.markCameraState(camera1, CameraInternal.State.CLOSED);
// Second camera is already open, should not be notified.
- verify(mockListener, never()).onOpenAvailable();
+ verify(mockOpenListener, never()).onOpenAvailable();
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+ }
+
+ @Test
+ public void cameraInConfiguredState_pairedCameraWillBeNotifiedToConfigureInConcurrentMode() {
+ // Only allow a single open camera at a time
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
+ mCameraCoordinator.setCameraOperatingMode(
+ CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT);
+
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
+ mock(CameraStateRegistry.OnOpenAvailableListener.class);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
+
+ // Open first camera
+ registry.tryOpenCamera(camera1);
+ registry.markCameraState(camera1, CameraInternal.State.OPEN);
+
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+
+ // Open the second camera
+ registry.tryOpenCamera(camera2);
+ registry.markCameraState(camera2, CameraInternal.State.OPEN);
+
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+
+ // Configure the first camera
+ registry.markCameraState(camera1, CameraInternal.State.CONFIGURED);
+
+ verify(mockConfigureListener).onConfigureAvailable();
+ }
+
+ @Test
+ public void cameraInConfiguredState_pairedCameraWillNotBeNotifiedToConfigureInSingleMode() {
+ // Only allow a single open camera at a time
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
+
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
+ mock(CameraStateRegistry.OnOpenAvailableListener.class);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
+
+ // Open first camera
+ registry.tryOpenCamera(camera1);
+ registry.markCameraState(camera1, CameraInternal.State.OPEN);
+
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+
+ // Open the second camera
+ registry.tryOpenCamera(camera2);
+ registry.markCameraState(camera2, CameraInternal.State.OPEN);
+
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+
+ // Configure the first camera
+ registry.markCameraState(camera1, CameraInternal.State.CONFIGURED);
+
+ verify(mockConfigureListener, never()).onConfigureAvailable();
+ }
+
+ @Test
+ public void tryOpenCaptureSession_returnFalseInConcurrentMode() {
+ // Only allow a single open camera at a time
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
+
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
+ mock(CameraStateRegistry.OnOpenAvailableListener.class);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
+
+ // Open first camera
+ registry.tryOpenCamera(camera1);
+ registry.markCameraState(camera1, CameraInternal.State.OPEN);
+
+ assertThat(registry.tryOpenCaptureSession(camera1Id, pairedCamera1Id)).isTrue();
+
+ // Open the second camera
+ registry.tryOpenCamera(camera2);
+ registry.markCameraState(camera2, CameraInternal.State.OPEN);
+
+ assertThat(registry.tryOpenCaptureSession(camera2Id, pairedCamera2Id)).isTrue();
+ }
+
+ @Test
+ public void tryOpenCaptureSession_returnTrueOnlyIfPairedCameraIsAlsoOpenedInConcurrentMode() {
+ // Only allow a single open camera at a time
+ CameraStateRegistry registry = new CameraStateRegistry(mCameraCoordinator, 1);
+ mCameraCoordinator.setCameraOperatingMode(
+ CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT);
+
+ String camera1Id = "1";
+ String pairedCamera1Id = "2";
+ String camera2Id = "2";
+ String pairedCamera2Id = "1";
+ Camera camera1 = createMockedCamera(camera1Id, mCameraCoordinator, pairedCamera1Id);
+ Camera camera2 = createMockedCamera(camera2Id, mCameraCoordinator, pairedCamera2Id);
+
+ CameraStateRegistry.OnOpenAvailableListener mockOpenListener =
+ mock(CameraStateRegistry.OnOpenAvailableListener.class);
+ CameraStateRegistry.OnConfigureAvailableListener mockConfigureListener =
+ mock(CameraStateRegistry.OnConfigureAvailableListener.class);
+ registry.registerCamera(camera1, CameraXExecutors.directExecutor(),
+ NO_OP_CONFIGURE_LISTENER, NO_OP_OPEN_LISTENER);
+ registry.registerCamera(camera2, CameraXExecutors.directExecutor(),
+ mockConfigureListener, mockOpenListener);
+
+ // Open first camera
+ registry.tryOpenCamera(camera1);
+ registry.markCameraState(camera1, CameraInternal.State.OPEN);
+
+ assertThat(registry.tryOpenCaptureSession(camera1Id, pairedCamera1Id)).isFalse();
+ assertThat(registry.tryOpenCaptureSession(camera2Id, pairedCamera2Id)).isFalse();
+
+ // Open the second camera
+ registry.tryOpenCamera(camera2);
+ registry.markCameraState(camera2, CameraInternal.State.OPEN);
+
+ assertThat(registry.tryOpenCaptureSession(camera1Id, pairedCamera1Id)).isTrue();
+ assertThat(registry.tryOpenCaptureSession(camera2Id, pairedCamera2Id)).isTrue();
+ }
+
+ @NonNull
+ private static Camera createMockedCamera(
+ @NonNull String cameraId,
+ @NonNull FakeCameraCoordinator cameraCoordinator,
+ @Nullable String pairedCameraId) {
+
+ Camera camera = mock(Camera.class);
+ CameraInfoInternal cameraInfoInternal = mock(CameraInfoInternal.class);
+ when(camera.getCameraInfo()).thenReturn(cameraInfoInternal);
+ when(cameraInfoInternal.getCameraId()).thenReturn(cameraId);
+
+ if (pairedCameraId != null) {
+ cameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(
+ new HashMap<String, CameraSelector>() {{
+ put(cameraId, CameraSelector.DEFAULT_BACK_CAMERA);
+ put(pairedCameraId, CameraSelector.DEFAULT_FRONT_CAMERA);
+ }});
+ }
+ return camera;
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 27eb18e..6bc9f63 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -90,7 +90,9 @@
private lateinit var useCaseConfigFactory: UseCaseConfigFactory
private lateinit var previewEffect: FakeSurfaceEffect
private lateinit var videoEffect: FakeSurfaceEffect
+ private lateinit var sharedEffect: FakeSurfaceEffect
private lateinit var cameraCoordinator: CameraCoordinator
+ private lateinit var surfaceProcessorInternal: FakeSurfaceProcessorInternal
private val fakeCameraSet = LinkedHashSet<CameraInternal>()
private val imageEffect = GrayscaleImageEffect()
private val preview = Preview.Builder().build()
@@ -109,13 +111,18 @@
useCaseConfigFactory = FakeUseCaseConfigFactory()
fakeCameraSet.add(fakeCamera)
executor = Executors.newSingleThreadExecutor()
+ surfaceProcessorInternal = FakeSurfaceProcessorInternal(mainThreadExecutor())
previewEffect = FakeSurfaceEffect(
PREVIEW,
- FakeSurfaceProcessorInternal(mainThreadExecutor())
+ surfaceProcessorInternal
)
videoEffect = FakeSurfaceEffect(
VIDEO_CAPTURE,
- FakeSurfaceProcessorInternal(mainThreadExecutor())
+ surfaceProcessorInternal
+ )
+ sharedEffect = FakeSurfaceEffect(
+ PREVIEW or VIDEO_CAPTURE,
+ surfaceProcessorInternal
)
effects = listOf(previewEffect, imageEffect, videoEffect)
adapter = CameraUseCaseAdapter(
@@ -124,11 +131,12 @@
fakeCameraDeviceSurfaceManager,
useCaseConfigFactory
)
- DefaultSurfaceProcessor.Factory.setSupplier { FakeSurfaceProcessorInternal(executor) }
+ DefaultSurfaceProcessor.Factory.setSupplier { surfaceProcessorInternal }
}
@After
fun tearDown() {
+ surfaceProcessorInternal.cleanUp()
executor.shutdown()
}
@@ -136,8 +144,7 @@
fun attachAndDetachUseCases_cameraUseCasesAttachedAndDetached() {
// Arrange: bind UseCases that requires sharing.
adapter.addUseCases(setOf(preview, video, image))
- val streamSharing =
- adapter.cameraUseCases.filterIsInstance(StreamSharing::class.java).single()
+ val streamSharing = adapter.getStreamSharing()
// Act: attach use cases.
adapter.attachUseCases()
// Assert: StreamSharing and image are attached.
@@ -219,8 +226,7 @@
StreamSharing::class.java,
ImageCapture::class.java
)
- val streamSharing =
- adapter.cameraUseCases.filterIsInstance(StreamSharing::class.java).single()
+ val streamSharing = adapter.getStreamSharing()
assertThat(streamSharing.camera).isNotNull()
// Act: remove UseCase so that StreamSharing is no longer needed
adapter.removeUseCases(setOf(video))
@@ -827,7 +833,42 @@
@Test(expected = IllegalStateException::class)
fun updateEffectsWithDuplicateTargets_throwsException() {
- CameraUseCaseAdapter.updateEffects(listOf(previewEffect, previewEffect), listOf(preview))
+ CameraUseCaseAdapter.updateEffects(
+ listOf(previewEffect, previewEffect),
+ listOf(preview),
+ emptyList()
+ )
+ }
+
+ @Test
+ fun hasSharedEffect_enableStreamSharing() {
+ // Arrange: add a shared effect and an image effect
+ adapter.setEffects(listOf(sharedEffect, imageEffect))
+
+ // Act: update use cases.
+ adapter.updateUseCases(listOf(preview, video, image, analysis))
+
+ // Assert: StreamSharing wraps preview and video with the shared effect.
+ val streamSharing = adapter.getStreamSharing()
+ assertThat(streamSharing.children).containsExactly(preview, video)
+ assertThat(streamSharing.effect).isEqualTo(sharedEffect)
+ assertThat(preview.effect).isNull()
+ assertThat(video.effect).isNull()
+ assertThat(analysis.effect).isNull()
+ assertThat(image.effect).isEqualTo(imageEffect)
+ }
+
+ @Test
+ fun hasSharedEffectButOnlyOneChild_theEffectIsEnabledOnTheChild() {
+ // Arrange: add a shared effect.
+ adapter.setEffects(listOf(sharedEffect))
+
+ // Act: update use cases.
+ adapter.updateUseCases(listOf(preview))
+
+ // Assert: no StreamSharing and preview gets the shared effect.
+ assertThat(adapter.cameraUseCases.filterIsInstance(StreamSharing::class.java)).isEmpty()
+ assertThat(preview.effect).isEqualTo(sharedEffect)
}
@Test
@@ -836,14 +877,14 @@
val useCases = listOf(preview, video, image)
// Act: update use cases with effects.
- CameraUseCaseAdapter.updateEffects(effects, useCases)
+ CameraUseCaseAdapter.updateEffects(effects, useCases, emptyList())
// Assert: UseCase have effects
assertThat(preview.effect).isEqualTo(previewEffect)
assertThat(image.effect).isEqualTo(imageEffect)
assertThat(video.effect).isEqualTo(videoEffect)
// Act: update again with no effects.
- CameraUseCaseAdapter.updateEffects(listOf(), useCases)
+ CameraUseCaseAdapter.updateEffects(listOf(), useCases, emptyList())
// Assert: use cases no longer has effects.
assertThat(preview.effect).isNull()
assertThat(image.effect).isNull()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt
new file mode 100644
index 0000000..1304995
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacyTest.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal
+
+import android.graphics.ImageFormat
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import java.util.Arrays
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private const val TARGET_ASPECT_RATIO_NONE = -99
+private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
+private val DEFAULT_SUPPORTED_SIZES = listOf(
+ Size(4032, 3024), // 4:3
+ Size(3840, 2160), // 16:9
+ Size(1920, 1440), // 4:3
+ Size(1920, 1080), // 16:9
+ Size(1280, 960), // 4:3
+ Size(1280, 720), // 16:9
+ Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+ Size(800, 450), // 16:9
+ Size(640, 480), // 4:3
+ Size(320, 240), // 4:3
+ Size(320, 180), // 16:9
+ Size(256, 144) // 16:9
+)
+
+/**
+ * Unit tests for [SupportedOutputSizesSorterLegacy].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
[email protected](minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SupportedOutputSizesSorterLegacyTest {
+ private val cameraInfoInternal = FakeCameraInfoInternal()
+ private val supportedOutputSizesSorterLegacy =
+ SupportedOutputSizesSorterLegacy(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+
+ @Test
+ fun checkFilterOutSmallSizesByDefaultSize640x480() {
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ createUseCaseConfig()
+ )
+ // Sizes smaller than 640x480 are filtered out by default
+ assertThat(resultList).containsNoneIn(
+ arrayOf(
+ Size(320, 240),
+ Size(320, 180),
+ Size(256, 144)
+ )
+ )
+ }
+
+ @Test
+ fun canPrioritize4x3SizesByTargetAspectRatio() {
+ val useCaseConfig = createUseCaseConfig(targetAspectRatio = AspectRatio.RATIO_4_3)
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // 4:3 items are prioritized in the result list
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 4:3 items
+ Size(4032, 3024),
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ // 16:9 items
+ Size(3840, 2160),
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canPrioritize16x9SizesByTargetAspectRatio() {
+ val useCaseConfig = createUseCaseConfig(targetAspectRatio = AspectRatio.RATIO_16_9)
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // 16:9 items are prioritized in the result list
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 16:9 items
+ Size(3840, 2160),
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ // 4:3 items
+ Size(4032, 3024),
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canPrioritize4x3SizesByTargetResolution() {
+ val useCaseConfig = createUseCaseConfig(targetResolution = Size(1280, 960))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // 4:3 items are prioritized in the result list
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 4:3 items
+ Size(1280, 960),
+ Size(1920, 1440),
+ Size(4032, 3024),
+ Size(640, 480),
+ // 16:9 items
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canPrioritize16x9SizesByTargetResolution() {
+ val useCaseConfig = createUseCaseConfig(targetResolution = Size(1920, 1080))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // 16:9 items are prioritized in the result list
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 16:9 items
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ // 4:3 items
+ Size(1920, 1440),
+ Size(4032, 3024),
+ Size(1280, 960),
+ Size(640, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canSelectSmallSizesByTargetResolutionSmallerThan640x480() {
+ val useCaseConfig = createUseCaseConfig(targetResolution = Size(320, 240))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // 16:9 items are prioritized in the result list
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 4:3 items
+ Size(320, 240),
+ Size(640, 480),
+ Size(1280, 960),
+ Size(1920, 1440),
+ Size(4032, 3024),
+ // 16:9 items
+ Size(800, 450),
+ Size(960, 544),
+ Size(1280, 720),
+ Size(1920, 1080),
+ Size(3840, 2160),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canPrioritizeSizesByResolutionOfUncommonAspectRatio() {
+ val useCaseConfig = createUseCaseConfig(targetResolution = Size(1280, 640))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
+ // ratio not matched list. Then, 1280x720 will be the nearest matched one.
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 16:9 items
+ Size(1280, 720),
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(960, 544),
+ Size(800, 450),
+ // 4:3 items
+ Size(1280, 960),
+ Size(1920, 1440),
+ Size(4032, 3024),
+ Size(640, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun checkFilterOutLargeSizesByMaxResolution() {
+ val useCaseConfig = createUseCaseConfig(maxResolution = Size(1920, 1080))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ Size(1920, 1080),
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(640, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canKeepSmallSizesBySmallMaxResolution() {
+ val useCaseConfig = createUseCaseConfig(maxResolution = Size(320, 240))
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ Size(320, 240),
+ Size(320, 180),
+ Size(256, 144),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canSelectCorrect4x3SizeByDefaultResolution() {
+ val useCaseConfig = createUseCaseConfig(
+ targetAspectRatio = AspectRatio.RATIO_4_3,
+ defaultResolution = Size(640, 480)
+ )
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // The default resolution 640x480 will be used as target resolution to filter out too-large
+ // items in each aspect ratio group
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 4:3 items
+ Size(640, 480),
+ Size(1280, 960),
+ Size(1920, 1440),
+ Size(4032, 3024),
+ // 16:9 items
+ Size(960, 544),
+ Size(1280, 720),
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(800, 450),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canSelectCorrect16x9SizeByDefaultResolution() {
+ val useCaseConfig = createUseCaseConfig(
+ targetAspectRatio = AspectRatio.RATIO_16_9,
+ defaultResolution = Size(640, 480)
+ )
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // The default resolution 640x480 will be used as target resolution to filter out too-large
+ // items in each aspect ratio group
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 16:9 items
+ Size(960, 544),
+ Size(1280, 720),
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(800, 450),
+ // 4:3 items
+ Size(640, 480),
+ Size(1280, 960),
+ Size(1920, 1440),
+ Size(4032, 3024),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun targetResolutionCanOverrideDefaultResolution() {
+ val useCaseConfig = createUseCaseConfig(
+ targetResolution = Size(1920, 1080),
+ defaultResolution = Size(640, 480)
+ )
+ val resultList = supportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
+ DEFAULT_SUPPORTED_SIZES,
+ useCaseConfig
+ )
+ // The default resolution 640x480 will be used as target resolution to filter out too-large
+ // items in each aspect ratio group
+ assertThat(resultList).containsExactlyElementsIn(
+ arrayOf(
+ // 16:9 items
+ Size(1920, 1080),
+ Size(3840, 2160),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ // 4:3 items
+ Size(1920, 1440),
+ Size(4032, 3024),
+ Size(1280, 960),
+ Size(640, 480),
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun sortByCompareSizesByArea_canSortSizesCorrectly() {
+ val sizes = arrayOfNulls<Size>(DEFAULT_SUPPORTED_SIZES.size)
+ // Generates a unsorted array from mSupportedSizes.
+ val centerIndex = DEFAULT_SUPPORTED_SIZES.size / 2
+ // Puts 2nd half sizes in the front
+ for (i in centerIndex until DEFAULT_SUPPORTED_SIZES.size) {
+ sizes[i - centerIndex] = DEFAULT_SUPPORTED_SIZES[i]
+ }
+ // Puts 1st half sizes inversely in the tail
+ for (j in centerIndex - 1 downTo 0) {
+ sizes[DEFAULT_SUPPORTED_SIZES.size - j - 1] = DEFAULT_SUPPORTED_SIZES[j]
+ }
+ // The testing sizes array will be equal to mSupportedSizes after sorting.
+ Arrays.sort(sizes, CompareSizesByArea(true))
+ assertThat(sizes.toList()).containsExactlyElementsIn(DEFAULT_SUPPORTED_SIZES).inOrder()
+ }
+
+ private fun createUseCaseConfig(
+ captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
+ targetRotation: Int = Surface.ROTATION_0,
+ targetAspectRatio: Int = TARGET_ASPECT_RATIO_NONE,
+ targetResolution: Size? = null,
+ maxResolution: Size? = null,
+ defaultResolution: Size? = null,
+ ): UseCaseConfig<*> {
+ val builder = FakeUseCaseConfig.Builder(captureType, ImageFormat.JPEG)
+ builder.setTargetRotation(targetRotation)
+ if (targetAspectRatio != TARGET_ASPECT_RATIO_NONE) {
+ builder.setTargetAspectRatio(targetAspectRatio)
+ }
+ targetResolution?.let { builder.setTargetResolution(it) }
+ maxResolution?.let { builder.setMaxResolution(it) }
+ defaultResolution?.let { builder.setDefaultResolution(it) }
+ return builder.useCaseConfig
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
new file mode 100644
index 0000000..fd9e4ea
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal
+
+import android.graphics.ImageFormat
+import android.os.Build
+import android.util.Pair
+import android.util.Size
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
+private val DEFAULT_SUPPORTED_SIZES = listOf(
+ Size(4032, 3024), // 4:3
+ Size(3840, 2160), // 16:9
+ Size(1920, 1440), // 4:3
+ Size(1920, 1080), // 16:9
+ Size(1280, 960), // 4:3
+ Size(1280, 720), // 16:9
+ Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+ Size(800, 450), // 16:9
+ Size(640, 480), // 4:3
+ Size(320, 240), // 4:3
+ Size(320, 180), // 16:9
+ Size(256, 144) // 16:9
+)
+
+/**
+ * Unit tests for [SupportedOutputSizesSorter].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
[email protected](minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SupportedOutputSizesSorterTest {
+
+ @Test
+ fun canSelectCustomOrderedResolutions() {
+ // Arrange
+ val imageFormat = ImageFormat.JPEG
+ val cameraInfoInternal = FakeCameraInfoInternal().apply {
+ setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
+ }
+ val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+ // Sets up the custom ordered resolutions
+ val useCaseConfig =
+ FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
+ setCustomOrderedResolutions(
+ listOf(
+ Size(1920, 1080),
+ Size(720, 480),
+ Size(640, 480)
+ )
+ )
+ }.useCaseConfig
+
+ // Act
+ val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+
+ // Assert
+ assertThat(sortedResult).containsExactlyElementsIn(
+ listOf(
+ Size(1920, 1080),
+ Size(720, 480),
+ Size(640, 480)
+ )
+ ).inOrder()
+ }
+
+ @Test
+ fun canSelectCustomSupportedResolutions() {
+ // Arrange
+ val imageFormat = ImageFormat.JPEG
+ val cameraInfoInternal = FakeCameraInfoInternal().apply {
+ setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
+ }
+ val supportedOutputSizesSorter =
+ SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
+ // Sets up the custom supported resolutions
+ val useCaseConfig =
+ FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
+ setSupportedResolutions(
+ listOf(
+ Pair.create(
+ imageFormat, arrayOf(
+ Size(1920, 1080),
+ Size(720, 480),
+ Size(640, 480)
+ )
+ )
+ )
+ )
+ }.useCaseConfig
+
+ // Act
+ val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+
+ // Assert
+ assertThat(sortedResult).containsExactlyElementsIn(
+ listOf(
+ Size(1920, 1080),
+ Size(720, 480),
+ Size(640, 480)
+ )
+ ).inOrder()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
index 3a16e56..8f06cf2 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
@@ -22,6 +22,7 @@
import android.os.Build
import android.util.Size
import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
import androidx.camera.core.UseCase
import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
import androidx.camera.core.impl.SessionConfig
@@ -31,8 +32,10 @@
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeDeferrableSurface
import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.testing.fakes.FakeUseCaseConfig
import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,9 +61,12 @@
.addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE)).build()
}
+ private val surfaceEdgesToClose = mutableListOf<SurfaceEdge>()
private val parentCamera = FakeCamera()
private val child1 = FakeUseCase()
- private val child2 = FakeUseCase()
+ private val child2 = FakeUseCaseConfig.Builder()
+ .setMirrorMode(MIRROR_MODE_ON)
+ .build()
private val childrenEdges = mapOf(
Pair(child1 as UseCase, createSurfaceEdge()),
Pair(child2 as UseCase, createSurfaceEdge())
@@ -73,6 +79,13 @@
virtualCamera = VirtualCamera(parentCamera, setOf(child1, child2), useCaseConfigFactory)
}
+ @After
+ fun tearDown() {
+ for (surfaceEdge in surfaceEdgesToClose) {
+ surfaceEdge.close()
+ }
+ }
+
@Test
fun setUseCaseActiveAndInactive_surfaceConnectsAndDisconnects() {
// Arrange.
@@ -142,6 +155,28 @@
}
@Test
+ fun getChildrenOutConfigs() {
+ // Arrange.
+ val cropRect = Rect(10, 10, 410, 310)
+
+ // Act.
+ val outConfigs = virtualCamera.getChildrenOutConfigs(
+ createSurfaceEdge(cropRect = cropRect)
+ )
+
+ // Assert: child1
+ val outConfig1 = outConfigs[child1]!!
+ assertThat(outConfig1.cropRect).isEqualTo(cropRect)
+ assertThat(outConfig1.size).isEqualTo(Size(400, 300))
+ assertThat(outConfig1.mirroring).isFalse()
+ // Assert: child2
+ val outConfig2 = outConfigs[child2]!!
+ assertThat(outConfig2.cropRect).isEqualTo(cropRect)
+ assertThat(outConfig2.size).isEqualTo(Size(400, 300))
+ assertThat(outConfig2.mirroring).isTrue()
+ }
+
+ @Test
fun updateChildrenSpec_updateAndNotifyChildren() {
// Act: update children with the map.
virtualCamera.setChildrenEdges(childrenEdges)
@@ -150,17 +185,26 @@
assertThat(child2.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
}
- private fun createSurfaceEdge(): SurfaceEdge {
+ private fun createSurfaceEdge(
+ target: Int = PREVIEW,
+ format: Int = INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ streamSpec: StreamSpec = StreamSpec.builder(INPUT_SIZE).build(),
+ matrix: Matrix = Matrix(),
+ hasCameraTransform: Boolean = true,
+ cropRect: Rect = Rect(),
+ rotationDegrees: Int = 0,
+ mirroring: Boolean = false
+ ): SurfaceEdge {
return SurfaceEdge(
- PREVIEW,
- INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
- StreamSpec.builder(INPUT_SIZE).build(),
- Matrix(),
- true,
- Rect(),
- 0,
- false
- )
+ target,
+ format,
+ streamSpec,
+ matrix,
+ hasCameraTransform,
+ cropRect,
+ rotationDegrees,
+ mirroring
+ ).also { surfaceEdgesToClose.add(it) }
}
private fun verifyEdge(child: UseCase, isClosed: Boolean, hasProvider: Boolean) {
diff --git a/camera/camera-extensions/proguard-rules.pro b/camera/camera-extensions/proguard-rules.pro
index f468308..898fbf8 100644
--- a/camera/camera-extensions/proguard-rules.pro
+++ b/camera/camera-extensions/proguard-rules.pro
@@ -15,3 +15,4 @@
# Needs to keep the classes implementing the vendor library interfaces in the extensions module.
# Otherwise, it will cause AbstractMethodError if proguard is enabled.
-keep class androidx.camera.extensions.ExtensionsManager$** {*;}
+-keep class androidx.camera.extensions.internal.sessionprocessor.AdvancedSessionProcessor$** {*;}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index 1ff9ad6..5d6163f 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -65,7 +65,10 @@
private Set<UseCase> mAttachedUseCases = new HashSet<>();
private State mState = State.CLOSED;
private int mAvailableCameraCount = 1;
- private List<UseCase> mUseCaseResetHistory = new ArrayList<>();
+ private final List<UseCase> mUseCaseActiveHistory = new ArrayList<>();
+ private final List<UseCase> mUseCaseInactiveHistory = new ArrayList<>();
+ private final List<UseCase> mUseCaseUpdateHistory = new ArrayList<>();
+ private final List<UseCase> mUseCaseResetHistory = new ArrayList<>();
private boolean mHasTransform = true;
@Nullable
@@ -199,7 +202,7 @@
@Override
public void onUseCaseActive(@NonNull UseCase useCase) {
Logger.d(TAG, "Use case " + useCase + " ACTIVE for camera " + mCameraId);
-
+ mUseCaseActiveHistory.add(useCase);
mUseCaseAttachState.setUseCaseActive(useCase.getName() + useCase.hashCode(),
useCase.getSessionConfig(), useCase.getCurrentConfig());
updateCaptureSessionConfig();
@@ -209,7 +212,7 @@
@Override
public void onUseCaseInactive(@NonNull UseCase useCase) {
Logger.d(TAG, "Use case " + useCase + " INACTIVE for camera " + mCameraId);
-
+ mUseCaseInactiveHistory.add(useCase);
mUseCaseAttachState.setUseCaseInactive(useCase.getName() + useCase.hashCode());
updateCaptureSessionConfig();
}
@@ -218,7 +221,7 @@
@Override
public void onUseCaseUpdated(@NonNull UseCase useCase) {
Logger.d(TAG, "Use case " + useCase + " UPDATED for camera " + mCameraId);
-
+ mUseCaseUpdateHistory.add(useCase);
mUseCaseAttachState.updateUseCase(useCase.getName() + useCase.hashCode(),
useCase.getSessionConfig(), useCase.getCurrentConfig());
updateCaptureSessionConfig();
@@ -307,6 +310,21 @@
}
@NonNull
+ public List<UseCase> getUseCaseActiveHistory() {
+ return mUseCaseActiveHistory;
+ }
+
+ @NonNull
+ public List<UseCase> getUseCaseInactiveHistory() {
+ return mUseCaseInactiveHistory;
+ }
+
+ @NonNull
+ public List<UseCase> getUseCaseUpdateHistory() {
+ return mUseCaseUpdateHistory;
+ }
+
+ @NonNull
public List<UseCase> getUseCaseResetHistory() {
return mUseCaseResetHistory;
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
index b4ce627..128c998 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
@@ -107,7 +107,9 @@
public void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
if (cameraOperatingMode != mCameraOperatingMode) {
for (ConcurrentCameraModeListener listener : mConcurrentCameraModeListeners) {
- listener.notifyConcurrentCameraModeUpdated(cameraOperatingMode);
+ listener.onCameraOperatingModeUpdated(
+ mCameraOperatingMode,
+ cameraOperatingMode);
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index ef49742..cd5d637 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -89,13 +89,14 @@
SurfaceConfig.ConfigSize.PREVIEW);
}
- @Override
@NonNull
+ @Override
public Map<UseCaseConfig<?>, StreamSpec> getSuggestedStreamSpecs(
- boolean isConcurrentCameraModeOn,
- @NonNull String cameraId,
+ boolean isConcurrentCameraModeOn, @NonNull String cameraId,
@NonNull List<AttachedSurfaceInfo> existingSurfaces,
- @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
+ @NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap) {
+ List<UseCaseConfig<?>> newUseCaseConfigs =
+ new ArrayList<>(newUseCaseConfigsSupportedSizeMap.keySet());
checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecs = new HashMap<>();
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
index 1ce5970..2fa0582 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
@@ -134,5 +134,7 @@
for (SurfaceOutput surfaceOutput : mSurfaceOutputs.values()) {
surfaceOutput.close();
}
+ mSurfaceTexture.release();
+ mInputSurface.release();
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 931d5f1..2267928 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -16,17 +16,19 @@
package androidx.camera.testing.fakes;
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
import android.util.Pair;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.MirrorMode;
import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
-import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
@@ -57,7 +59,7 @@
@Override
public int getInputFormat() {
return retrieveOption(OPTION_INPUT_FORMAT,
- ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
}
/** Builder for an empty Config */
@@ -81,6 +83,11 @@
this(MutableOptionsBundle.create(), captureType);
}
+ public Builder(@NonNull CaptureType captureType, int inputFormat) {
+ this(MutableOptionsBundle.create(), captureType);
+ mOptionsBundle.insertOption(OPTION_INPUT_FORMAT, inputFormat);
+ }
+
public Builder(@NonNull Config config, @NonNull CaptureType captureType) {
mOptionsBundle = MutableOptionsBundle.from(config);
setTargetClass(FakeUseCase.class);
@@ -197,6 +204,13 @@
@NonNull
@Override
+ public Builder setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
+ getMutableConfig().insertOption(OPTION_MIRROR_MODE, mirrorMode);
+ return this;
+ }
+
+ @NonNull
+ @Override
public Builder setTargetResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(ImageOutputConfig.OPTION_TARGET_RESOLUTION, resolution);
return this;
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 7bd2de4..0f3fa1c 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -23,7 +23,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@@ -31,6 +30,7 @@
import android.util.Range;
import android.util.Size;
+import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.StreamSpec;
@@ -45,6 +45,7 @@
import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -69,8 +70,6 @@
private FakeUseCaseConfig mFakeUseCaseConfig;
- private List<UseCaseConfig<?>> mUseCaseConfigList;
-
@Before
public void setUp() {
mFakeCameraDeviceSurfaceManager = new FakeCameraDeviceSurfaceManager();
@@ -82,8 +81,6 @@
mFakeCameraDeviceSurfaceManager.setSuggestedStreamSpec(FAKE_CAMERA_ID1,
mFakeUseCaseConfig.getClass(),
StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
-
- mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
}
@Test
@@ -94,7 +91,7 @@
/* isConcurrentCameraModeOn = */false,
FAKE_CAMERA_ID0,
emptyList(),
- asList(preview, analysis));
+ createConfigOutputSizesMap(preview, analysis));
}
@Test(expected = IllegalArgumentException.class)
@@ -109,7 +106,7 @@
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
/* isConcurrentCameraModeOn = */false,
FAKE_CAMERA_ID0,
- singletonList(analysis), asList(preview, video));
+ singletonList(analysis), createConfigOutputSizesMap(preview, video));
}
@Test(expected = IllegalArgumentException.class)
@@ -120,7 +117,7 @@
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
/* isConcurrentCameraModeOn = */false,
FAKE_CAMERA_ID0,
- Collections.emptyList(), asList(preview, video, analysis));
+ Collections.emptyList(), createConfigOutputSizesMap(preview, video, analysis));
}
@Test
@@ -129,12 +126,12 @@
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
/* isConcurrentCameraModeOn = */false,
FAKE_CAMERA_ID0,
- Collections.emptyList(), mUseCaseConfigList);
+ Collections.emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig));
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecCamera1 =
mFakeCameraDeviceSurfaceManager.getSuggestedStreamSpecs(
/* isConcurrentCameraModeOn = */false,
FAKE_CAMERA_ID1,
- Collections.emptyList(), mUseCaseConfigList);
+ Collections.emptyList(), createConfigOutputSizesMap(mFakeUseCaseConfig));
assertThat(suggestedStreamSpecsCamera0.get(mFakeUseCaseConfig)).isEqualTo(
StreamSpec.builder(new Size(FAKE_WIDTH0, FAKE_HEIGHT0)).build());
@@ -142,4 +139,12 @@
StreamSpec.builder(new Size(FAKE_WIDTH1, FAKE_HEIGHT1)).build());
}
+ private Map<UseCaseConfig<?>, List<Size>> createConfigOutputSizesMap(
+ @NonNull UseCaseConfig<?>... useCaseConfigs) {
+ Map<UseCaseConfig<?>, List<Size>> configOutputSizesMap = new HashMap<>();
+ for (UseCaseConfig<?> useCaseConfig : useCaseConfigs) {
+ configOutputSizesMap.put(useCaseConfig, Collections.emptyList());
+ }
+ return configOutputSizesMap;
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index c8effb5..b0cc785 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -21,6 +21,7 @@
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MIRROR_MODE;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
@@ -70,6 +71,7 @@
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Logger;
+import androidx.camera.core.MirrorMode;
import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
@@ -269,6 +271,23 @@
}
}
+ // TODO: to public API
+ /**
+ * Returns the mirror mode.
+ *
+ * <p>The mirror mode is set by {@link VideoCapture.Builder#setMirrorMode(int)}. If not set,
+ * it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ *
+ * @return The mirror mode of the intended target.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @MirrorMode.Mirror
+ public int getMirrorMode() {
+ return getMirrorModeInternal();
+ }
+
/**
* {@inheritDoc}
*
@@ -391,7 +410,8 @@
SurfaceRequest surfaceRequest = mSurfaceRequest;
Rect cropRect = mCropRect;
if (cameraInternal != null && surfaceRequest != null && cropRect != null) {
- int relativeRotation = getRelativeRotation(cameraInternal);
+ int relativeRotation = getRelativeRotation(cameraInternal,
+ isMirroringRequired(cameraInternal));
int targetRotation = getAppTargetRotation();
if (mCameraEdge != null) {
mCameraEdge.setRotationDegrees(relativeRotation);
@@ -456,7 +476,8 @@
VideoEncoderInfo videoEncoderInfo = getVideoEncoderInfo(config.getVideoEncoderInfoFinder(),
videoCapabilities, mediaSpec, resolution, targetFpsRange);
mCropRect = calculateCropRect(resolution, videoEncoderInfo);
- mNode = createNodeIfNeeded(isCropNeeded(mCropRect, resolution));
+ boolean shouldMirror = camera.getHasTransform() && isMirroringRequired(camera);
+ mNode = createNodeIfNeeded(isCropNeeded(mCropRect, resolution), shouldMirror);
// Choose Timebase based on the whether the buffer is copied.
Timebase timebase;
if (mNode != null || !camera.getHasTransform()) {
@@ -479,8 +500,8 @@
getSensorToBufferTransformMatrix(),
camera.getHasTransform(),
mCropRect,
- getRelativeRotation(camera),
- /*mirroring=*/false);
+ getRelativeRotation(camera, isMirroringRequired(camera)),
+ shouldMirror);
cameraEdge.addOnInvalidatedListener(onSurfaceInvalidated);
mCameraEdge = cameraEdge;
SurfaceProcessorNode.OutConfig outConfig =
@@ -712,8 +733,9 @@
}
@Nullable
- private SurfaceProcessorNode createNodeIfNeeded(boolean isCropNeeded) {
- if (getEffect() != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded) {
+ private SurfaceProcessorNode createNodeIfNeeded(boolean isCropNeeded, boolean mirroring) {
+ if (getEffect() != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded
+ || mirroring) {
Logger.d(TAG, "Surface processing is enabled.");
return new SurfaceProcessorNode(requireNonNull(getCamera()),
getEffect() != null ? getEffect().createSurfaceProcessorInternal() :
@@ -1311,6 +1333,27 @@
return this;
}
+ // TODO: to public API
+ /**
+ * Sets the mirror mode.
+ *
+ * <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
+ * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
+ * If not set, it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+ *
+ * @param mirrorMode The mirror mode of the intended target.
+ * @return The current Builder.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder<T> setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
+ getMutableConfig().insertOption(OPTION_MIRROR_MODE, mirrorMode);
+ return this;
+ }
+
/**
* setTargetResolution is not supported on VideoCapture
*
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 2f35c7a..8c5c619 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -43,6 +43,9 @@
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.CameraXConfig
+import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCase
import androidx.camera.core.impl.CameraFactory
@@ -62,6 +65,7 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.processing.DefaultSurfaceProcessor
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
import androidx.camera.testing.EncoderProfilesUtil.PROFILES_1080P
@@ -98,6 +102,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.Collections
+import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import org.junit.After
import org.junit.Assert.assertThrows
@@ -139,6 +144,8 @@
@Before
fun setup() {
ShadowLog.stream = System.out
+
+ DefaultSurfaceProcessor.Factory.setSupplier { createFakeSurfaceProcessor() }
}
@After
@@ -281,7 +288,7 @@
// Arrange: create videoCapture.
setupCamera()
createCameraUseCaseAdapter()
- val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+ val processor = createFakeSurfaceProcessor()
val effect = createFakeEffect(processor)
val videoCapture = createVideoCapture(createVideoOutput())
cameraUseCaseAdapter.setEffects(listOf(effect))
@@ -729,6 +736,46 @@
}
@Test
+ fun defaultMirrorModeIsOff() {
+ val videoCapture = createVideoCapture()
+ assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_OFF)
+ }
+
+ @Test
+ fun canGetSetMirrorMode() {
+ val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_FRONT_ON)
+ assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_FRONT_ON)
+ }
+
+ @Test
+ fun setMirrorMode_nodeIsNeeded() {
+ // Arrange.
+ setupCamera()
+ createCameraUseCaseAdapter()
+
+ // Act.
+ val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_ON)
+ addAndAttachUseCases(videoCapture)
+
+ // Assert.
+ assertThat(videoCapture.node).isNotNull()
+ }
+
+ @Test
+ fun setMirrorMode_noCameraTransform_nodeIsNotNeeded() {
+ // Arrange.
+ setupCamera(hasTransform = false)
+ createCameraUseCaseAdapter()
+
+ // Act.
+ val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_ON)
+ addAndAttachUseCases(videoCapture)
+
+ // Assert: the input stream should already be mirrored.
+ assertThat(videoCapture.node).isNull()
+ }
+
+ @Test
fun setTargetRotationInBuilder_rotationIsChanged() {
// Act.
val videoCapture = createVideoCapture(targetRotation = Surface.ROTATION_180)
@@ -831,6 +878,37 @@
}
@Test
+ fun setTargetRotation_backCameraInitial0MirrorOn_transformationInfoUpdated() {
+ testSetTargetRotation_transformationInfoUpdated(
+ lensFacing = LENS_FACING_BACK,
+ sensorRotationDegrees = 90,
+ initialTargetRotation = Surface.ROTATION_0,
+ mirrorMode = MIRROR_MODE_ON
+ )
+ }
+
+ @Test
+ fun setTargetRotation_backCameraInitial0NoCameraTransform_transformationInfoUpdated() {
+ testSetTargetRotation_transformationInfoUpdated(
+ lensFacing = LENS_FACING_BACK,
+ sensorRotationDegrees = 90,
+ hasCameraTransform = false,
+ initialTargetRotation = Surface.ROTATION_0,
+ )
+ }
+
+ @Test
+ fun setTargetRotation_backCameraInitial0NoCameraTransformMirrorOn_transformationInfoUpdated() {
+ testSetTargetRotation_transformationInfoUpdated(
+ lensFacing = LENS_FACING_BACK,
+ sensorRotationDegrees = 90,
+ hasCameraTransform = false,
+ initialTargetRotation = Surface.ROTATION_0,
+ mirrorMode = MIRROR_MODE_ON,
+ )
+ }
+
+ @Test
fun setTargetRotation_backCameraInitial90_transformationInfoUpdated() {
testSetTargetRotation_transformationInfoUpdated(
lensFacing = LENS_FACING_BACK,
@@ -849,6 +927,16 @@
}
@Test
+ fun setTargetRotation_frontCameraInitial0MirrorOn_transformationInfoUpdated() {
+ testSetTargetRotation_transformationInfoUpdated(
+ lensFacing = LENS_FACING_FRONT,
+ sensorRotationDegrees = 270,
+ initialTargetRotation = Surface.ROTATION_0,
+ mirrorMode = MIRROR_MODE_ON
+ )
+ }
+
+ @Test
fun setTargetRotation_frontCameraInitial90_transformationInfoUpdated() {
testSetTargetRotation_transformationInfoUpdated(
lensFacing = LENS_FACING_FRONT,
@@ -900,11 +988,17 @@
private fun testSetTargetRotation_transformationInfoUpdated(
lensFacing: Int = LENS_FACING_BACK,
sensorRotationDegrees: Int = 0,
+ hasCameraTransform: Boolean = true,
effect: CameraEffect? = null,
initialTargetRotation: Int = Surface.ROTATION_0,
+ mirrorMode: Int? = null,
) {
// Arrange.
- setupCamera(lensFacing = lensFacing, sensorRotation = sensorRotationDegrees)
+ setupCamera(
+ lensFacing = lensFacing,
+ sensorRotation = sensorRotationDegrees,
+ hasTransform = hasCameraTransform,
+ )
createCameraUseCaseAdapter(lensFacing = lensFacing)
var transformationInfo: SurfaceRequest.TransformationInfo? = null
val videoOutput = createVideoOutput(
@@ -916,7 +1010,12 @@
}
}
)
- val videoCapture = createVideoCapture(videoOutput, targetRotation = initialTargetRotation)
+ val videoCapture = createVideoCapture(
+ videoOutput,
+ targetRotation = initialTargetRotation,
+ mirrorMode = mirrorMode
+ )
+ val requireMirroring = videoCapture.isMirroringRequired(camera)
// Act.
effect?.apply { cameraUseCaseAdapter.setEffects(listOf(this)) }
@@ -926,8 +1025,8 @@
// Assert.
var videoContentDegrees: Int
var metadataDegrees: Int
- cameraInfo.getSensorRotationDegrees(initialTargetRotation).let {
- if (effect != null) {
+ cameraInfo.getRelativeRotation(initialTargetRotation, requireMirroring).let {
+ if (videoCapture.node != null) {
// If effect is enabled, the rotation is applied on video content but not metadata.
videoContentDegrees = it
metadataDegrees = 0
@@ -949,8 +1048,8 @@
shadowOf(Looper.getMainLooper()).idle()
// Assert.
- val requiredDegrees = cameraInfo.getSensorRotationDegrees(targetRotation)
- val expectedDegrees = if (effect != null) {
+ val requiredDegrees = cameraInfo.getRelativeRotation(targetRotation, requireMirroring)
+ val expectedDegrees = if (videoCapture.node != null) {
// If effect is enabled, the rotation should eliminate the video content rotation.
within360(requiredDegrees - videoContentDegrees)
} else {
@@ -961,6 +1060,7 @@
", initialTargetRotation = $initialTargetRotation" +
", targetRotation = ${surfaceRotationToDegrees(targetRotation)}" +
", effect = ${effect != null}" +
+ ", requireMirroring = $requireMirroring" +
", videoContentDegrees = $videoContentDegrees" +
", metadataDegrees = $metadataDegrees" +
", requiredDegrees = $requiredDegrees" +
@@ -976,10 +1076,7 @@
// Arrange.
setupCamera()
createCameraUseCaseAdapter()
- val processor = FakeSurfaceProcessorInternal(
- mainThreadExecutor(),
- false
- )
+ val processor = createFakeSurfaceProcessor(autoCloseSurfaceOutput = false)
var appSurfaceReadyToRelease = false
val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest, _ ->
surfaceRequest.provideSurface(
@@ -1225,6 +1322,7 @@
private fun createVideoCapture(
videoOutput: VideoOutput = createVideoOutput(),
targetRotation: Int? = null,
+ mirrorMode: Int? = null,
targetResolution: Size? = null,
videoEncoderInfoFinder: Function<VideoEncoderConfig, VideoEncoderInfo> =
Function { createVideoEncoderInfo() },
@@ -1232,19 +1330,22 @@
.setSessionOptionUnpacker { _, _ -> }
.apply {
targetRotation?.let { setTargetRotation(it) }
+ mirrorMode?.let { setMirrorMode(it) }
targetResolution?.let { setTargetResolution(it) }
setVideoEncoderInfoFinder(videoEncoderInfoFinder)
}.build()
private fun createFakeEffect(
- processor: FakeSurfaceProcessorInternal = FakeSurfaceProcessorInternal(
- mainThreadExecutor()
- )
- ) =
- FakeSurfaceEffect(
- VIDEO_CAPTURE,
- processor
- )
+ processor: FakeSurfaceProcessorInternal = createFakeSurfaceProcessor()
+ ) = FakeSurfaceEffect(
+ VIDEO_CAPTURE,
+ processor
+ )
+
+ private fun createFakeSurfaceProcessor(
+ executor: Executor = mainThreadExecutor(),
+ autoCloseSurfaceOutput: Boolean = true
+ ) = FakeSurfaceProcessorInternal(executor, autoCloseSurfaceOutput)
private fun createBackgroundHandler(): Handler {
val handler = Handler(HandlerThread("VideoCaptureTest").run {
@@ -1303,6 +1404,15 @@
CameraXUtil.initialize(context, cameraXConfig).get()
}
+ private fun CameraInfoInternal.getRelativeRotation(
+ targetRotation: Int,
+ requireMirroring: Boolean
+ ) = getSensorRotationDegrees(targetRotation).let {
+ if (requireMirroring) {
+ within360(-it)
+ } else it
+ }
+
companion object {
private val CAMERA_0_QUALITY_SIZE: Map<Quality, Size> = mapOf(
SD to RESOLUTION_480P,
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 4004eae..a754486 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -35,7 +35,7 @@
*/
api("androidx.annotation:annotation:1.1.0")
- api("androidx.compose.ui:ui:1.2.1")
+ api(project(":compose:ui:ui"))
api("androidx.compose.ui:ui-unit:1.2.1")
implementation("androidx.compose.runtime:runtime:1.2.1")
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
index 354bea8..3aac9c3 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
@@ -18,13 +18,12 @@
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
@@ -47,17 +46,15 @@
* @sample androidx.compose.foundation.layout.samples.OffsetModifier
*/
@Stable
-fun Modifier.offset(x: Dp = 0.dp, y: Dp = 0.dp) = this.then(
- OffsetModifier(
- x = x,
- y = y,
- rtlAware = true,
- inspectorInfo = debugInspectorInfo {
- name = "offset"
- properties["x"] = x
- properties["y"] = y
- }
- )
+fun Modifier.offset(x: Dp = 0.dp, y: Dp = 0.dp) = this then OffsetModifierElement(
+ x = x,
+ y = y,
+ rtlAware = true,
+ inspectorInfo = {
+ name = "offset"
+ properties["x"] = x
+ properties["y"] = y
+ }
)
/**
@@ -75,17 +72,15 @@
* @sample androidx.compose.foundation.layout.samples.AbsoluteOffsetModifier
*/
@Stable
-fun Modifier.absoluteOffset(x: Dp = 0.dp, y: Dp = 0.dp) = this.then(
- OffsetModifier(
- x = x,
- y = y,
- rtlAware = false,
- inspectorInfo = debugInspectorInfo {
- name = "absoluteOffset"
- properties["x"] = x
- properties["y"] = y
- }
- )
+fun Modifier.absoluteOffset(x: Dp = 0.dp, y: Dp = 0.dp) = this then OffsetModifierElement(
+ x = x,
+ y = y,
+ rtlAware = false,
+ inspectorInfo = {
+ name = "absoluteOffset"
+ properties["x"] = x
+ properties["y"] = y
+ }
)
/**
@@ -107,16 +102,15 @@
* Example usage:
* @sample androidx.compose.foundation.layout.samples.OffsetPxModifier
*/
-fun Modifier.offset(offset: Density.() -> IntOffset) = this.then(
- OffsetPxModifier(
+fun Modifier.offset(offset: Density.() -> IntOffset) = this then
+ OffsetPxModifierElement(
offset = offset,
rtlAware = true,
- inspectorInfo = debugInspectorInfo {
+ inspectorInfo = {
name = "offset"
properties["offset"] = offset
}
)
-)
/**
* Offset the content by [offset] px. The offsets can be positive as well as non-positive.
@@ -138,23 +132,58 @@
*/
fun Modifier.absoluteOffset(
offset: Density.() -> IntOffset
-) = this.then(
- OffsetPxModifier(
- offset = offset,
- rtlAware = false,
- inspectorInfo = debugInspectorInfo {
- name = "absoluteOffset"
- properties["offset"] = offset
- }
- )
+) = this then OffsetPxModifierElement(
+ offset = offset,
+ rtlAware = false,
+ inspectorInfo = {
+ name = "absoluteOffset"
+ properties["offset"] = offset
+ }
)
-private class OffsetModifier(
+private class OffsetModifierElement(
val x: Dp,
val y: Dp,
val rtlAware: Boolean,
- inspectorInfo: InspectorInfo.() -> Unit
-) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+ val inspectorInfo: InspectorInfo.() -> Unit
+) : ModifierNodeElement<OffsetModifier>() {
+ override fun create(): OffsetModifier {
+ return OffsetModifier(x, y, rtlAware)
+ }
+
+ override fun update(node: OffsetModifier): OffsetModifier = node.also {
+ it.x = x
+ it.y = y
+ it.rtlAware = rtlAware
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ val otherModifierElement = other as? OffsetModifierElement ?: return false
+
+ return x == otherModifierElement.x &&
+ y == otherModifierElement.y &&
+ rtlAware == otherModifierElement.rtlAware
+ }
+
+ override fun hashCode(): Int {
+ var result = x.hashCode()
+ result = 31 * result + y.hashCode()
+ result = 31 * result + rtlAware.hashCode()
+ return result
+ }
+
+ override fun toString(): String = "OffsetModifierElement(x=$x, y=$y, rtlAware=$rtlAware)"
+
+ override fun InspectorInfo.inspectableProperties() { inspectorInfo() }
+}
+
+private class OffsetModifier(
+ var x: Dp,
+ var y: Dp,
+ var rtlAware: Boolean
+) : LayoutModifierNode, Modifier.Node() {
+
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
@@ -168,31 +197,47 @@
}
}
}
+}
+
+private class OffsetPxModifierElement(
+ val offset: Density.() -> IntOffset,
+ val rtlAware: Boolean,
+ val inspectorInfo: InspectorInfo.() -> Unit
+) : ModifierNodeElement<OffsetPxModifier>() {
+ override fun create(): OffsetPxModifier {
+ return OffsetPxModifier(offset, rtlAware)
+ }
+
+ override fun update(node: OffsetPxModifier): OffsetPxModifier = node.also {
+ it.offset = offset
+ it.rtlAware = rtlAware
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
- val otherModifier = other as? OffsetModifier ?: return false
+ val otherModifier = other as? OffsetPxModifierElement ?: return false
- return x == otherModifier.x &&
- y == otherModifier.y &&
+ return offset == otherModifier.offset &&
rtlAware == otherModifier.rtlAware
}
+ override fun toString(): String = "OffsetPxModifier(offset=$offset, rtlAware=$rtlAware)"
+
override fun hashCode(): Int {
- var result = x.hashCode()
- result = 31 * result + y.hashCode()
+ var result = offset.hashCode()
result = 31 * result + rtlAware.hashCode()
return result
}
- override fun toString(): String = "OffsetModifier(x=$x, y=$y, rtlAware=$rtlAware)"
+ override fun InspectorInfo.inspectableProperties() {
+ inspectorInfo()
+ }
}
private class OffsetPxModifier(
- val offset: Density.() -> IntOffset,
- val rtlAware: Boolean,
- inspectorInfo: InspectorInfo.() -> Unit
-) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+ var offset: Density.() -> IntOffset,
+ var rtlAware: Boolean
+) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
@@ -207,20 +252,4 @@
}
}
}
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- val otherModifier = other as? OffsetPxModifier ?: return false
-
- return offset == otherModifier.offset &&
- rtlAware == otherModifier.rtlAware
- }
-
- override fun hashCode(): Int {
- var result = offset.hashCode()
- result = 31 * result + rtlAware.hashCode()
- return result
- }
-
- override fun toString(): String = "OffsetPxModifier(offset=$offset, rtlAware=$rtlAware)"
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/PlatformTextInputAdapterDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/PlatformTextInputAdapterDemo.kt
index e3a3777..193fd9d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/PlatformTextInputAdapterDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/PlatformTextInputAdapterDemo.kt
@@ -347,10 +347,6 @@
endBatchEdit()
}
- override fun submitTextForTest() {
- throw UnsupportedOperationException("just a test")
- }
-
// endregion
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 37899be..3d8a7782 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -42,6 +42,7 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.computeSizeForDefaultText
import androidx.compose.runtime.Composable
@@ -84,6 +85,7 @@
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
@@ -96,6 +98,7 @@
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTextClearance
@@ -156,6 +159,7 @@
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
+import kotlin.test.assertFailsWith
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Ignore
@@ -608,6 +612,63 @@
}
@Test
+ fun semantics_imeEnterAction() {
+ var done = false
+ rule.setContent {
+ var value by remember { mutableStateOf("") }
+ BasicTextField(
+ modifier = Modifier.testTag(Tag),
+ value = value,
+ onValueChange = { value = it },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { done = true })
+ )
+ }
+
+ rule.onNodeWithTag(Tag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertIsFocused()
+
+ rule.runOnIdle {
+ assertThat(done).isFalse()
+ }
+
+ rule.onNodeWithTag(Tag)
+ .performImeAction()
+
+ rule.runOnIdle {
+ assertThat(done).isTrue()
+ }
+ }
+
+ @Test
+ fun semantics_defaultImeEnterAction() {
+ rule.setContent {
+ var value by remember { mutableStateOf("") }
+ BasicTextField(
+ modifier = Modifier.testTag(Tag),
+ value = value,
+ onValueChange = { value = it },
+ keyboardActions = KeyboardActions()
+ )
+ }
+
+ rule.onNodeWithTag(Tag)
+ .performSemanticsAction(SemanticsActions.RequestFocus)
+ .assertIsFocused()
+
+ val error = assertFailsWith<AssertionError> {
+ rule.onNodeWithTag(Tag)
+ .performImeAction()
+ }
+ assertThat(error).hasMessageThat().startsWith(
+ "Failed to perform IME action.\n" +
+ "Failed to assert the following: (NOT (ImeAction = 'Default'))\n" +
+ "Semantics of the node:"
+ )
+ }
+
+ @Test
fun semantics_setTextSetSelectionActions() {
rule.setContent {
var value by remember { mutableStateOf("") }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
index b988c4c..448e1a3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt
@@ -22,11 +22,12 @@
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
@@ -46,36 +47,27 @@
}
override fun Modifier.fillParentMaxSize(fraction: Float) = then(
- ParentSizeModifier(
+ ParentSizeModifierElement(
widthState = maxWidthState,
heightState = maxHeightState,
fraction = fraction,
- inspectorInfo = debugInspectorInfo {
- name = "fillParentMaxSize"
- value = fraction
- }
+ inspectorName = "fillParentMaxSize"
)
)
override fun Modifier.fillParentMaxWidth(fraction: Float) = then(
- ParentSizeModifier(
+ ParentSizeModifierElement(
widthState = maxWidthState,
fraction = fraction,
- inspectorInfo = debugInspectorInfo {
- name = "fillParentMaxWidth"
- value = fraction
- }
+ inspectorName = "fillParentMaxWidth"
)
)
override fun Modifier.fillParentMaxHeight(fraction: Float) = then(
- ParentSizeModifier(
+ ParentSizeModifierElement(
heightState = maxHeightState,
fraction = fraction,
- inspectorInfo = debugInspectorInfo {
- name = "fillParentMaxHeight"
- value = fraction
- }
+ inspectorName = "fillParentMaxHeight"
)
)
@@ -87,27 +79,72 @@
}))
}
-private class ParentSizeModifier(
+private class ParentSizeModifierElement(
val fraction: Float,
- inspectorInfo: InspectorInfo.() -> Unit,
val widthState: State<Int>? = null,
val heightState: State<Int>? = null,
-) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+ val inspectorName: String
+) : ModifierNodeElement<ParentSizeModifier>() {
+ override fun create(): ParentSizeModifier {
+ return ParentSizeModifier(
+ fraction = fraction,
+ widthState = widthState,
+ heightState = heightState
+ )
+ }
+
+ override fun update(node: ParentSizeModifier): ParentSizeModifier = node.also {
+ it.fraction = fraction
+ it.widthState = widthState
+ it.heightState = heightState
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ParentSizeModifier) return false
+ return fraction == other.fraction &&
+ widthState == other.widthState &&
+ heightState == other.heightState
+ }
+
+ override fun hashCode(): Int {
+ var result = widthState?.hashCode() ?: 0
+ result = 31 * result + (heightState?.hashCode() ?: 0)
+ result = 31 * result + fraction.hashCode()
+ return result
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = inspectorName
+ value = fraction
+ }
+}
+
+private class ParentSizeModifier(
+ var fraction: Float,
+ var widthState: State<Int>? = null,
+ var heightState: State<Int>? = null,
+) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
- val width = if (widthState != null && widthState.value != Constraints.Infinity) {
- (widthState.value * fraction).roundToInt()
- } else {
- Constraints.Infinity
- }
- val height = if (heightState != null && heightState.value != Constraints.Infinity) {
- (heightState.value * fraction).roundToInt()
- } else {
- Constraints.Infinity
- }
+ val width = widthState?.let {
+ if (it.value != Constraints.Infinity) {
+ (it.value * fraction).roundToInt()
+ } else {
+ Constraints.Infinity
+ }
+ } ?: Constraints.Infinity
+
+ val height = heightState?.let {
+ if (it.value != Constraints.Infinity) {
+ (it.value * fraction).roundToInt()
+ } else {
+ Constraints.Infinity
+ }
+ } ?: Constraints.Infinity
val childConstraints = Constraints(
minWidth = if (width != Constraints.Infinity) width else constraints.minWidth,
minHeight = if (height != Constraints.Infinity) height else constraints.minHeight,
@@ -119,21 +156,6 @@
placeable.place(0, 0)
}
}
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ParentSizeModifier) return false
- return widthState == other.widthState &&
- heightState == other.heightState &&
- fraction == other.fraction
- }
-
- override fun hashCode(): Int {
- var result = widthState?.hashCode() ?: 0
- result = 31 * result + (heightState?.hashCode() ?: 0)
- result = 31 * result + fraction.hashCode()
- return result
- }
}
private class AnimateItemPlacementModifier(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index f935bfe..e5427cd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -82,6 +82,7 @@
import androidx.compose.ui.semantics.editableText
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.imeAction
+import androidx.compose.ui.semantics.performImeAction
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.password
@@ -478,6 +479,13 @@
false
}
}
+ performImeAction {
+ // This will perform the appropriate default action if no handler has been specified, so
+ // as far as the platform is concerned, we always handle the action and never want to
+ // defer to the default _platform_ implementation.
+ state.onImeActionPerformed(imeOptions.imeAction)
+ true
+ }
onClick {
// according to the documentation, we still need to provide proper semantics actions
// even if the state is 'disabled'
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 ca943ef..48cb5ef 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
@@ -66,6 +66,11 @@
val PackageName = Package("androidx.compose.ui.unit")
val Dp = Name(PackageName, "Dp")
}
+
+ object Node {
+ val PackageName = Package(Ui.PackageName, "node")
+ val CurrentValueOf = Name(PackageName, "currentValueOf")
+ }
}
object UiGraphics {
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
new file mode 100644
index 0000000..e28d869
--- /dev/null
+++ b/compose/runtime/runtime/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentCompositionLocalMap():
+ Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
+AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
+ Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 8e89aa50..e6b9f52 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -127,6 +127,7 @@
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public androidx.compose.runtime.CompositionLocalMap getCurrentCompositionLocalMap();
method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
@@ -153,6 +154,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract androidx.compose.runtime.CompositionLocalMap currentCompositionLocalMap;
property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
@@ -188,6 +190,8 @@
}
public abstract class CompositionContext {
+ method public abstract kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
+ property public abstract kotlin.coroutines.CoroutineContext effectCoroutineContext;
}
public final class CompositionKt {
@@ -210,6 +214,16 @@
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> staticCompositionLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
+ public sealed interface CompositionLocalMap {
+ method public operator <T> T! get(androidx.compose.runtime.CompositionLocal<T> key);
+ field public static final androidx.compose.runtime.CompositionLocalMap.Companion Companion;
+ }
+
+ public static final class CompositionLocalMap.Companion {
+ method public androidx.compose.runtime.CompositionLocalMap getEmpty();
+ property public final androidx.compose.runtime.CompositionLocalMap Empty;
+ }
+
public sealed interface ControlledComposition extends androidx.compose.runtime.Composition {
method public void applyChanges();
method public void applyLateChanges();
@@ -346,12 +360,14 @@
method public void close();
method public long getChangeCount();
method public kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> getCurrentState();
+ method public kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
method public boolean getHasPendingWork();
method @Deprecated public kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> getState();
method public suspend Object? join(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? runRecomposeAndApplyChanges(kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public final long changeCount;
property public final kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> currentState;
+ property public kotlin.coroutines.CoroutineContext effectCoroutineContext;
property public final boolean hasPendingWork;
property @Deprecated public final kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> state;
field public static final androidx.compose.runtime.Recomposer.Companion Companion;
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 972dbc1..892930c 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -135,6 +135,7 @@
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public androidx.compose.runtime.CompositionLocalMap getCurrentCompositionLocalMap();
method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
@@ -167,6 +168,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract androidx.compose.runtime.CompositionLocalMap currentCompositionLocalMap;
property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
@@ -203,6 +205,8 @@
}
public abstract class CompositionContext {
+ method public abstract kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
+ property public abstract kotlin.coroutines.CoroutineContext effectCoroutineContext;
}
public final class CompositionKt {
@@ -228,6 +232,16 @@
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> staticCompositionLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
+ public sealed interface CompositionLocalMap {
+ method public operator <T> T! get(androidx.compose.runtime.CompositionLocal<T> key);
+ field public static final androidx.compose.runtime.CompositionLocalMap.Companion Companion;
+ }
+
+ public static final class CompositionLocalMap.Companion {
+ method public androidx.compose.runtime.CompositionLocalMap getEmpty();
+ property public final androidx.compose.runtime.CompositionLocalMap Empty;
+ }
+
@androidx.compose.runtime.InternalComposeTracingApi public interface CompositionTracer {
method public boolean isTraceInProgress();
method public void traceEventEnd();
@@ -394,6 +408,7 @@
method public void close();
method public long getChangeCount();
method public kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> getCurrentState();
+ method public kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
method public boolean getHasPendingWork();
method @Deprecated public kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> getState();
method public suspend Object? join(kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -401,6 +416,7 @@
method @androidx.compose.runtime.ExperimentalComposeApi public suspend Object? runRecomposeConcurrentlyAndApplyChanges(kotlin.coroutines.CoroutineContext recomposeCoroutineContext, kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public final long changeCount;
property public final kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> currentState;
+ property public kotlin.coroutines.CoroutineContext effectCoroutineContext;
property public final boolean hasPendingWork;
property @Deprecated public final kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> state;
field public static final androidx.compose.runtime.Recomposer.Companion Companion;
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..e28d869
--- /dev/null
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.runtime.Composer#getCurrentCompositionLocalMap():
+ Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
+AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
+ Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 9c2a046..39b95b6 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -132,6 +132,7 @@
method @org.jetbrains.annotations.TestOnly public androidx.compose.runtime.ControlledComposition getComposition();
method public androidx.compose.runtime.tooling.CompositionData getCompositionData();
method public int getCompoundKeyHash();
+ method public androidx.compose.runtime.CompositionLocalMap getCurrentCompositionLocalMap();
method public int getCurrentMarker();
method public boolean getDefaultsInvalid();
method public boolean getInserting();
@@ -158,6 +159,7 @@
property @org.jetbrains.annotations.TestOnly public abstract androidx.compose.runtime.ControlledComposition composition;
property public abstract androidx.compose.runtime.tooling.CompositionData compositionData;
property public abstract int compoundKeyHash;
+ property public abstract androidx.compose.runtime.CompositionLocalMap currentCompositionLocalMap;
property public abstract int currentMarker;
property public abstract boolean defaultsInvalid;
property public abstract boolean inserting;
@@ -206,6 +208,8 @@
}
public abstract class CompositionContext {
+ method public abstract kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
+ property public abstract kotlin.coroutines.CoroutineContext effectCoroutineContext;
}
public final class CompositionKt {
@@ -228,6 +232,16 @@
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> staticCompositionLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
+ public sealed interface CompositionLocalMap {
+ method public operator <T> T! get(androidx.compose.runtime.CompositionLocal<T> key);
+ field public static final androidx.compose.runtime.CompositionLocalMap.Companion Companion;
+ }
+
+ public static final class CompositionLocalMap.Companion {
+ method public androidx.compose.runtime.CompositionLocalMap getEmpty();
+ property public final androidx.compose.runtime.CompositionLocalMap Empty;
+ }
+
@kotlin.PublishedApi internal final class CompositionScopedCoroutineScopeCanceller implements androidx.compose.runtime.RememberObserver {
ctor public CompositionScopedCoroutineScopeCanceller(kotlinx.coroutines.CoroutineScope coroutineScope);
method public kotlinx.coroutines.CoroutineScope getCoroutineScope();
@@ -382,12 +396,14 @@
method public void close();
method public long getChangeCount();
method public kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> getCurrentState();
+ method public kotlin.coroutines.CoroutineContext getEffectCoroutineContext();
method public boolean getHasPendingWork();
method @Deprecated public kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> getState();
method public suspend Object? join(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? runRecomposeAndApplyChanges(kotlin.coroutines.Continuation<? super kotlin.Unit>);
property public final long changeCount;
property public final kotlinx.coroutines.flow.StateFlow<androidx.compose.runtime.Recomposer.State> currentState;
+ property public kotlin.coroutines.CoroutineContext effectCoroutineContext;
property public final boolean hasPendingWork;
property @Deprecated public final kotlinx.coroutines.flow.Flow<androidx.compose.runtime.Recomposer.State> state;
field public static final androidx.compose.runtime.Recomposer.Companion Companion;
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index f0f620f..319bccf 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -23,8 +23,7 @@
import androidx.compose.runtime.collection.IdentityArrayMap
import androidx.compose.runtime.collection.IdentityArraySet
import androidx.compose.runtime.collection.IntMap
-import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap
-import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentHashMapOf
+import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.snapshots.currentSnapshot
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.fastForEachIndexed
@@ -298,42 +297,6 @@
)
/**
- * A [CompositionLocal] map is is an immutable map that maps [CompositionLocal] keys to a provider
- * of their current value. It is used to represent the combined scope of all provided
- * [CompositionLocal]s.
- */
-internal typealias CompositionLocalMap = PersistentMap<CompositionLocal<Any?>, State<Any?>>
-
-internal inline fun CompositionLocalMap.mutate(
- mutator: (MutableMap<CompositionLocal<Any?>, State<Any?>>) -> Unit
-): CompositionLocalMap = builder().apply(mutator).build()
-
-@Suppress("UNCHECKED_CAST")
-internal fun <T> CompositionLocalMap.contains(key: CompositionLocal<T>) =
- this.containsKey(key as CompositionLocal<Any?>)
-
-@Suppress("UNCHECKED_CAST")
-internal fun <T> CompositionLocalMap.getValueOf(key: CompositionLocal<T>) =
- this[key as CompositionLocal<Any?>]?.value as T
-
-@Composable
-private fun compositionLocalMapOf(
- values: Array<out ProvidedValue<*>>,
- parentScope: CompositionLocalMap
-): CompositionLocalMap {
- val result: CompositionLocalMap = persistentHashMapOf()
- return result.mutate {
- for (provided in values) {
- if (provided.canOverride || !parentScope.contains(provided.compositionLocal)) {
- @Suppress("UNCHECKED_CAST")
- it[provided.compositionLocal as CompositionLocal<Any?>] =
- provided.compositionLocal.provided(provided.value)
- }
- }
- }
-}
-
-/**
* A Compose compiler plugin API. DO NOT call directly.
*
* An instance used to track the identity of the movable content. Using a holder object allows
@@ -358,7 +321,7 @@
internal val slotTable: SlotTable,
internal val anchor: Anchor,
internal val invalidations: List<Pair<RecomposeScopeImpl, IdentityArraySet<Any>?>>,
- internal val locals: CompositionLocalMap
+ internal val locals: PersistentCompositionLocalMap
)
/**
@@ -990,6 +953,26 @@
fun recordSideEffect(effect: () -> Unit)
/**
+ * Returns the active set of CompositionLocals at the current position in the composition
+ * hierarchy. This is a lower level API that can be used to export and access CompositionLocal
+ * values outside of Composition.
+ *
+ * This API does not track reads of CompositionLocals and does not automatically dispatch new
+ * values to previous readers when the value of a CompositionLocal changes. To use this API as
+ * intended, you must set up observation manually. This means:
+ * - For [non-static CompositionLocals][compositionLocalOf], composables reading this map need
+ * to observe the snapshot state for CompositionLocals being read to be notified when their
+ * values in this map change.
+ * - For [static CompositionLocals][staticCompositionLocalOf], all composables including the
+ * composable reading this map will be recomposed and you will need to re-obtain this map to
+ * get the latest values.
+ *
+ * Most applications shouldn't use this API directly, and should instead use
+ * [CompositionLocal.current].
+ */
+ val currentCompositionLocalMap: CompositionLocalMap
+
+ /**
* A Compose internal function. DO NOT call directly.
*
* Return the [CompositionLocal] value associated with [key]. This is the primitive function
@@ -1267,8 +1250,9 @@
private var nodeExpected = false
private val invalidations: MutableList<Invalidation> = mutableListOf()
private val entersStack = IntStack()
- private var parentProvider: CompositionLocalMap = persistentHashMapOf()
- private val providerUpdates = IntMap<CompositionLocalMap>()
+ private var parentProvider: PersistentCompositionLocalMap =
+ persistentCompositionLocalHashMapOf()
+ private val providerUpdates = IntMap<PersistentCompositionLocalMap>()
private var providersInvalid = false
private val providersInvalidStack = IntStack()
private var reusing = false
@@ -1294,7 +1278,7 @@
private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
private var writerHasAProvider = false
- private var providerCache: CompositionLocalMap? = null
+ private var providerCache: PersistentCompositionLocalMap? = null
internal var deferredChanges: MutableList<Change>? = null
private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
@@ -1444,7 +1428,7 @@
if (!forceRecomposeScopes) {
forceRecomposeScopes = parentContext.collectingParameterInformation
}
- resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let {
+ parentProvider.read(LocalInspectionTables)?.let {
it.add(slotTable)
parentContext.recordInspectionTable(it)
}
@@ -1912,15 +1896,18 @@
record { _, _, rememberManager -> rememberManager.sideEffect(effect) }
}
- private fun currentCompositionLocalScope(): CompositionLocalMap {
+ private fun currentCompositionLocalScope(): PersistentCompositionLocalMap {
providerCache?.let { return it }
return currentCompositionLocalScope(reader.parent)
}
+ override val currentCompositionLocalMap: CompositionLocalMap
+ get() = currentCompositionLocalScope()
+
/**
* Return the current [CompositionLocal] scope which was provided by a parent group.
*/
- private fun currentCompositionLocalScope(group: Int): CompositionLocalMap {
+ private fun currentCompositionLocalScope(group: Int): PersistentCompositionLocalMap {
if (inserting && writerHasAProvider) {
var current = writer.parent
while (current > 0) {
@@ -1928,7 +1915,7 @@
writer.groupObjectKey(current) == compositionLocalMap
) {
@Suppress("UNCHECKED_CAST")
- val providers = writer.groupAux(current) as CompositionLocalMap
+ val providers = writer.groupAux(current) as PersistentCompositionLocalMap
providerCache = providers
return providers
}
@@ -1943,7 +1930,7 @@
) {
@Suppress("UNCHECKED_CAST")
val providers = providerUpdates[current]
- ?: reader.groupAux(current) as CompositionLocalMap
+ ?: reader.groupAux(current) as PersistentCompositionLocalMap
providerCache = providers
return providers
}
@@ -1960,9 +1947,9 @@
* inserts, updates and deletes to the providers.
*/
private fun updateProviderMapGroup(
- parentScope: CompositionLocalMap,
- currentProviders: CompositionLocalMap
- ): CompositionLocalMap {
+ parentScope: PersistentCompositionLocalMap,
+ currentProviders: PersistentCompositionLocalMap
+ ): PersistentCompositionLocalMap {
val providerScope = parentScope.mutate { it.putAll(currentProviders) }
startGroup(providerMapsKey, providerMaps)
changed(providerScope)
@@ -1983,7 +1970,7 @@
compositionLocalMapOf(values, parentScope)
}
endGroup()
- val providers: CompositionLocalMap
+ val providers: PersistentCompositionLocalMap
val invalid: Boolean
if (inserting) {
providers = updateProviderMapGroup(parentScope, currentProviders)
@@ -1991,10 +1978,10 @@
writerHasAProvider = true
} else {
@Suppress("UNCHECKED_CAST")
- val oldScope = reader.groupGet(0) as CompositionLocalMap
+ val oldScope = reader.groupGet(0) as PersistentCompositionLocalMap
@Suppress("UNCHECKED_CAST")
- val oldValues = reader.groupGet(1) as CompositionLocalMap
+ val oldValues = reader.groupGet(1) as PersistentCompositionLocalMap
// skipping is true iff parentScope has not changed.
if (!skipping || oldValues != currentProviders) {
@@ -2033,8 +2020,7 @@
}
@InternalComposeApi
- override fun <T> consume(key: CompositionLocal<T>): T =
- resolveCompositionLocal(key, currentCompositionLocalScope())
+ override fun <T> consume(key: CompositionLocal<T>): T = currentCompositionLocalScope().read(key)
/**
* Create or use a memoized [CompositionContext] instance at this position in the slot table.
@@ -2060,15 +2046,6 @@
return holder.ref
}
- private fun <T> resolveCompositionLocal(
- key: CompositionLocal<T>,
- scope: CompositionLocalMap
- ): T = if (scope.contains(key)) {
- scope.getValueOf(key)
- } else {
- key.defaultValueHolder.value
- }
-
/**
* The number of changes that have been scheduled to be applied during
* [ControlledComposition.applyChanges].
@@ -2898,7 +2875,7 @@
private fun invokeMovableContentLambda(
content: MovableContent<Any?>,
- locals: CompositionLocalMap,
+ locals: PersistentCompositionLocalMap,
parameter: Any?,
force: Boolean
) {
@@ -3974,13 +3951,14 @@
// we need changes made to it in composition to be visible for the rest of the current
// composition and not become visible outside of the composition process until composition
// succeeds.
- private var compositionLocalScope by mutableStateOf<CompositionLocalMap>(
- persistentHashMapOf()
+ private var compositionLocalScope by mutableStateOf<PersistentCompositionLocalMap>(
+ persistentCompositionLocalHashMapOf()
)
- override fun getCompositionLocalScope(): CompositionLocalMap = compositionLocalScope
+ override fun getCompositionLocalScope(): PersistentCompositionLocalMap =
+ compositionLocalScope
- fun updateCompositionLocalScope(scope: CompositionLocalMap) {
+ fun updateCompositionLocalScope(scope: PersistentCompositionLocalMap) {
compositionLocalScope = scope
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 6115b17..596d03d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -16,11 +16,12 @@
package androidx.compose.runtime
+import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.tooling.CompositionData
-import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentHashMapOf
import kotlin.coroutines.CoroutineContext
-private val EmptyCompositionLocalMap: CompositionLocalMap = persistentHashMapOf()
+private val EmptyPersistentCompositionLocalMap: PersistentCompositionLocalMap =
+ persistentCompositionLocalHashMapOf()
/**
* A [CompositionContext] is an opaque type that is used to logically "link" two compositions
@@ -37,7 +38,10 @@
abstract class CompositionContext internal constructor() {
internal abstract val compoundHashKey: Int
internal abstract val collectingParameterInformation: Boolean
- internal abstract val effectCoroutineContext: CoroutineContext
+ /**
+ * The [CoroutineContext] with which effects for the composition will be executed in.
+ **/
+ abstract val effectCoroutineContext: CoroutineContext
internal abstract val recomposeCoroutineContext: CoroutineContext
internal abstract fun composeInitial(
composition: ControlledComposition,
@@ -52,7 +56,8 @@
internal abstract fun registerComposition(composition: ControlledComposition)
internal abstract fun unregisterComposition(composition: ControlledComposition)
- internal open fun getCompositionLocalScope(): CompositionLocalMap = EmptyCompositionLocalMap
+ internal open fun getCompositionLocalScope(): PersistentCompositionLocalMap =
+ EmptyPersistentCompositionLocalMap
internal open fun startComposing() {}
internal open fun doneComposing() {}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
index 1031f82..12acbc7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
@@ -206,7 +206,7 @@
*/
@Stable
class CompositionLocalContext internal constructor(
- internal val compositionLocals: CompositionLocalMap
+ internal val compositionLocals: PersistentCompositionLocalMap
)
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt
new file mode 100644
index 0000000..8b7c958
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap
+import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
+
+/**
+ * A read-only, immutable snapshot of the [CompositionLocals][CompositionLocal] that are set at a
+ * specific position in the composition hierarchy.
+ */
+sealed interface CompositionLocalMap {
+ /**
+ * Returns the value of the provided [composition local][key] at the position in the composition
+ * hierarchy represented by this [CompositionLocalMap] instance. If the provided [key]
+ * is not set at this point in the hierarchy, its default value will be used.
+ *
+ * For [non-static CompositionLocals][compositionLocalOf], this function will return the latest
+ * value of the CompositionLocal, which might change over time across the same instance of the
+ * CompositionLocalMap. Reads done in this way are not tracked in the snapshot system.
+ *
+ * For [static CompositionLocals][staticCompositionLocalOf], this function returns the value
+ * at the time of creation of the CompositionLocalMap. When a static CompositionLocal is
+ * reassigned, the entire composition hierarchy is recomposed and a new CompositionLocalMap is
+ * created with the updated value of the static CompositionLocal.
+ */
+ operator fun <T> get(key: CompositionLocal<T>): T
+
+ companion object {
+ /**
+ * An empty [CompositionLocalMap] instance which contains no keys or values.
+ */
+ val Empty: CompositionLocalMap = persistentCompositionLocalHashMapOf()
+ }
+}
+
+/**
+ * A [CompositionLocal] map is is an immutable map that maps [CompositionLocal] keys to a provider
+ * of their current value. It is used to represent the combined scope of all provided
+ * [CompositionLocal]s.
+ */
+internal interface PersistentCompositionLocalMap :
+ PersistentMap<CompositionLocal<Any?>, State<Any?>>,
+ CompositionLocalMap {
+
+ // Override the builder APIs so that we can create new PersistentMaps that retain the type
+ // information of PersistentCompositionLocalMap. If we use the built-in implementation, we'll
+ // get back a PersistentMap<CompositionLocal<Any?>, State<Any?>> instead of a
+ // PersistentCompositionLocalMap
+ override fun builder(): Builder
+
+ interface Builder : PersistentMap.Builder<CompositionLocal<Any?>, State<Any?>> {
+ override fun build(): PersistentCompositionLocalMap
+ }
+}
+
+internal inline fun PersistentCompositionLocalMap.mutate(
+ mutator: (MutableMap<CompositionLocal<Any?>, State<Any?>>) -> Unit
+): PersistentCompositionLocalMap = builder().apply(mutator).build()
+
+@Suppress("UNCHECKED_CAST")
+internal fun <T> PersistentCompositionLocalMap.contains(key: CompositionLocal<T>) =
+ this.containsKey(key as CompositionLocal<Any?>)
+
+@Suppress("UNCHECKED_CAST")
+internal fun <T> PersistentCompositionLocalMap.getValueOf(key: CompositionLocal<T>) =
+ this[key as CompositionLocal<Any?>]?.value as T
+
+internal fun <T> PersistentCompositionLocalMap.read(
+ key: CompositionLocal<T>
+): T = if (contains(key)) {
+ getValueOf(key)
+} else {
+ key.defaultValueHolder.value
+}
+
+@Composable
+internal fun compositionLocalMapOf(
+ values: Array<out ProvidedValue<*>>,
+ parentScope: PersistentCompositionLocalMap
+): PersistentCompositionLocalMap {
+ val result: PersistentCompositionLocalMap = persistentCompositionLocalHashMapOf()
+ return result.mutate {
+ for (provided in values) {
+ if (provided.canOverride || !parentScope.contains(provided.compositionLocal)) {
+ @Suppress("UNCHECKED_CAST")
+ it[provided.compositionLocal as CompositionLocal<Any?>] =
+ provided.compositionLocal.provided(provided.value)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 62189bc..b9d3905 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -267,7 +267,7 @@
/**
* The [effectCoroutineContext] is derived from the parameter of the same name.
*/
- internal override val effectCoroutineContext: CoroutineContext =
+ override val effectCoroutineContext: CoroutineContext =
effectCoroutineContext + broadcastFrameClock + effectJob
internal override val recomposeCoroutineContext: CoroutineContext
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
index 690bb1f..b730a1f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
@@ -10,7 +10,6 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkPositionIndex
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
-import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.modCount
internal class PersistentVectorBuilder<E>(private var vector: PersistentList<E>,
private var vectorRoot: Array<Any?>?,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap.kt
index 2db51de..85ac958 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMap.kt
@@ -10,7 +10,7 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap
import androidx.compose.runtime.external.kotlinx.collections.immutable.mutate
-internal class PersistentHashMap<K, V>(internal val node: TrieNode<K, V>,
+internal open class PersistentHashMap<K, V>(internal val node: TrieNode<K, V>,
override val size: Int): AbstractMap<K, V>(), PersistentMap<K, V> {
override val keys: ImmutableSet<K>
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder.kt
index bce5a73..839c9d4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/PersistentHashMapBuilder.kt
@@ -9,9 +9,9 @@
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.DeltaCounter
import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
-internal class PersistentHashMapBuilder<K, V>(private var map: PersistentHashMap<K, V>) : PersistentMap.Builder<K, V>, AbstractMutableMap<K, V>() {
- internal var ownership = MutabilityOwnership()
- private set
+internal open class PersistentHashMapBuilder<K, V>(private var map: PersistentHashMap<K, V>) : PersistentMap.Builder<K, V>, AbstractMutableMap<K, V>() {
+ var ownership = MutabilityOwnership()
+ protected set
internal var node = map.node
internal var operationResult: V? = null
internal var modCount = 0
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt
new file mode 100644
index 0000000..f3fb67c
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.internal
+
+import androidx.compose.runtime.CompositionLocal
+import androidx.compose.runtime.PersistentCompositionLocalMap
+import androidx.compose.runtime.State
+import androidx.compose.runtime.external.kotlinx.collections.immutable.ImmutableSet
+import androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
+import androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMapBuilder
+import androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.TrieNode
+import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
+import androidx.compose.runtime.mutate
+import androidx.compose.runtime.read
+
+internal class PersistentCompositionLocalHashMap(
+ node: TrieNode<CompositionLocal<Any?>, State<Any?>>,
+ size: Int
+) : PersistentHashMap<CompositionLocal<Any?>, State<Any?>>(node, size),
+ PersistentCompositionLocalMap {
+
+ override val entries: ImmutableSet<Map.Entry<CompositionLocal<Any?>, State<Any?>>>
+ get() = super.entries
+
+ override fun <T> get(key: CompositionLocal<T>): T = read(key)
+
+ override fun builder(): Builder {
+ return Builder(this)
+ }
+
+ class Builder(
+ internal var map: PersistentCompositionLocalHashMap
+ ) : PersistentHashMapBuilder<CompositionLocal<Any?>, State<Any?>>(map),
+ PersistentCompositionLocalMap.Builder {
+ override fun build(): PersistentCompositionLocalHashMap {
+ map = if (node === map.node) {
+ map
+ } else {
+ ownership = MutabilityOwnership()
+ PersistentCompositionLocalHashMap(node, size)
+ }
+ return map
+ }
+ }
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ val Empty = PersistentCompositionLocalHashMap(
+ node = TrieNode.EMPTY as TrieNode<CompositionLocal<Any?>, State<Any?>>,
+ size = 0
+ )
+ }
+}
+
+internal fun persistentCompositionLocalHashMapOf() = PersistentCompositionLocalHashMap.Empty
+
+internal fun persistentCompositionLocalHashMapOf(
+ vararg pairs: Pair<CompositionLocal<Any?>, State<Any?>>
+): PersistentCompositionLocalMap = PersistentCompositionLocalHashMap.Empty.mutate { it += pairs }
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
new file mode 100644
index 0000000..9a3a49a
--- /dev/null
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.lint
+
+import androidx.compose.lint.Names
+import androidx.compose.lint.Package
+import androidx.compose.lint.PackageName
+import androidx.compose.lint.isInPackageName
+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.intellij.psi.PsiClass
+import com.intellij.psi.PsiMethod
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+
+@Suppress("UnstableApiUsage")
+class SuspiciousCompositionLocalModifierReadDetector : Detector(), SourceCodeScanner {
+
+ private val NodeLifecycleCallbacks = listOf("onAttach", "onDetach")
+
+ override fun getApplicableMethodNames(): List<String> =
+ listOf(Names.Ui.Node.CurrentValueOf.shortName)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!method.isInPackageName(Names.Ui.Node.PackageName)) return
+ reportIfAnyParentIsNodeLifecycleCallback(context, node, node)
+ }
+
+ private tailrec fun reportIfAnyParentIsNodeLifecycleCallback(
+ context: JavaContext,
+ node: UElement?,
+ usage: UCallExpression
+ ) {
+ if (node == null) {
+ return
+ } else if (node is UMethod) {
+ if (node.containingClass.isClConsumerNode()) {
+ if (node.name in NodeLifecycleCallbacks) {
+ report(context, usage) { localBeingRead ->
+ val action = node.name.removePrefix("on")
+ .replaceFirstChar { it.lowercase() }
+
+ "Reading $localBeingRead in ${node.name} will only access the " +
+ "CompositionLocal's value when the modifier is ${action}ed. " +
+ "To be notified of the latest value of the CompositionLocal, read " +
+ "the value in one of the modifier's other callbacks."
+ }
+ } else if (node.isConstructor) {
+ report(context, usage) {
+ "CompositionLocals cannot be read in modifiers before the node is attached."
+ }
+ }
+ }
+ return
+ } else if (node is KotlinUFunctionCallExpression && node.isLazyDelegate()) {
+ report(context, usage) { localBeingRead ->
+ "Reading $localBeingRead lazily will only access the CompositionLocal's value " +
+ "once. To be notified of the latest value of the CompositionLocal, read " +
+ "the value in one of the modifier's callbacks."
+ }
+ return
+ }
+
+ reportIfAnyParentIsNodeLifecycleCallback(context, node.uastParent, usage)
+ }
+
+ private inline fun report(
+ context: JavaContext,
+ usage: UCallExpression,
+ message: (compositionLocalName: String) -> String
+ ) {
+ val localBeingRead = usage.getArgumentForParameter(1)?.sourcePsi?.text
+ ?: "a composition local"
+
+ context.report(
+ SuspiciousCompositionLocalModifierRead,
+ context.getLocation(usage),
+ message(localBeingRead)
+ )
+ }
+
+ private fun PsiClass?.isClConsumerNode(): Boolean =
+ this?.implementsListTypes
+ ?.any { it.canonicalText == ClConsumerModifierNode } == true
+
+ private fun KotlinUFunctionCallExpression.isLazyDelegate(): Boolean =
+ resolve()?.run { isInPackageName(Package("kotlin")) && name == "lazy" } == true
+
+ companion object {
+ private const val ClConsumerModifierNode =
+ "androidx.compose.ui.node.CompositionLocalConsumerModifierNode"
+
+ val SuspiciousCompositionLocalModifierRead = Issue.create(
+ "SuspiciousCompositionLocalModifierRead",
+ "CompositionLocals should not be read in Modifier.onAttach() or Modifier.onDetach()",
+ "Jetpack Compose is unable to send updated values of a CompositionLocal when it's " +
+ "read in a Modifier.Node's initializer and onAttach() or onDetach() callbacks. " +
+ "Modifier.Node's callbacks are not aware of snapshot reads, and their lifecycle " +
+ "callbacks are not invoked on every recomposition. If you read a " +
+ "CompositionLocal in onAttach() or onDetach(), you will only get the " +
+ "CompositionLocal's value once at the moment of the read, which may lead to " +
+ "unexpected behaviors. We recommend instead accessing CompositionLocals in the " +
+ "main phase of your Modifier, like measure(), draw(), semanticsConfiguration, " +
+ "onKeyEvent(), etc. Accesses to CompositionLocals in any of these main phase " +
+ "events will be kept informed ",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ SuspiciousCompositionLocalModifierReadDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
index d296f22..c5b31bc 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
@@ -38,6 +38,7 @@
ModifierNodeInspectablePropertiesDetector.ModifierNodeInspectableProperties,
ModifierParameterDetector.ModifierParameter,
ReturnFromAwaitPointerEventScopeDetector.ExitAwaitPointerEventScope,
+ SuspiciousCompositionLocalModifierReadDetector.SuspiciousCompositionLocalModifierRead,
MultipleAwaitPointerEventScopesDetector.MultipleAwaitPointerEventScopes
)
override val vendor = Vendor(
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetectorTest.kt
new file mode 100644
index 0000000..8a8bc0d
--- /dev/null
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetectorTest.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.lint
+
+import androidx.compose.ui.lint.SuspiciousCompositionLocalModifierReadDetector.Companion.SuspiciousCompositionLocalModifierRead
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class SuspiciousCompositionLocalModifierReadDetectorTest : LintDetectorTest() {
+
+ override fun getDetector(): Detector = SuspiciousCompositionLocalModifierReadDetector()
+
+ override fun getIssues(): MutableList<Issue> = mutableListOf(
+ SuspiciousCompositionLocalModifierRead
+ )
+
+ private val CompositionLocalConsumerModifierStub = kotlin(
+ """
+ package androidx.compose.ui.node
+
+ import androidx.compose.runtime.CompositionLocal
+ import java.lang.RuntimeException
+
+ interface CompositionLocalConsumerModifierNode
+
+ fun <T> CompositionLocalConsumerModifierNode.currentValueOf(
+ local: CompositionLocal<T>
+ ): T {
+ throw RuntimeException("Not implemented in lint stubs.")
+ }
+ """
+ )
+
+ private val ModifierNodeStub = kotlin(
+ """
+ package androidx.compose.ui
+
+ interface Modifier {
+ class Node {
+ open fun onAttach() {}
+ open fun onDestroy() {}
+ }
+ }
+ """
+ )
+
+ private val CompositionLocalStub = kotlin(
+ """
+ package androidx.compose.runtime
+
+ import java.lang.RuntimeException
+
+ class CompositionLocal<T>(defaultFactory: () -> T)
+
+ class ProvidedValue<T> internal constructor(
+ val compositionLocal: CompositionLocal<T>,
+ val value: T,
+ val canOverride: Boolean
+ )
+
+ fun <T> compositionLocalOf(defaultFactory: () -> T): CompositionLocal<T> =
+ throw RuntimeException("Not implemented in lint stubs.")
+
+ fun <T> staticCompositionLocalOf(defaultFactory: () -> T): CompositionLocal<T> =
+ throw RuntimeException("Not implemented in lint stubs.")
+ """
+ )
+
+ @Test
+ fun testCompositionLocalReadInModifierAttachAndDetach() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+ import androidx.compose.ui.node.currentValueOf
+ import androidx.compose.runtime.CompositionLocal
+ import androidx.compose.runtime.compositionLocalOf
+ import androidx.compose.runtime.staticCompositionLocalOf
+
+ val staticLocalInt = staticCompositionLocalOf { 0 }
+ val localInt = compositionLocalOf { 0 }
+
+ class NodeUnderTest : Modifier.Node(), CompositionLocalConsumerModifierNode {
+ override fun onAttach() {
+ val readValue = currentValueOf(localInt)
+ }
+
+ override fun onDetach() {
+ val readValue = currentValueOf(staticLocalInt)
+ }
+ }
+ """
+ ),
+ CompositionLocalStub,
+ CompositionLocalConsumerModifierStub,
+ ModifierNodeStub
+ )
+ .run()
+ .expect(
+ """
+src/test/NodeUnderTest.kt:16: Error: Reading localInt in onAttach will only access the CompositionLocal's value when the modifier is attached. To be notified of the latest value of the CompositionLocal, read the value in one of the modifier's other callbacks. [SuspiciousCompositionLocalModifierRead]
+ val readValue = currentValueOf(localInt)
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+src/test/NodeUnderTest.kt:20: Error: Reading staticLocalInt in onDetach will only access the CompositionLocal's value when the modifier is detached. To be notified of the latest value of the CompositionLocal, read the value in one of the modifier's other callbacks. [SuspiciousCompositionLocalModifierRead]
+ val readValue = currentValueOf(staticLocalInt)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testCompositionLocalReadInModifierInitializer() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+ import androidx.compose.ui.node.currentValueOf
+ import androidx.compose.runtime.CompositionLocal
+ import androidx.compose.runtime.compositionLocalOf
+ import androidx.compose.runtime.staticCompositionLocalOf
+
+ val staticLocalInt = staticCompositionLocalOf { 0 }
+ val localInt = compositionLocalOf { 0 }
+
+ class NodeUnderTest : Modifier.Node(), CompositionLocalConsumerModifierNode {
+ init {
+ val readValue = currentValueOf(localInt)
+ val readValue = currentValueOf(staticLocalInt)
+ }
+ }
+ """
+ ),
+ CompositionLocalStub,
+ CompositionLocalConsumerModifierStub,
+ ModifierNodeStub
+ )
+ .run()
+ .expect(
+ """
+src/test/NodeUnderTest.kt:16: Error: CompositionLocals cannot be read in modifiers before the node is attached. [SuspiciousCompositionLocalModifierRead]
+ val readValue = currentValueOf(localInt)
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+src/test/NodeUnderTest.kt:17: Error: CompositionLocals cannot be read in modifiers before the node is attached. [SuspiciousCompositionLocalModifierRead]
+ val readValue = currentValueOf(staticLocalInt)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testCompositionLocalReadInModifierComputedProperty() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+ import androidx.compose.ui.node.currentValueOf
+ import androidx.compose.runtime.CompositionLocal
+ import androidx.compose.runtime.compositionLocalOf
+ import androidx.compose.runtime.staticCompositionLocalOf
+
+ val staticLocalInt = staticCompositionLocalOf { 0 }
+ val localInt = compositionLocalOf { 0 }
+
+ class NodeUnderTest : Modifier.Node(), CompositionLocalConsumerModifierNode {
+ val readValue: Int get() = currentValueOf(localInt)
+ val readValue: Int get() = currentValueOf(staticLocalInt)
+ }
+ """
+ ),
+ CompositionLocalStub,
+ CompositionLocalConsumerModifierStub,
+ ModifierNodeStub
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testCompositionLocalReadInLazyPropertyDelegate() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+ import androidx.compose.ui.node.currentValueOf
+ import androidx.compose.runtime.CompositionLocal
+ import androidx.compose.runtime.compositionLocalOf
+ import androidx.compose.runtime.staticCompositionLocalOf
+
+ val staticLocalInt = staticCompositionLocalOf { 0 }
+ val localInt = compositionLocalOf { 0 }
+
+ class NodeUnderTest : Modifier.Node(), CompositionLocalConsumerModifierNode {
+ val readValue by lazy { currentValueOf(localInt) }
+ val staticReadValue by lazy { currentValueOf(staticLocalInt) }
+ }
+ """
+ ),
+ CompositionLocalStub,
+ CompositionLocalConsumerModifierStub,
+ ModifierNodeStub
+ )
+ .run()
+ .expect(
+ """
+src/test/NodeUnderTest.kt:15: Error: Reading localInt lazily will only access the CompositionLocal's value once. To be notified of the latest value of the CompositionLocal, read the value in one of the modifier's callbacks. [SuspiciousCompositionLocalModifierRead]
+ val readValue by lazy { currentValueOf(localInt) }
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+src/test/NodeUnderTest.kt:16: Error: Reading staticLocalInt lazily will only access the CompositionLocal's value once. To be notified of the latest value of the CompositionLocal, read the value in one of the modifier's callbacks. [SuspiciousCompositionLocalModifierRead]
+ val staticReadValue by lazy { currentValueOf(staticLocalInt) }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun testCompositionLocalReadInArbitraryFunction() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.ui.Modifier
+ import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+ import androidx.compose.ui.node.currentValueOf
+ import androidx.compose.runtime.CompositionLocal
+ import androidx.compose.runtime.compositionLocalOf
+ import androidx.compose.runtime.staticCompositionLocalOf
+
+ val staticLocalInt = staticCompositionLocalOf { 0 }
+ val localInt = compositionLocalOf { 0 }
+
+ class NodeUnderTest : Modifier.Node(), CompositionLocalConsumerModifierNode {
+ fun onDoSomethingElse() {
+ val readValue = currentValueOf(localInt)
+ val readStaticValue = currentValueOf(staticLocalInt)
+ }
+ }
+ """
+ ),
+ CompositionLocalStub,
+ CompositionLocalConsumerModifierStub,
+ ModifierNodeStub
+ )
+ .run()
+ .expectClean()
+ }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/junit4/TextInputServiceForTests.kt b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/junit4/TextInputServiceForTests.kt
index b029929..143dcda 100644
--- a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/junit4/TextInputServiceForTests.kt
+++ b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/junit4/TextInputServiceForTests.kt
@@ -75,17 +75,6 @@
performEditCommand(listOf(CommitTextCommand(text, 1)))
}
- override fun submitTextForTest() {
- with(requireSession()) {
- if (imeOptions.imeAction == ImeAction.Default) {
- throw AssertionError(
- "Failed to perform IME action as current node does not specify any."
- )
- }
- onImeActionPerformed(imeOptions.imeAction)
- }
- }
-
private fun performEditCommand(commands: List<EditCommand>) {
requireSession().onEditCommand(commands)
}
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index ae53863..ce009b4 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -79,6 +79,7 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
+ method public static androidx.compose.ui.test.SemanticsMatcher hasPerformImeAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollToIndexAction();
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index c2c1e14..14f4cb4 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -85,6 +85,7 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
+ method public static androidx.compose.ui.test.SemanticsMatcher hasPerformImeAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollToIndexAction();
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 8264e10..659da21 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -79,6 +79,7 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
+ method public static androidx.compose.ui.test.SemanticsMatcher hasPerformImeAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasProgressBarRangeInfo(androidx.compose.ui.semantics.ProgressBarRangeInfo rangeInfo);
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasScrollToIndexAction();
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
index 7480feb..0e295d8 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TextActionsTest.kt
@@ -25,6 +25,9 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.performImeAction
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.util.BoundaryNode
import androidx.compose.ui.test.util.expectErrorMessageStartsWith
@@ -159,7 +162,7 @@
}
@Test
- fun sendImeAction_search() {
+ fun performImeAction_search() {
var actionPerformed = false
rule.setContent {
TextFieldUi(
@@ -178,7 +181,7 @@
}
@Test
- fun sendImeAction_actionNotDefined_shouldFail() {
+ fun performImeAction_actionNotDefined_shouldFail() {
var actionPerformed = false
rule.setContent {
TextFieldUi(
@@ -189,8 +192,8 @@
assertThat(actionPerformed).isFalse()
expectErrorMessageStartsWith(
- "" +
- "Failed to perform IME action as current node does not specify any.\n" +
+ "Failed to perform IME action.\n" +
+ "Failed to assert the following: (NOT (ImeAction = 'Default'))\n" +
"Semantics of the node:"
) {
rule.onNodeWithTag(fieldTag)
@@ -199,15 +202,32 @@
}
@Test
- fun sendImeAction_inputNotSupported_shouldFail() {
+ fun performImeAction_actionReturnsFalse_shouldFail() {
+ rule.setContent {
+ BoundaryNode(testTag = "node", Modifier.semantics {
+ setText { true }
+ performImeAction { false }
+ })
+ }
+
+ expectErrorMessageStartsWith(
+ "Failed to perform IME action, handler returned false.\n" +
+ "Semantics of the node:"
+ ) {
+ rule.onNodeWithTag("node")
+ .performImeAction()
+ }
+ }
+
+ @Test
+ fun performImeAction_inputNotSupported_shouldFail() {
rule.setContent {
BoundaryNode(testTag = "node")
}
expectErrorMessageStartsWith(
- "" +
- "Failed to perform IME action.\n" +
- "Failed to assert the following: (SetText is defined)\n" +
+ "Failed to perform IME action.\n" +
+ "Failed to assert the following: (PerformImeAction is defined)\n" +
"Semantics of the node:"
) {
rule.onNodeWithTag("node")
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
index 0c42170..1b7f84e 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/BoundaryNodes.kt
@@ -31,7 +31,8 @@
@Composable
fun BoundaryNode(
- testTag: String
+ testTag: String,
+ modifier: Modifier = Modifier
) {
- Column(Modifier.testTag(testTag)) {}
+ Column(modifier.testTag(testTag)) {}
}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
index ac8d819..ca4d4ea 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
@@ -374,9 +374,9 @@
SemanticsMatcher.expectValue(SemanticsProperties.ImeAction, actionType)
/**
- * Returns whether the node defines semantics action to set text to it.
+ * Returns whether the node defines a semantics action to set text on it.
*
- * This can be used to for instance filter out text fields.
+ * This can be used to, for instance, filter out text fields.
*
* @see SemanticsActions.SetText
*/
@@ -384,6 +384,14 @@
hasKey(SemanticsActions.SetText)
/**
+ * Returns whether the node defines a semantics action to perform the
+ * [IME action][SemanticsProperties.ImeAction] on it.
+ *
+ * @see SemanticsActions.PerformImeAction
+ */
+fun hasPerformImeAction() = hasKey(SemanticsActions.PerformImeAction)
+
+/**
* Returns whether the node defines the ability to scroll to an item index.
*
* Note that not all scrollable containers have item indices. For example, a
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
index 7a94d9f..79a1cd8 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
@@ -17,10 +17,15 @@
package androidx.compose.ui.test
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsActions.PerformImeAction
import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.performImeAction
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextInputForTests
/**
@@ -68,22 +73,31 @@
}
/**
- * Sends to this node the IME action associated with it in similar way to IME.
+ * Sends to this node the IME action associated with it in a similar way to the IME.
*
- * The node needs to define its IME action in semantics.
+ * The node needs to define its IME action in semantics via
+ * [SemanticsPropertyReceiver.performImeAction].
*
* @throws AssertionError if the node does not support input or does not define IME action.
- * @throws IllegalStateException if tne node did not establish input connection (e.g. is not
- * focused)
+ * @throws IllegalStateException if the node did is not an editor or would not be able to establish
+ * an input connection (e.g. does not define [ImeAction][SemanticsProperties.ImeAction] or
+ * [PerformImeAction] or is not focused).
*/
-// TODO(b/269633506) Use SemanticsAction for this when available.
-@OptIn(ExperimentalTextApi::class)
fun SemanticsNodeInteraction.performImeAction() {
- val node = getNodeAndFocus("Failed to perform IME action.")
+ val errorOnFail = "Failed to perform IME action."
+ assert(hasPerformImeAction()) { errorOnFail }
+ assert(!hasImeAction(ImeAction.Default)) { errorOnFail }
+ val node = getNodeAndFocus(errorOnFail)
+
wrapAssertionErrorsWithNodeInfo(selector, node) {
- @OptIn(InternalTestApi::class)
- testContext.testOwner.performTextInput(node) {
- submitTextForTest()
+ performSemanticsAction(PerformImeAction) {
+ assert(it()) {
+ buildGeneralErrorMessage(
+ "Failed to perform IME action, handler returned false.",
+ selector,
+ node
+ )
+ }
}
}
}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 9a92eb6..422f06f 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1006,6 +1006,26 @@
property public final char mask;
}
+ public sealed interface PlatformTextInput {
+ method public void releaseInputFocus();
+ method public void requestInputFocus();
+ }
+
+ public interface PlatformTextInputAdapter {
+ method public android.view.inputmethod.InputConnection? createInputConnection(android.view.inputmethod.EditorInfo outAttrs);
+ method public androidx.compose.ui.text.input.TextInputForTests? getInputForTests();
+ method public default void onDisposed();
+ property public abstract androidx.compose.ui.text.input.TextInputForTests? inputForTests;
+ }
+
+ @androidx.compose.runtime.Immutable public fun interface PlatformTextInputPlugin<T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> {
+ method public T createAdapter(androidx.compose.ui.text.input.PlatformTextInput platformTextInput, android.view.View view);
+ }
+
+ @androidx.compose.runtime.Stable public sealed interface PlatformTextInputPluginRegistry {
+ method @androidx.compose.runtime.Composable public <T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> T rememberAdapter(androidx.compose.ui.text.input.PlatformTextInputPlugin<T> plugin);
+ }
+
public interface PlatformTextInputService {
method public void hideSoftwareKeyboard();
method public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
@@ -1072,6 +1092,10 @@
method public static androidx.compose.ui.text.AnnotatedString getTextBeforeSelection(androidx.compose.ui.text.input.TextFieldValue, int maxChars);
}
+ public interface TextInputForTests {
+ method public void inputTextForTest(String text);
+ }
+
public class TextInputService {
ctor public TextInputService(androidx.compose.ui.text.input.PlatformTextInputService platformTextInputService);
method @Deprecated public final void hideSoftwareKeyboard();
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index f4e3580..54a842a 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -1083,23 +1083,23 @@
property public final char mask;
}
- @androidx.compose.ui.text.ExperimentalTextApi public sealed interface PlatformTextInput {
+ public sealed interface PlatformTextInput {
method public void releaseInputFocus();
method public void requestInputFocus();
}
- @androidx.compose.ui.text.ExperimentalTextApi public interface PlatformTextInputAdapter {
+ public interface PlatformTextInputAdapter {
method public android.view.inputmethod.InputConnection? createInputConnection(android.view.inputmethod.EditorInfo outAttrs);
method public androidx.compose.ui.text.input.TextInputForTests? getInputForTests();
method public default void onDisposed();
property public abstract androidx.compose.ui.text.input.TextInputForTests? inputForTests;
}
- @androidx.compose.runtime.Immutable @androidx.compose.ui.text.ExperimentalTextApi public fun interface PlatformTextInputPlugin<T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> {
+ @androidx.compose.runtime.Immutable public fun interface PlatformTextInputPlugin<T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> {
method public T createAdapter(androidx.compose.ui.text.input.PlatformTextInput platformTextInput, android.view.View view);
}
- @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public sealed interface PlatformTextInputPluginRegistry {
+ @androidx.compose.runtime.Stable public sealed interface PlatformTextInputPluginRegistry {
method @androidx.compose.runtime.Composable public <T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> T rememberAdapter(androidx.compose.ui.text.input.PlatformTextInputPlugin<T> plugin);
}
@@ -1184,9 +1184,8 @@
method public static androidx.compose.ui.text.AnnotatedString getTextBeforeSelection(androidx.compose.ui.text.input.TextFieldValue, int maxChars);
}
- @androidx.compose.ui.text.ExperimentalTextApi public interface TextInputForTests {
+ public interface TextInputForTests {
method public void inputTextForTest(String text);
- method @androidx.compose.ui.text.ExperimentalTextApi public void submitTextForTest();
}
public class TextInputService {
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 9a92eb6..422f06f 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1006,6 +1006,26 @@
property public final char mask;
}
+ public sealed interface PlatformTextInput {
+ method public void releaseInputFocus();
+ method public void requestInputFocus();
+ }
+
+ public interface PlatformTextInputAdapter {
+ method public android.view.inputmethod.InputConnection? createInputConnection(android.view.inputmethod.EditorInfo outAttrs);
+ method public androidx.compose.ui.text.input.TextInputForTests? getInputForTests();
+ method public default void onDisposed();
+ property public abstract androidx.compose.ui.text.input.TextInputForTests? inputForTests;
+ }
+
+ @androidx.compose.runtime.Immutable public fun interface PlatformTextInputPlugin<T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> {
+ method public T createAdapter(androidx.compose.ui.text.input.PlatformTextInput platformTextInput, android.view.View view);
+ }
+
+ @androidx.compose.runtime.Stable public sealed interface PlatformTextInputPluginRegistry {
+ method @androidx.compose.runtime.Composable public <T extends androidx.compose.ui.text.input.PlatformTextInputAdapter> T rememberAdapter(androidx.compose.ui.text.input.PlatformTextInputPlugin<T> plugin);
+ }
+
public interface PlatformTextInputService {
method public void hideSoftwareKeyboard();
method public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
@@ -1072,6 +1092,10 @@
method public static androidx.compose.ui.text.AnnotatedString getTextBeforeSelection(androidx.compose.ui.text.input.TextFieldValue, int maxChars);
}
+ public interface TextInputForTests {
+ method public void inputTextForTest(String text);
+ }
+
public class TextInputService {
ctor public TextInputService(androidx.compose.ui.text.input.PlatformTextInputService platformTextInputService);
method @Deprecated public final void hideSoftwareKeyboard();
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.android.kt
index 118fedc..395de4c 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.android.kt
@@ -20,7 +20,6 @@
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.compose.runtime.Immutable
-import androidx.compose.ui.text.ExperimentalTextApi
/**
* Defines a plugin to the Compose text input system. Instances of this interface should be
@@ -36,7 +35,6 @@
* Implementations are intended to be used only by your text editor implementation, and probably not
* exposed as public API.
*/
-@ExperimentalTextApi
@Immutable
actual fun interface PlatformTextInputPlugin<T : PlatformTextInputAdapter> {
/**
@@ -72,7 +70,6 @@
* exposed as public API. Your adapter can define whatever internal API it needs to communicate with
* the rest of your text editor code.
*/
-@ExperimentalTextApi
actual interface PlatformTextInputAdapter {
// TODO(b/267235947) When fleshing out the desktop actual, we might want to pull some of these
// members up into the expect interface (e.g. maybe inputForTests).
@@ -93,7 +90,6 @@
fun onDisposed() {}
}
-@OptIn(ExperimentalTextApi::class)
internal actual fun PlatformTextInputAdapter.dispose() {
onDisposed()
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/ImeAction.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/ImeAction.kt
index 5583635..1ef3163 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/ImeAction.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/ImeAction.kt
@@ -19,7 +19,7 @@
import androidx.compose.runtime.Stable
/**
- * Signals the keyboard what type of action should be displayed. It is not guaranteed if
+ * Signals the keyboard what type of action should be displayed. It is not guaranteed that
* the keyboard will show the requested action.
*/
@kotlin.jvm.JvmInline
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.kt
index c3f5c92..cf5d4be 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapter.kt
@@ -35,18 +35,21 @@
private const val DEBUG = false
/** See kdoc on actual interfaces. */
-@ExperimentalTextApi
+// Experimental in desktop.
+@OptIn(ExperimentalTextApi::class)
@Immutable
expect interface PlatformTextInputPlugin<T : PlatformTextInputAdapter>
/** See kdoc on actual interfaces. */
-@ExperimentalTextApi
+// Experimental in desktop.
+@OptIn(ExperimentalTextApi::class)
expect interface PlatformTextInputAdapter
/**
* Calls the [PlatformTextInputAdapter]'s onDisposed method. This is done through a proxy method
* because expect interfaces aren't allowed to have default implementations.
*/
+// Experimental in desktop.
@OptIn(ExperimentalTextApi::class)
internal expect fun PlatformTextInputAdapter.dispose()
@@ -55,7 +58,6 @@
* methods that allow adapters to interact with it. Instances are passed to
* [PlatformTextInputPlugin.createAdapter].
*/
-@ExperimentalTextApi
sealed interface PlatformTextInput {
/**
* Requests that the platform input be connected to this receiver until either:
@@ -88,7 +90,6 @@
*/
// Implementation note: this is separated as a sealed interface + impl pair to avoid exposing
// @InternalTextApi members to code reading LocalPlatformTextInputAdapterProvider.
-@ExperimentalTextApi
@Stable
sealed interface PlatformTextInputPluginRegistry {
/**
@@ -103,6 +104,8 @@
* @param T The type of [PlatformTextInputAdapter] that [plugin] creates.
* @param plugin The factory for adapters and the key into the cache of those adapters.
*/
+ // Experimental in desktop.
+ @OptIn(ExperimentalTextApi::class)
@Composable
fun <T : PlatformTextInputAdapter> rememberAdapter(plugin: PlatformTextInputPlugin<T>): T
}
@@ -116,6 +119,7 @@
*/
// This doesn't violate the EndsWithImpl rule because it's not public API.
@Suppress("EndsWithImpl")
+// Experimental in desktop.
@OptIn(ExperimentalTextApi::class)
@InternalTextApi
class PlatformTextInputPluginRegistryImpl(
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputForTests.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputForTests.kt
index 955d161..eccbfc7 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputForTests.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputForTests.kt
@@ -16,8 +16,6 @@
package androidx.compose.ui.text.input
-import androidx.compose.ui.text.ExperimentalTextApi
-
/**
* Defines additional operations that can be performed on text editors by UI tests that aren't
* available as semantics actions. Tests call these methods indirectly, by the various `perform*`
@@ -34,7 +32,6 @@
// be given default implementations that throw UnsupportedOperationExceptions. This is not a concern
// for backwards compatibility because it simply means that tests may not use new perform* methods
// on older implementations that haven't linked against the newer version of Compose.
-@ExperimentalTextApi
interface TextInputForTests {
/**
@@ -44,13 +41,4 @@
* @param text Text to send.
*/
fun inputTextForTest(text: String)
-
- /**
- * Performs the submit action configured on the current node, if any.
- *
- * On Android, this is the IME action.
- */
- // TODO(b/269633168, b/269633506) Remove and implement using semantics instead.
- @ExperimentalTextApi
- fun submitTextForTest()
}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index d279a77..b791bd7 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -139,6 +139,20 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ public abstract static class Modifier.Node implements androidx.compose.ui.node.DelegatableNode {
+ ctor public Modifier.Node();
+ method public final kotlinx.coroutines.CoroutineScope getCoroutineScope();
+ method public final androidx.compose.ui.Modifier.Node getNode();
+ method public final boolean isAttached();
+ method public void onAttach();
+ method public void onDetach();
+ method public void onReset();
+ method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
+ property public final kotlinx.coroutines.CoroutineScope coroutineScope;
+ property public final boolean isAttached;
+ property public final androidx.compose.ui.Modifier.Node node;
+ }
+
@androidx.compose.runtime.Stable public interface MotionDurationScale extends kotlin.coroutines.CoroutineContext.Element {
method public default kotlin.coroutines.CoroutineContext.Key<?> getKey();
method public float getScaleFactor();
@@ -281,6 +295,10 @@
method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
}
+ public interface FocusEventModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
method public void clearFocus(optional boolean force);
method public boolean moveFocus(int focusDirection);
@@ -363,6 +381,10 @@
method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
}
+ public interface FocusPropertiesModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void modifyFocusProperties(androidx.compose.ui.focus.FocusProperties focusProperties);
+ }
+
@androidx.compose.runtime.Stable public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
@@ -385,6 +407,15 @@
method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
+ public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ }
+
+ public final class FocusRequesterModifierNodeKt {
+ method public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ }
+
public interface FocusState {
method public boolean getHasFocus();
method public boolean isCaptured();
@@ -394,6 +425,13 @@
property public abstract boolean isFocused;
}
+ public final class FocusTargetModifierNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.modifier.ModifierLocalNode androidx.compose.ui.node.ObserverNode {
+ ctor public FocusTargetModifierNode();
+ method public androidx.compose.ui.focus.FocusState getFocusState();
+ method public void onObservedReadsChanged();
+ property public final androidx.compose.ui.focus.FocusState focusState;
+ }
+
}
package androidx.compose.ui.graphics {
@@ -1406,6 +1444,11 @@
method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
+ public interface KeyInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onKeyEvent(android.view.KeyEvent event);
+ method public boolean onPreKeyEvent(android.view.KeyEvent event);
+ }
+
public final class Key_androidKt {
method public static long Key(int nativeKeyCode);
method public static int getNativeKeyCode(long);
@@ -2151,6 +2194,24 @@
method public static <T> androidx.compose.ui.modifier.ProvidableModifierLocal<T> modifierLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
+ public abstract sealed class ModifierLocalMap {
+ }
+
+ public interface ModifierLocalNode extends androidx.compose.ui.modifier.ModifierLocalReadScope androidx.compose.ui.node.DelegatableNode {
+ method public default <T> T! getCurrent(androidx.compose.ui.modifier.ModifierLocal<T>);
+ method public default androidx.compose.ui.modifier.ModifierLocalMap getProvidedValues();
+ method public default <T> void provide(androidx.compose.ui.modifier.ModifierLocal<T> key, T? value);
+ property public default androidx.compose.ui.modifier.ModifierLocalMap providedValues;
+ }
+
+ public final class ModifierLocalNodeKt {
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf();
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<T> key);
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<T>,? extends T> entry);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<?>... keys);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<?>,?>... entries);
+ }
+
@androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface ModifierLocalProvider<T> extends androidx.compose.ui.Modifier.Element {
method public androidx.compose.ui.modifier.ProvidableModifierLocal<T> getKey();
method public T! getValue();
@@ -2170,6 +2231,101 @@
package androidx.compose.ui.node {
+ public interface DelegatableNode {
+ method public androidx.compose.ui.Modifier.Node getNode();
+ property public abstract androidx.compose.ui.Modifier.Node node;
+ }
+
+ public final class DelegatableNodeKt {
+ method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.LayoutDirection requireLayoutDirection(androidx.compose.ui.node.DelegatableNode);
+ }
+
+ public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
+ ctor public DelegatingNode();
+ method public final <T extends androidx.compose.ui.Modifier.Node> T delegated(kotlin.jvm.functions.Function0<? extends T> fn);
+ }
+
+ public interface DrawModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
+ method public default void onMeasureResultChanged();
+ }
+
+ public final class DrawModifierNodeKt {
+ method public static void invalidateDraw(androidx.compose.ui.node.DrawModifierNode);
+ }
+
+ public interface GlobalPositionAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
+ }
+
+ public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
+ method public long getTargetSize();
+ method public void setTargetSize(long);
+ property public abstract long targetSize;
+ }
+
+ public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
+ method public default void onRemeasured(long size);
+ }
+
+ public interface LayoutModifierNode extends androidx.compose.ui.layout.Remeasurement androidx.compose.ui.node.DelegatableNode {
+ method public default void forceRemeasure();
+ method public default int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public androidx.compose.ui.layout.MeasureResult measure(androidx.compose.ui.layout.MeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
+ method public default int minIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ }
+
+ public final class LayoutModifierNodeKt {
+ method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ }
+
+ public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
+ ctor public ModifierNodeElement();
+ method public abstract N create();
+ method public abstract boolean equals(Object? other);
+ method public boolean getAutoInvalidate();
+ method public final kotlin.sequences.Sequence<androidx.compose.ui.platform.ValueElement> getInspectableElements();
+ method public final String? getNameFallback();
+ method public final Object? getValueOverride();
+ method public abstract int hashCode();
+ method public void inspectableProperties(androidx.compose.ui.platform.InspectorInfo);
+ method public abstract N update(N node);
+ property public boolean autoInvalidate;
+ property public final kotlin.sequences.Sequence<androidx.compose.ui.platform.ValueElement> inspectableElements;
+ property public final String? nameFallback;
+ property public final Object? valueOverride;
+ }
+
+ public interface ObserverNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onObservedReadsChanged();
+ }
+
+ public final class ObserverNodeKt {
+ method public static <T extends androidx.compose.ui.Modifier.Node & androidx.compose.ui.node.ObserverNode> void observeReads(T, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ }
+
+ public interface ParentDataModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public Object? modifyParentData(androidx.compose.ui.unit.Density, Object? parentData);
+ }
+
+ public final class ParentDataModifierNodeKt {
+ method public static void invalidateParentData(androidx.compose.ui.node.ParentDataModifierNode);
+ }
+
+ public interface PointerInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public default boolean interceptOutOfBoundsChildEvents();
+ method public void onCancelPointerInput();
+ method public void onPointerEvent(androidx.compose.ui.input.pointer.PointerEvent pointerEvent, androidx.compose.ui.input.pointer.PointerEventPass pass, long bounds);
+ method public default boolean sharePointerInputWithSiblings();
+ }
+
public final class Ref<T> {
ctor public Ref();
method public T? getValue();
@@ -2187,6 +2343,16 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
}
+ 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;
+ }
+
+ public final class SemanticsModifierNodeKt {
+ method public static androidx.compose.ui.semantics.SemanticsConfiguration collapsedSemanticsConfiguration(androidx.compose.ui.node.SemanticsModifierNode);
+ method public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
+ }
+
}
package androidx.compose.ui.platform {
@@ -2295,6 +2461,7 @@
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> getLocalHapticFeedback();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> getLocalInputModeManager();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> getLocalLayoutDirection();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> getLocalPlatformTextInputPluginRegistry();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> getLocalTextInputService();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> getLocalTextToolbar();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> getLocalUriHandler();
@@ -2308,6 +2475,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> LocalHapticFeedback;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> LocalInputModeManager;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> LocalLayoutDirection;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> LocalPlatformTextInputPluginRegistry;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> LocalTextInputService;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> LocalTextToolbar;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> LocalUriHandler;
@@ -2654,6 +2822,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageRight();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageUp();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPasteText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPerformImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getRequestFocus();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> getScrollBy();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> getScrollToIndex();
@@ -2674,6 +2843,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageRight;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageUp;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PasteText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PerformImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> RequestFocus;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> ScrollBy;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> ScrollToIndex;
@@ -2859,6 +3029,7 @@
method public static void pageUp(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void password(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
+ method public static void performImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void requestFocus(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean>? action);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 3c8eb37..a73682d 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -149,14 +149,16 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public abstract static class Modifier.Node implements androidx.compose.ui.node.DelegatableNode {
+ public abstract static class Modifier.Node implements androidx.compose.ui.node.DelegatableNode {
ctor public Modifier.Node();
+ method public final kotlinx.coroutines.CoroutineScope getCoroutineScope();
method public final androidx.compose.ui.Modifier.Node getNode();
method public final boolean isAttached();
method public void onAttach();
method public void onDetach();
method public void onReset();
method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
+ property public final kotlinx.coroutines.CoroutineScope coroutineScope;
property public final boolean isAttached;
property public final androidx.compose.ui.Modifier.Node node;
}
@@ -382,7 +384,7 @@
method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusEventModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface FocusEventModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
}
@@ -474,7 +476,7 @@
method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusPropertiesModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface FocusPropertiesModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public void modifyFocusProperties(androidx.compose.ui.focus.FocusProperties focusProperties);
}
@@ -523,13 +525,13 @@
method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
}
public final class FocusRequesterModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
}
public interface FocusState {
@@ -541,7 +543,7 @@
property public abstract boolean isFocused;
}
- @androidx.compose.ui.ExperimentalComposeUiApi public final class FocusTargetModifierNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.modifier.ModifierLocalNode androidx.compose.ui.node.ObserverNode {
+ public final class FocusTargetModifierNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.modifier.ModifierLocalNode androidx.compose.ui.node.ObserverNode {
ctor public FocusTargetModifierNode();
method public androidx.compose.ui.focus.FocusState getFocusState();
method public void onObservedReadsChanged();
@@ -1561,7 +1563,7 @@
method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface KeyInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface KeyInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public boolean onKeyEvent(android.view.KeyEvent event);
method public boolean onPreKeyEvent(android.view.KeyEvent event);
}
@@ -2380,10 +2382,10 @@
method public static <T> androidx.compose.ui.modifier.ProvidableModifierLocal<T> modifierLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public abstract sealed class ModifierLocalMap {
+ public abstract sealed class ModifierLocalMap {
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface ModifierLocalNode extends androidx.compose.ui.modifier.ModifierLocalReadScope androidx.compose.ui.node.DelegatableNode {
+ public interface ModifierLocalNode extends androidx.compose.ui.modifier.ModifierLocalReadScope androidx.compose.ui.node.DelegatableNode {
method public default <T> T! getCurrent(androidx.compose.ui.modifier.ModifierLocal<T>);
method public default androidx.compose.ui.modifier.ModifierLocalMap getProvidedValues();
method public default <T> void provide(androidx.compose.ui.modifier.ModifierLocal<T> key, T? value);
@@ -2391,11 +2393,11 @@
}
public final class ModifierLocalNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf();
- method @androidx.compose.ui.ExperimentalComposeUiApi public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<T> key);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<T>,? extends T> entry);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<?>... keys);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<?>,?>... entries);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf();
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<T> key);
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<T>,? extends T> entry);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<?>... keys);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<?>,?>... entries);
}
@androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface ModifierLocalProvider<T> extends androidx.compose.ui.Modifier.Element {
@@ -2421,36 +2423,43 @@
package androidx.compose.ui.node {
- @androidx.compose.ui.ExperimentalComposeUiApi public interface DelegatableNode {
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface CompositionLocalConsumerModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ }
+
+ public final class CompositionLocalConsumerModifierNodeKt {
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static <T> T! currentValueOf(androidx.compose.ui.node.CompositionLocalConsumerModifierNode, androidx.compose.runtime.CompositionLocal<T> local);
+ }
+
+ public interface DelegatableNode {
method public androidx.compose.ui.Modifier.Node getNode();
property public abstract androidx.compose.ui.Modifier.Node node;
}
public final class DelegatableNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.unit.LayoutDirection requireLayoutDirection(androidx.compose.ui.node.DelegatableNode);
+ method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.LayoutDirection requireLayoutDirection(androidx.compose.ui.node.DelegatableNode);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
+ public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
ctor public DelegatingNode();
method public final <T extends androidx.compose.ui.Modifier.Node> T delegated(kotlin.jvm.functions.Function0<? extends T> fn);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface DrawModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface DrawModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
method public default void onMeasureResultChanged();
}
public final class DrawModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateDraw(androidx.compose.ui.node.DrawModifierNode);
+ method public static void invalidateDraw(androidx.compose.ui.node.DrawModifierNode);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface GlobalPositionAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface GlobalPositionAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
+ public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
method public long getTargetSize();
method public void setTargetSize(long);
property public abstract long targetSize;
@@ -2463,13 +2472,13 @@
method public android.view.View? getInteropView();
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
- method public default void onLookaheadPlaced(androidx.compose.ui.layout.LookaheadLayoutCoordinates coordinates);
+ public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method @androidx.compose.ui.ExperimentalComposeUiApi public default void onLookaheadPlaced(androidx.compose.ui.layout.LookaheadLayoutCoordinates coordinates);
method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
method public default void onRemeasured(long size);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface LayoutModifierNode extends androidx.compose.ui.layout.Remeasurement androidx.compose.ui.node.DelegatableNode {
+ public interface LayoutModifierNode extends androidx.compose.ui.layout.Remeasurement androidx.compose.ui.node.DelegatableNode {
method public default void forceRemeasure();
method public default int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
method public default int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
@@ -2479,12 +2488,12 @@
}
public final class LayoutModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
+ public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
ctor public ModifierNodeElement();
method public abstract N create();
method public abstract boolean equals(Object? other);
@@ -2501,23 +2510,23 @@
property public final Object? valueOverride;
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface ObserverNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface ObserverNode extends androidx.compose.ui.node.DelegatableNode {
method public void onObservedReadsChanged();
}
public final class ObserverNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static <T extends androidx.compose.ui.Modifier.Node & androidx.compose.ui.node.ObserverNode> void observeReads(T, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ method public static <T extends androidx.compose.ui.Modifier.Node & androidx.compose.ui.node.ObserverNode> void observeReads(T, kotlin.jvm.functions.Function0<kotlin.Unit> block);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface ParentDataModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface ParentDataModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public Object? modifyParentData(androidx.compose.ui.unit.Density, Object? parentData);
}
public final class ParentDataModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateParentData(androidx.compose.ui.node.ParentDataModifierNode);
+ method public static void invalidateParentData(androidx.compose.ui.node.ParentDataModifierNode);
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface PointerInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ public interface PointerInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
method public default boolean interceptOutOfBoundsChildEvents();
method public void onCancelPointerInput();
method public void onPointerEvent(androidx.compose.ui.input.pointer.PointerEvent pointerEvent, androidx.compose.ui.input.pointer.PointerEventPass pass, long bounds);
@@ -2544,14 +2553,14 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ 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;
}
public final class SemanticsModifierNodeKt {
- method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.semantics.SemanticsConfiguration collapsedSemanticsConfiguration(androidx.compose.ui.node.SemanticsModifierNode);
- method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
+ method public static androidx.compose.ui.semantics.SemanticsConfiguration collapsedSemanticsConfiguration(androidx.compose.ui.node.SemanticsModifierNode);
+ method public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
}
}
@@ -2664,7 +2673,7 @@
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> getLocalHapticFeedback();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> getLocalInputModeManager();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> getLocalLayoutDirection();
- method @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> getLocalPlatformTextInputPluginRegistry();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> getLocalPlatformTextInputPluginRegistry();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> getLocalTextInputService();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> getLocalTextToolbar();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> getLocalUriHandler();
@@ -2680,7 +2689,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> LocalHapticFeedback;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> LocalInputModeManager;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> LocalLayoutDirection;
- property @androidx.compose.ui.text.ExperimentalTextApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> LocalPlatformTextInputPluginRegistry;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> LocalPlatformTextInputPluginRegistry;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> LocalTextInputService;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> LocalTextToolbar;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> LocalUriHandler;
@@ -3060,6 +3069,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageRight();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageUp();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPasteText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPerformImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getRequestFocus();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> getScrollBy();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> getScrollToIndex();
@@ -3080,6 +3090,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageRight;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageUp;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PasteText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PerformImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> RequestFocus;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> ScrollBy;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> ScrollToIndex;
@@ -3272,6 +3283,7 @@
method public static void pageUp(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void password(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
+ method public static void performImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void requestFocus(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean>? action);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index bffa9e9..3e61d76 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -139,6 +139,20 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ public abstract static class Modifier.Node implements androidx.compose.ui.node.DelegatableNode {
+ ctor public Modifier.Node();
+ method public final kotlinx.coroutines.CoroutineScope getCoroutineScope();
+ method public final androidx.compose.ui.Modifier.Node getNode();
+ method public final boolean isAttached();
+ method public void onAttach();
+ method public void onDetach();
+ method public void onReset();
+ method public final void sideEffect(kotlin.jvm.functions.Function0<kotlin.Unit> effect);
+ property public final kotlinx.coroutines.CoroutineScope coroutineScope;
+ property public final boolean isAttached;
+ property public final androidx.compose.ui.Modifier.Node node;
+ }
+
@androidx.compose.runtime.Stable public interface MotionDurationScale extends kotlin.coroutines.CoroutineContext.Element {
method public default kotlin.coroutines.CoroutineContext.Key<?> getKey();
method public float getScaleFactor();
@@ -281,6 +295,10 @@
method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
}
+ public interface FocusEventModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
method public void clearFocus(optional boolean force);
method public boolean moveFocus(int focusDirection);
@@ -363,6 +381,10 @@
method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
}
+ public interface FocusPropertiesModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void modifyFocusProperties(androidx.compose.ui.focus.FocusProperties focusProperties);
+ }
+
@androidx.compose.runtime.Stable public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
@@ -385,6 +407,15 @@
method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
+ public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ }
+
+ public final class FocusRequesterModifierNodeKt {
+ method public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ }
+
public interface FocusState {
method public boolean getHasFocus();
method public boolean isCaptured();
@@ -394,6 +425,13 @@
property public abstract boolean isFocused;
}
+ public final class FocusTargetModifierNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.modifier.ModifierLocalNode androidx.compose.ui.node.ObserverNode {
+ ctor public FocusTargetModifierNode();
+ method public androidx.compose.ui.focus.FocusState getFocusState();
+ method public void onObservedReadsChanged();
+ property public final androidx.compose.ui.focus.FocusState focusState;
+ }
+
}
package androidx.compose.ui.graphics {
@@ -1406,6 +1444,11 @@
method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
+ public interface KeyInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onKeyEvent(android.view.KeyEvent event);
+ method public boolean onPreKeyEvent(android.view.KeyEvent event);
+ }
+
public final class Key_androidKt {
method public static long Key(int nativeKeyCode);
method public static int getNativeKeyCode(long);
@@ -2157,6 +2200,24 @@
method public static <T> androidx.compose.ui.modifier.ProvidableModifierLocal<T> modifierLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
+ public abstract sealed class ModifierLocalMap {
+ }
+
+ public interface ModifierLocalNode extends androidx.compose.ui.modifier.ModifierLocalReadScope androidx.compose.ui.node.DelegatableNode {
+ method public default <T> T! getCurrent(androidx.compose.ui.modifier.ModifierLocal<T>);
+ method public default androidx.compose.ui.modifier.ModifierLocalMap getProvidedValues();
+ method public default <T> void provide(androidx.compose.ui.modifier.ModifierLocal<T> key, T? value);
+ property public default androidx.compose.ui.modifier.ModifierLocalMap providedValues;
+ }
+
+ public final class ModifierLocalNodeKt {
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf();
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<T> key);
+ method public static <T> androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<T>,? extends T> entry);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(androidx.compose.ui.modifier.ModifierLocal<?>... keys);
+ method public static androidx.compose.ui.modifier.ModifierLocalMap modifierLocalMapOf(kotlin.Pair<? extends androidx.compose.ui.modifier.ModifierLocal<?>,?>... entries);
+ }
+
@androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface ModifierLocalProvider<T> extends androidx.compose.ui.Modifier.Element {
method public androidx.compose.ui.modifier.ProvidableModifierLocal<T> getKey();
method public T! getValue();
@@ -2177,16 +2238,19 @@
package androidx.compose.ui.node {
@kotlin.PublishedApi internal interface ComposeUiNode {
+ method public androidx.compose.runtime.CompositionLocalMap getCompositionLocalMap();
method public androidx.compose.ui.unit.Density getDensity();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public androidx.compose.ui.layout.MeasurePolicy getMeasurePolicy();
method public androidx.compose.ui.Modifier getModifier();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+ method public void setCompositionLocalMap(androidx.compose.runtime.CompositionLocalMap);
method public void setDensity(androidx.compose.ui.unit.Density);
method public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection);
method public void setMeasurePolicy(androidx.compose.ui.layout.MeasurePolicy);
method public void setModifier(androidx.compose.ui.Modifier);
method public void setViewConfiguration(androidx.compose.ui.platform.ViewConfiguration);
+ property public abstract androidx.compose.runtime.CompositionLocalMap compositionLocalMap;
property public abstract androidx.compose.ui.unit.Density density;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.MeasurePolicy measurePolicy;
@@ -2201,6 +2265,7 @@
method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> getSetLayoutDirection();
method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.layout.MeasurePolicy,kotlin.Unit> getSetMeasurePolicy();
method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.Modifier,kotlin.Unit> getSetModifier();
+ method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.runtime.CompositionLocalMap,kotlin.Unit> getSetResolvedCompositionLocals();
method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.platform.ViewConfiguration,kotlin.Unit> getSetViewConfiguration();
method public kotlin.jvm.functions.Function0<androidx.compose.ui.node.ComposeUiNode> getVirtualConstructor();
property public final kotlin.jvm.functions.Function0<androidx.compose.ui.node.ComposeUiNode> Constructor;
@@ -2208,10 +2273,106 @@
property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> SetLayoutDirection;
property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.layout.MeasurePolicy,kotlin.Unit> SetMeasurePolicy;
property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.Modifier,kotlin.Unit> SetModifier;
+ property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.runtime.CompositionLocalMap,kotlin.Unit> SetResolvedCompositionLocals;
property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.platform.ViewConfiguration,kotlin.Unit> SetViewConfiguration;
property public final kotlin.jvm.functions.Function0<androidx.compose.ui.node.ComposeUiNode> VirtualConstructor;
}
+ public interface DelegatableNode {
+ method public androidx.compose.ui.Modifier.Node getNode();
+ property public abstract androidx.compose.ui.Modifier.Node node;
+ }
+
+ public final class DelegatableNodeKt {
+ method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
+ method public static androidx.compose.ui.unit.LayoutDirection requireLayoutDirection(androidx.compose.ui.node.DelegatableNode);
+ }
+
+ public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
+ ctor public DelegatingNode();
+ method public final <T extends androidx.compose.ui.Modifier.Node> T delegated(kotlin.jvm.functions.Function0<? extends T> fn);
+ }
+
+ public interface DrawModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
+ method public default void onMeasureResultChanged();
+ }
+
+ public final class DrawModifierNodeKt {
+ method public static void invalidateDraw(androidx.compose.ui.node.DrawModifierNode);
+ }
+
+ public interface GlobalPositionAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
+ }
+
+ public interface IntermediateLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
+ method public long getTargetSize();
+ method public void setTargetSize(long);
+ property public abstract long targetSize;
+ }
+
+ public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public default void onPlaced(androidx.compose.ui.layout.LayoutCoordinates coordinates);
+ method public default void onRemeasured(long size);
+ }
+
+ public interface LayoutModifierNode extends androidx.compose.ui.layout.Remeasurement androidx.compose.ui.node.DelegatableNode {
+ method public default void forceRemeasure();
+ method public default int maxIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int maxIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public androidx.compose.ui.layout.MeasureResult measure(androidx.compose.ui.layout.MeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
+ method public default int minIntrinsicHeight(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ }
+
+ public final class LayoutModifierNodeKt {
+ method public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
+ method public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
+ }
+
+ public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> implements androidx.compose.ui.platform.InspectableValue androidx.compose.ui.Modifier.Element {
+ ctor public ModifierNodeElement();
+ method public abstract N create();
+ method public abstract boolean equals(Object? other);
+ method public boolean getAutoInvalidate();
+ method public final kotlin.sequences.Sequence<androidx.compose.ui.platform.ValueElement> getInspectableElements();
+ method public final String? getNameFallback();
+ method public final Object? getValueOverride();
+ method public abstract int hashCode();
+ method public void inspectableProperties(androidx.compose.ui.platform.InspectorInfo);
+ method public abstract N update(N node);
+ property public boolean autoInvalidate;
+ property public final kotlin.sequences.Sequence<androidx.compose.ui.platform.ValueElement> inspectableElements;
+ property public final String? nameFallback;
+ property public final Object? valueOverride;
+ }
+
+ public interface ObserverNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onObservedReadsChanged();
+ }
+
+ public final class ObserverNodeKt {
+ method public static <T extends androidx.compose.ui.Modifier.Node & androidx.compose.ui.node.ObserverNode> void observeReads(T, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+ }
+
+ public interface ParentDataModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public Object? modifyParentData(androidx.compose.ui.unit.Density, Object? parentData);
+ }
+
+ public final class ParentDataModifierNodeKt {
+ method public static void invalidateParentData(androidx.compose.ui.node.ParentDataModifierNode);
+ }
+
+ public interface PointerInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public default boolean interceptOutOfBoundsChildEvents();
+ method public void onCancelPointerInput();
+ method public void onPointerEvent(androidx.compose.ui.input.pointer.PointerEvent pointerEvent, androidx.compose.ui.input.pointer.PointerEventPass pass, long bounds);
+ method public default boolean sharePointerInputWithSiblings();
+ }
+
public final class Ref<T> {
ctor public Ref();
method public T? getValue();
@@ -2229,6 +2390,16 @@
property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
}
+ 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;
+ }
+
+ public final class SemanticsModifierNodeKt {
+ method public static androidx.compose.ui.semantics.SemanticsConfiguration collapsedSemanticsConfiguration(androidx.compose.ui.node.SemanticsModifierNode);
+ method public static void invalidateSemantics(androidx.compose.ui.node.SemanticsModifierNode);
+ }
+
}
package androidx.compose.ui.platform {
@@ -2337,6 +2508,7 @@
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> getLocalHapticFeedback();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> getLocalInputModeManager();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> getLocalLayoutDirection();
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> getLocalPlatformTextInputPluginRegistry();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> getLocalTextInputService();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> getLocalTextToolbar();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> getLocalUriHandler();
@@ -2350,6 +2522,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.hapticfeedback.HapticFeedback> LocalHapticFeedback;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.input.InputModeManager> LocalInputModeManager;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.LayoutDirection> LocalLayoutDirection;
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.PlatformTextInputPluginRegistry> LocalPlatformTextInputPluginRegistry;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.input.TextInputService> LocalTextInputService;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.TextToolbar> LocalTextToolbar;
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.platform.UriHandler> LocalUriHandler;
@@ -2697,6 +2870,7 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageRight();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPageUp();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPasteText();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getPerformImeAction();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getRequestFocus();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> getScrollBy();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> getScrollToIndex();
@@ -2717,6 +2891,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageRight;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PageUp;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PasteText;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> PerformImeAction;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> RequestFocus;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function2<java.lang.Float,java.lang.Float,java.lang.Boolean>>> ScrollBy;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Boolean>>> ScrollToIndex;
@@ -2902,6 +3077,7 @@
method public static void pageUp(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void password(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
+ method public static void performImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void requestFocus(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean>? action);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean>? action);
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt
new file mode 100644
index 0000000..b98dee2
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierCompositionLocalSample.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.platform.InspectorInfo
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun CompositionLocalConsumingModifierSample() {
+ val LocalBackgroundColor = compositionLocalOf { Color.White }
+ class BackgroundColor : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ override fun ContentDrawScope.draw() {
+ val backgroundColor = currentValueOf(LocalBackgroundColor)
+ drawRect(backgroundColor)
+ drawContent()
+ }
+ }
+ val BackgroundColorModifierElement = object : ModifierNodeElement<BackgroundColor>() {
+ override fun create() = BackgroundColor()
+ override fun update(node: BackgroundColor) = node
+ override fun hashCode() = System.identityHashCode(this)
+ override fun equals(other: Any?) = (other === this)
+ override fun InspectorInfo.inspectableProperties() {
+ name = "backgroundColor"
+ }
+ }
+ fun Modifier.backgroundColor() = this then BackgroundColorModifierElement
+ Box(Modifier.backgroundColor()) {
+ Text("Hello, world!")
+ }
+}
\ No newline at end of file
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 9f37d20..92cffbf 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
@@ -97,6 +97,8 @@
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
+import java.util.concurrent.Executors
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -123,7 +125,10 @@
// Use uiAutomation to enable accessibility manager.
InstrumentationRegistry.getInstrumentation().uiAutomation
rule.activityRule.scenario.onActivity {
- androidComposeView = AndroidComposeView(it)
+ androidComposeView = AndroidComposeView(
+ it,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ )
container = spy(FrameLayout(it)) {
on {
onRequestSendAccessibilityEvent(
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 273688d..3a416f3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -113,11 +113,13 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -352,7 +354,10 @@
expectedCompositing: Boolean,
expectedOverlappingRendering: Boolean
): Boolean {
- val node = RenderNodeApi29(AndroidComposeView(activity)).apply {
+ val node = RenderNodeApi29(AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ )).apply {
this.compositingStrategy = compositingStrategy
}
return expectedCompositing == node.isUsingCompositingLayer() &&
@@ -365,7 +370,10 @@
expectedLayerType: Int,
expectedOverlappingRendering: Boolean
): Boolean {
- val node = RenderNodeApi23(AndroidComposeView(activity)).apply {
+ val node = RenderNodeApi23(AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ )).apply {
this.compositingStrategy = compositingStrategy
}
return expectedLayerType == node.getLayerType() &&
@@ -378,7 +386,10 @@
expectedOverlappingRendering: Boolean
): Boolean {
val view = ViewLayer(
- AndroidComposeView(activity),
+ AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ ),
ViewLayerContainer(activity),
{},
{}).apply {
@@ -412,7 +423,10 @@
private fun verifyRenderNode29CameraDistance(cameraDistance: Float): Boolean =
// Verify that the internal render node has the camera distance property
// given to the wrapper
- RenderNodeApi29(AndroidComposeView(activity)).apply {
+ RenderNodeApi29(AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ )).apply {
this.cameraDistance = cameraDistance
}.dumpRenderNodeData().cameraDistance == cameraDistance
@@ -420,13 +434,19 @@
private fun verifyRenderNode23CameraDistance(cameraDistance: Float): Boolean =
// Verify that the internal render node has the camera distance property
// given to the wrapper
- RenderNodeApi23(AndroidComposeView(activity)).apply {
+ RenderNodeApi23(AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ )).apply {
this.cameraDistance = cameraDistance
}.dumpRenderNodeData().cameraDistance == -cameraDistance // Camera distance is negative
private fun verifyViewLayerCameraDistance(cameraDistance: Float): Boolean {
val layer = ViewLayer(
- AndroidComposeView(activity),
+ AndroidComposeView(
+ activity,
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+ ),
ViewLayerContainer(activity),
{},
{}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index fe60095..b22ea31 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -72,6 +72,9 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -3615,7 +3618,9 @@
@OptIn(InternalCoreApi::class)
private class MockOwner(
val position: IntOffset = IntOffset.Zero,
- override val root: LayoutNode = LayoutNode()
+ override val root: LayoutNode = LayoutNode(),
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
) : Owner {
val onRequestMeasureParams = mutableListOf<LayoutNode>()
val onAttachParams = mutableListOf<LayoutNode>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index fbdd74e..c59c37e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -64,6 +64,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -3407,6 +3410,9 @@
get() = TODO("Not yet implemented")
override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
+
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
onEndListeners += listener
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 6aca24e..cf021ed 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -58,8 +58,11 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
import kotlin.math.max
import kotlin.math.min
+import kotlinx.coroutines.asCoroutineDispatcher
internal fun createDelegate(
root: LayoutNode,
@@ -80,7 +83,9 @@
@OptIn(ExperimentalComposeUiApi::class)
private class FakeOwner(
val delegate: MeasureAndLayoutDelegate,
- val createLayer: () -> OwnedLayer
+ val createLayer: () -> OwnedLayer,
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
) : Owner {
override val measureIteration: Long
get() = delegate.measureIteration
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt
new file mode 100644
index 0000000..27cee1b
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/CompositionLocalModifierNodeTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CompositionLocalModifierNodeTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val staticLocalInt = staticCompositionLocalOf { 0 }
+ private val localInt = compositionLocalOf { 0 }
+ private val EmptyBoxMeasurePolicy = MeasurePolicy { _, constraints ->
+ layout(constraints.maxWidth, constraints.maxHeight) {}
+ }
+
+ @Test
+ fun defaultValueReturnedIfNotProvided() {
+ var readValue = -1
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ override fun ContentDrawScope.draw() {
+ readValue = currentValueOf(localInt)
+ drawContent()
+ }
+ }
+ rule.setContent {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun providedValueReturned() {
+ var readValue = -1
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ override fun ContentDrawScope.draw() {
+ readValue = currentValueOf(localInt)
+ drawContent()
+ }
+ }
+ rule.setContent {
+ CompositionLocalProvider(localInt provides 2) {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun providedValueUpdatedReadsNewValue() {
+ var readValue = -1
+ var providedValue by mutableStateOf(2)
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ fun getValue(): Int = currentValueOf(localInt)
+ override fun ContentDrawScope.draw() {
+ readValue = getValue()
+ drawContent()
+ }
+ }
+ rule.setContent {
+ CompositionLocalProvider(localInt provides providedValue) {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(2)
+ assertThat(node.getValue()).isEqualTo(2)
+ providedValue = 3
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(3)
+ assertThat(node.getValue()).isEqualTo(3)
+ }
+ }
+
+ @Test
+ fun defaultStaticValueReturnedIfNotProvided() {
+ var readValue = -1
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ override fun ContentDrawScope.draw() {
+ readValue = currentValueOf(staticLocalInt)
+ drawContent()
+ }
+ }
+ rule.setContent {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun providedStaticValueReturned() {
+ var readValue = -1
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ override fun ContentDrawScope.draw() {
+ readValue = currentValueOf(staticLocalInt)
+ drawContent()
+ }
+ }
+ rule.setContent {
+ CompositionLocalProvider(staticLocalInt provides 2) {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun providedStaticValueUpdatedReadsNewValue() {
+ var readValue = -1
+ var providedValue by mutableStateOf(2)
+ val node = object : Modifier.Node(), DrawModifierNode,
+ CompositionLocalConsumerModifierNode {
+ fun getValue(): Int = currentValueOf(staticLocalInt)
+ override fun ContentDrawScope.draw() {
+ readValue = getValue()
+ drawContent()
+ }
+ }
+ rule.setContent {
+ CompositionLocalProvider(staticLocalInt provides providedValue) {
+ Layout(modifierNodeElementOf { node }, EmptyBoxMeasurePolicy)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(2)
+ assertThat(node.getValue()).isEqualTo(2)
+ providedValue = 3
+ }
+
+ rule.runOnIdle {
+ assertThat(readValue).isEqualTo(3)
+ assertThat(node.getValue()).isEqualTo(3)
+ }
+ }
+
+ @ExperimentalComposeUiApi
+ private inline fun <reified T : Modifier.Node> modifierNodeElementOf(
+ crossinline create: () -> T
+ ): ModifierNodeElement<T> = object : ModifierNodeElement<T>() {
+ override fun create(): T = create()
+ override fun update(node: T) = node
+ override fun hashCode(): Int = System.identityHashCode(this)
+ override fun equals(other: Any?) = (other === this)
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapterRegistryTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapterRegistryTest.kt
index f9504dd..483afd2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapterRegistryTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputAdapterRegistryTest.kt
@@ -332,6 +332,5 @@
private object NoopInputForTests : TextInputForTests {
override fun inputTextForTest(text: String) = TODO("Not implemented for test")
- override fun submitTextForTest() = TODO("Not implemented for test")
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputEditTextIntegrationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputEditTextIntegrationTest.kt
index ba35eee..9a470d9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputEditTextIntegrationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputEditTextIntegrationTest.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalPlatformTextInputPluginRegistry
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.performImeAction
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setSelection
import androidx.compose.ui.semantics.setText
@@ -125,7 +126,7 @@
}
@Test
- fun textSubmit() {
+ fun textPerformImeAction() {
var recordedActionCode: Int = -1
var recordedKeyEvent: KeyEvent? = null
setContentAndFocusField()
@@ -178,6 +179,10 @@
}
return@setSelection false
}
+ performImeAction {
+ editText.onEditorAction(ExpectedActionCode)
+ true
+ }
},
factory = { context ->
EditTextWrapper(context, adapter)
@@ -212,10 +217,6 @@
override fun inputTextForTest(text: String) {
this.text.append(text)
}
-
- override fun submitTextForTest() {
- onEditorAction(ExpectedActionCode)
- }
}
private object TestPlugin : PlatformTextInputPlugin<TestAdapter> {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputTestIntegrationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputTestIntegrationTest.kt
index 9c14a20..9d158a5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputTestIntegrationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputTestIntegrationTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.platform.LocalPlatformTextInputPluginRegistry
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.performImeAction
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setText
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -90,7 +91,7 @@
rule.runOnIdle {
assertThat(testCommands).containsExactly(
"input(hello)",
- "submit",
+ "performImeAction",
).inOrder()
}
}
@@ -140,6 +141,10 @@
.focusable()
.semantics {
setText { true }
+ performImeAction {
+ testCommands += "performImeAction"
+ true
+ }
}
)
}
@@ -173,9 +178,5 @@
override fun inputTextForTest(text: String) {
testCommands!! += "input($text)"
}
-
- override fun submitTextForTest() {
- testCommands!! += "submit"
- }
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
index 5ef1140..77d84d7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/text/input/PlatformTextInputViewIntegrationTest.kt
@@ -151,6 +151,5 @@
private object NoopInputForTests : TextInputForTests {
override fun inputTextForTest(text: String) = TODO("Not implemented for test")
- override fun submitTextForTest() = TODO("Not implemented for test")
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index c995494..de755b1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -202,13 +202,21 @@
}
}
+ // Assert view initially attached
rule.runOnUiThread {
assertThat(frameLayout.parent).isNotNull()
emit = false
}
+ // Assert view detached when removed from composition hierarchy
rule.runOnIdle {
assertThat(frameLayout.parent).isNull()
+ emit = true
+ }
+
+ // Assert view reattached when added back to the composition hierarchy
+ rule.runOnIdle {
+ assertThat(frameLayout.parent).isNotNull()
}
}
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 1a3044f..5cad811 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
@@ -159,11 +159,12 @@
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import java.lang.reflect.Method
+import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(ExperimentalComposeUiApi::class, InternalTextApi::class, ExperimentalTextApi::class)
-internal class AndroidComposeView(context: Context) :
+internal class AndroidComposeView(context: Context, coroutineContext: CoroutineContext) :
ViewGroup(context), Owner, ViewRootForTest, PositionCalculator, DefaultLifecycleObserver {
/**
@@ -452,6 +453,8 @@
*/
override val textToolbar: TextToolbar = AndroidTextToolbar(this)
+ override val coroutineContext: CoroutineContext = coroutineContext
+
/**
* When the first event for a mouse is ACTION_DOWN, an ACTION_HOVER_ENTER is never sent.
* This means that we won't receive an `Enter` event for the first mouse. In order to prevent
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 95f7c7e..dfedb9f 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
@@ -96,15 +96,15 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.lifecycle.Lifecycle
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
-import kotlin.math.roundToInt
-import kotlin.math.sign
import kotlin.math.max
import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
// TODO(mnuzen): This code is copy-pasted from experimental API in the Kotlin 1.7 standard library: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/range-until.html.
// Delete it when this API graduates to stable in Kotlin (when the docs page linked no longer has @ExperimentalStdlibApi annotation).
@@ -943,6 +943,15 @@
)
}
+ semanticsNode.unmergedConfig.getOrNull(SemanticsActions.PerformImeAction)?.let {
+ info.addAction(
+ AccessibilityActionCompat(
+ android.R.id.accessibilityActionImeEnter,
+ it.label
+ )
+ )
+ }
+
// The config will contain this action only if there is a text selection at the moment.
semanticsNode.unmergedConfig.getOrNull(SemanticsActions.CutText)?.let {
info.addAction(
@@ -1672,6 +1681,11 @@
?.action?.invoke(AnnotatedString(text ?: "")) ?: false
}
+ android.R.id.accessibilityActionImeEnter -> {
+ return node.unmergedConfig.getOrNull(SemanticsActions.PerformImeAction)
+ ?.action?.invoke() ?: false
+ }
+
AccessibilityNodeInfoCompat.ACTION_PASTE -> {
return node.unmergedConfig.getOrNull(
SemanticsActions.PasteText
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt
index 90aafa5..820d05d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt
@@ -77,7 +77,9 @@
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
- } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
+ } ?: AndroidComposeView(context, parent.effectCoroutineContext).also {
+ addView(it.view, DefaultLayoutParams)
+ }
return doSetContent(composeView, parent, content)
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 4abf6e6..524d0ec 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -300,7 +300,7 @@
val dispatcher: NestedScrollDispatcher,
private val saveStateRegistry: SaveableStateRegistry?,
private val saveStateKey: String
-) : AndroidViewHolder(context, parentContext, dispatcher), ViewRootForInspector {
+) : AndroidViewHolder(context, parentContext, dispatcher, typedView), ViewRootForInspector {
constructor(
context: Context,
@@ -329,7 +329,6 @@
init {
clipChildren = false
- view = typedView
@Suppress("UNCHECKED_CAST")
val savedState = saveStateRegistry
?.consumeRestored(saveStateKey) as? SparseArray<Parcelable>
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 258b717..40825d9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -71,7 +71,11 @@
internal open class AndroidViewHolder(
context: Context,
parentContext: CompositionContext?,
- private val dispatcher: NestedScrollDispatcher
+ private val dispatcher: NestedScrollDispatcher,
+ /**
+ * The view hosted by this holder.
+ */
+ val view: View
) : ViewGroup(context), NestedScrollingParent3, ComposeNodeLifecycleCallback {
init {
@@ -83,23 +87,13 @@
}
// We save state ourselves, depending on composition.
isSaveFromParentEnabled = false
+
+ @Suppress("LeakingThis")
+ addView(view)
}
- /**
- * The view hosted by this holder.
- */
- var view: View? = null
- internal set(value) {
- if (value !== field) {
- field = value
- removeAllViewsInLayout()
- if (value != null) {
- addView(value)
- runUpdate()
- }
- }
- }
-
+ // Keep nullable to match the `expect` declaration of InteropViewFactoryHolder
+ @Suppress("RedundantNullableReturnType")
fun getInteropView(): InteropView? = view
/**
@@ -195,7 +189,7 @@
// We reset at the same time we remove the view. So if the view was removed, we can just
// re-add it and it's ready to go. If it's already attached, we didn't reset it and need
// to do so for it to be reused correctly.
- if (view!!.parent !== this) {
+ if (view.parent !== this) {
addView(view)
} else {
reset()
@@ -212,8 +206,8 @@
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- view?.measure(widthMeasureSpec, heightMeasureSpec)
- setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
+ view.measure(widthMeasureSpec, heightMeasureSpec)
+ setMeasuredDimension(view.measuredWidth, view.measuredHeight)
lastWidthMeasureSpec = widthMeasureSpec
lastHeightMeasureSpec = heightMeasureSpec
}
@@ -228,11 +222,11 @@
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
- view?.layout(0, 0, r - l, b - t)
+ view.layout(0, 0, r - l, b - t)
}
override fun getLayoutParams(): LayoutParams? {
- return view?.layoutParams
+ return view.layoutParams
?: LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
@@ -323,15 +317,13 @@
layoutNode.density = density
onDensityChanged = { layoutNode.density = it }
- var viewRemovedOnDetach: View? = null
layoutNode.onAttach = { owner ->
(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
- if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
+ if (view.parent !== this) addView(view)
}
layoutNode.onDetach = { owner ->
(owner as? AndroidComposeView)?.removeAndroidView(this)
- viewRemovedOnDetach = view
- view = null
+ removeAllViewsInLayout()
}
layoutNode.measurePolicy = object : MeasurePolicy {
@@ -533,7 +525,7 @@
}
override fun isNestedScrollingEnabled(): Boolean {
- return view?.isNestedScrollingEnabled ?: super.isNestedScrollingEnabled()
+ return view.isNestedScrollingEnabled
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index a3f8e91..3e001e7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -23,6 +23,9 @@
import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.node.NodeKind
import androidx.compose.ui.node.requireOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
/**
* An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add
@@ -147,6 +150,7 @@
* [androidx.compose.ui.node.ModifierNodeElement] to a [Modifier] chain.
*
* @see androidx.compose.ui.node.ModifierNodeElement
+ * @see androidx.compose.ui.node.CompositionLocalConsumerModifierNode
* @see androidx.compose.ui.node.DelegatableNode
* @see androidx.compose.ui.node.DelegatingNode
* @see androidx.compose.ui.node.LayoutModifierNode
@@ -159,11 +163,21 @@
* @see androidx.compose.ui.node.GlobalPositionAwareModifierNode
* @see androidx.compose.ui.node.IntermediateLayoutModifierNode
*/
- @ExperimentalComposeUiApi
abstract class Node : DelegatableNode {
@Suppress("LeakingThis")
final override var node: Node = this
private set
+
+ private var scope: CoroutineScope? = null
+ // CoroutineScope(baseContext + Job(parent = baseContext[Job]))
+ val coroutineScope: CoroutineScope
+ get() = scope ?: CoroutineScope(
+ requireOwner().coroutineContext +
+ Job(parent = requireOwner().coroutineContext[Job])
+ ).also {
+ scope = it
+ }
+
internal var kindSet: Int = 0
// NOTE: We use an aggregate mask that or's all of the type masks of the children of the
// chain so that we can quickly prune a subtree. This INCLUDES the kindSet of this node
@@ -208,7 +222,12 @@
check(coordinator != null)
onDetach()
isAttached = false
-// coordinator = null
+
+ scope?.let {
+ it.cancel("Modifier.Node was detached")
+ scope = null
+ }
+ // coordinator = null
// TODO(lmr): cancel jobs / side effects?
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index 681cfed..1f3306f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -141,7 +141,6 @@
onBuildDrawCache: CacheDrawScope.() -> DrawResult
) = this then DrawWithCacheElement(onBuildDrawCache)
-@ExperimentalComposeUiApi
private data class DrawWithCacheElement(
val onBuildDrawCache: CacheDrawScope.() -> DrawResult
) : ModifierNodeElement<CacheDrawNode>() {
@@ -159,7 +158,6 @@
}
}
-@ExperimentalComposeUiApi
private class CacheDrawNode(
private val cacheDrawScope: CacheDrawScope,
block: CacheDrawScope.() -> DrawResult
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 4b3bbd9..3c9fcbc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -92,7 +92,6 @@
*
* @sample androidx.compose.ui.samples.PainterModifierSample
*/
-@ExperimentalComposeUiApi
private data class PainterModifierNodeElement(
val painter: Painter,
val sizeToIntrinsics: Boolean,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
index 4f4e15f..8f4adae 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
@@ -25,7 +24,6 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-@ExperimentalComposeUiApi
internal fun <T> FocusTargetModifierNode.searchBeyondBounds(
direction: FocusDirection,
block: BeyondBoundsScope.() -> T?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
index 49f428d..1dd3153 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
@@ -35,7 +34,6 @@
onFocusChanged: (FocusState) -> Unit
): Modifier = this then FocusChangedElement(onFocusChanged)
-@OptIn(ExperimentalComposeUiApi::class)
private data class FocusChangedElement(
val onFocusChanged: (FocusState) -> Unit
) : ModifierNodeElement<FocusChangedModifierNode>() {
@@ -51,7 +49,6 @@
}
}
-@ExperimentalComposeUiApi
private class FocusChangedModifierNode(
var onFocusChanged: (FocusState) -> Unit
) : FocusEventModifierNode, Modifier.Node() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt
index f1d9d4a..12fd0c7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
@@ -30,7 +29,6 @@
* Implement this interface create a modifier node that can be used to observe focus state changes
* to a [FocusTargetModifierNode] down the hierarchy.
*/
-@ExperimentalComposeUiApi
interface FocusEventModifierNode : DelegatableNode {
/**
@@ -40,7 +38,6 @@
fun onFocusEvent(focusState: FocusState)
}
-@OptIn(ExperimentalComposeUiApi::class)
internal fun FocusEventModifierNode.getFocusState(): FocusState {
visitChildren(Nodes.FocusTarget) {
when (val focusState = it.focusStateImpl) {
@@ -60,7 +57,6 @@
*
* Make this public after [FocusTargetModifierNode] is made public.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.refreshFocusEventNodes() {
visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
// If we reach the previous focus target node, we have gone too far, as
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 154afb0..7a08381 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -53,7 +53,6 @@
*/
internal class FocusOwnerImpl(onRequestApplyChangesListener: (() -> Unit) -> Unit) : FocusOwner {
- @OptIn(ExperimentalComposeUiApi::class)
internal var rootFocusNode = FocusTargetModifierNode()
private val focusInvalidationManager = FocusInvalidationManager(onRequestApplyChangesListener)
@@ -63,7 +62,6 @@
* list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
*/
// TODO(b/168831247): return an empty Modifier when there are no focusable children.
- @OptIn(ExperimentalComposeUiApi::class)
override val modifier: Modifier = object : ModifierNodeElement<FocusTargetModifierNode>() {
override fun create() = rootFocusNode
@@ -89,7 +87,6 @@
override fun takeFocus() {
// If the focus state is not Inactive, it indicates that the focus state is already
// set (possibly by dispatchWindowFocusChanged). So we don't update the state.
- @OptIn(ExperimentalComposeUiApi::class)
if (rootFocusNode.focusStateImpl == Inactive) {
rootFocusNode.focusStateImpl = Active
// TODO(b/152535715): propagate focus to children based on child focusability.
@@ -104,7 +101,6 @@
* all the focus modifiers in the component hierarchy.
*/
override fun releaseFocus() {
- @OptIn(ExperimentalComposeUiApi::class)
rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
}
@@ -183,7 +179,6 @@
/**
* Dispatches a key event through the compose hierarchy.
*/
- @OptIn(ExperimentalComposeUiApi::class)
override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
val activeFocusTarget = rootFocusNode.findActiveFocusNode()
checkNotNull(activeFocusTarget) {
@@ -218,22 +213,18 @@
return false
}
- @OptIn(ExperimentalComposeUiApi::class)
override fun scheduleInvalidation(node: FocusTargetModifierNode) {
focusInvalidationManager.scheduleInvalidation(node)
}
- @OptIn(ExperimentalComposeUiApi::class)
override fun scheduleInvalidation(node: FocusEventModifierNode) {
focusInvalidationManager.scheduleInvalidation(node)
}
- @OptIn(ExperimentalComposeUiApi::class)
override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
focusInvalidationManager.scheduleInvalidation(node)
}
- @ExperimentalComposeUiApi
private inline fun <reified T : DelegatableNode> T.traverseAncestors(
type: NodeKind<T>,
onPreVisit: (T) -> Unit,
@@ -250,11 +241,9 @@
* Searches for the currently focused item, and returns its coordinates as a rect.
*/
override fun getFocusRect(): Rect? {
- @OptIn(ExperimentalComposeUiApi::class)
return rootFocusNode.findActiveFocusNode()?.focusRect()
}
- @OptIn(ExperimentalComposeUiApi::class)
private fun DelegatableNode.lastLocalKeyInputNode(): KeyInputModifierNode? {
var focusedKeyInputNode: KeyInputModifierNode? = null
visitLocalChildren(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
@@ -271,7 +260,6 @@
// will then pass focus to other views, and ultimately return back to this compose view.
private fun wrapAroundFocus(focusDirection: FocusDirection): Boolean {
// Wrap is not supported when this sub-hierarchy doesn't have focus.
- @OptIn(ExperimentalComposeUiApi::class)
if (!rootFocusNode.focusState.hasFocus || rootFocusNode.focusState.isFocused) return false
// Next and Previous wraps around.
@@ -279,7 +267,6 @@
Next, Previous -> {
// Clear Focus to send focus the root node.
clearFocus(force = false)
- @OptIn(ExperimentalComposeUiApi::class)
if (!rootFocusNode.focusState.isFocused) return false
// Wrap around by calling moveFocus after the root gains focus.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt
index 4bfb10f..bf18f73 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt
@@ -16,14 +16,12 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.node.DelegatableNode
/**
* Implement this interface create a modifier node that can be used to modify the focus properties
* of the associated [FocusTargetModifierNode].
*/
-@ExperimentalComposeUiApi
interface FocusPropertiesModifierNode : DelegatableNode {
/**
* A parent can modify the focus properties associated with the nearest
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index fd0c2b5..7044bb7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -50,7 +50,6 @@
@Stable
class FocusRequester {
- @OptIn(ExperimentalComposeUiApi::class)
internal val focusRequesterNodes: MutableVector<FocusRequesterModifierNode> = mutableVectorOf()
/**
@@ -94,7 +93,6 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
- @OptIn(ExperimentalComposeUiApi::class)
fun captureFocus(): Boolean {
check(focusRequesterNodes.isNotEmpty()) { FocusRequesterNotInitialized }
focusRequesterNodes.forEach {
@@ -119,7 +117,6 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
- @OptIn(ExperimentalComposeUiApi::class)
fun freeFocus(): Boolean {
check(focusRequesterNodes.isNotEmpty()) { FocusRequesterNotInitialized }
focusRequesterNodes.forEach {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
index 520be2b..a24d056 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
@@ -26,7 +26,6 @@
* Implement this interface to create a modifier node that can be used to request changes in
* the focus state of a [FocusTargetModifierNode] down the hierarchy.
*/
-@ExperimentalComposeUiApi
interface FocusRequesterModifierNode : DelegatableNode
/**
@@ -36,7 +35,7 @@
*
* @sample androidx.compose.ui.samples.RequestFocusSample
*/
-@ExperimentalComposeUiApi
+@OptIn(ExperimentalComposeUiApi::class)
fun FocusRequesterModifierNode.requestFocus(): Boolean {
visitChildren(Nodes.FocusTarget) { focusTarget ->
val focusProperties = focusTarget.fetchFocusProperties()
@@ -66,7 +65,6 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
-@ExperimentalComposeUiApi
fun FocusRequesterModifierNode.captureFocus(): Boolean {
visitChildren(Nodes.FocusTarget) {
if (it.captureFocus()) {
@@ -90,7 +88,6 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
-@ExperimentalComposeUiApi
fun FocusRequesterModifierNode.freeFocus(): Boolean {
visitChildren(Nodes.FocusTarget) {
if (it.freeFocus()) return true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
index 086ccc5..dac3281 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
@@ -38,7 +38,6 @@
* This modifier node can be used to create a modifier that makes a component focusable.
* Use a different instance of [FocusTargetModifierNode] for each focusable component.
*/
-@ExperimentalComposeUiApi
class FocusTargetModifierNode : ObserverNode, ModifierLocalNode, Modifier.Node() {
/**
* The [FocusState] associated with this [FocusTargetModifierNode].
@@ -82,7 +81,6 @@
* [FocusPropertiesModifierNode.modifyFocusProperties] on each parent.
* This effectively collects an aggregated focus state.
*/
- @ExperimentalComposeUiApi
internal fun fetchFocusProperties(): FocusProperties {
val properties = FocusPropertiesImpl()
visitAncestors(Nodes.FocusProperties or Nodes.FocusTarget) {
@@ -107,7 +105,7 @@
* This function prevents that re-entrant scenario by ensuring there is only one concurrent
* invocation of this lambda.
*/
- @ExperimentalComposeUiApi
+ @OptIn(ExperimentalComposeUiApi::class)
internal inline fun fetchCustomEnter(
focusDirection: FocusDirection,
block: (FocusRequester) -> Unit
@@ -131,7 +129,7 @@
* This function prevents that re-entrant scenario by ensuring there is only one concurrent
* invocation of this lambda.
*/
- @ExperimentalComposeUiApi
+ @OptIn(ExperimentalComposeUiApi::class)
internal inline fun fetchCustomExit(
focusDirection: FocusDirection,
block: (FocusRequester) -> Unit
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
index 0b03a17..a9154e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
@@ -38,7 +38,7 @@
* children. Calling this function will send a focus request to this
* [FocusNode][FocusTargetModifierNode]'s parent [FocusNode][FocusTargetModifierNode].
*/
-@ExperimentalComposeUiApi
+@OptIn(ExperimentalComposeUiApi::class)
internal fun FocusTargetModifierNode.requestFocus(): Boolean {
return when (performCustomRequestFocus(Enter)) {
None -> performRequestFocus()
@@ -54,7 +54,6 @@
* custom focus [enter][FocusProperties.enter] and [exit][FocusProperties.exit]
* [properties][FocusProperties] have been specified.
*/
-@OptIn(ExperimentalComposeUiApi::class)
internal fun FocusTargetModifierNode.performRequestFocus(): Boolean {
when (focusStateImpl) {
Active, Captured -> {
@@ -82,7 +81,6 @@
*
* @return true if the focus was successfully captured. False otherwise.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.captureFocus() = when (focusStateImpl) {
Active -> {
focusStateImpl = Captured
@@ -100,7 +98,6 @@
*
* @return true if the captured focus was released. False Otherwise.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.freeFocus() = when (focusStateImpl) {
Captured -> {
focusStateImpl = Active
@@ -118,7 +115,6 @@
* clear focus from one of its child [focus node][FocusTargetModifierNode]s. It does not change the
* state of the parent.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.clearFocus(
forced: Boolean = false,
refreshFocusEvents: Boolean
@@ -161,7 +157,6 @@
* Note: This is a private function that just changes the state of this node and does not affect any
* other nodes in the hierarchy.
*/
-@OptIn(ExperimentalComposeUiApi::class)
private fun FocusTargetModifierNode.grantFocus(): Boolean {
// When we grant focus to this node, we need to observe changes to the canFocus property.
// If canFocus is set to false, we need to clear focus.
@@ -175,7 +170,6 @@
}
/** This function clears any focus from the focused child. */
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.clearChildFocus(
forced: Boolean = false,
refreshFocusEvents: Boolean = true
@@ -188,7 +182,6 @@
* @param childNode: The node that is requesting focus.
* @return true if focus was granted, false otherwise.
*/
-@OptIn(ExperimentalComposeUiApi::class)
private fun FocusTargetModifierNode.requestFocusForChild(
childNode: FocusTargetModifierNode
): Boolean {
@@ -247,14 +240,12 @@
}
}
-@OptIn(ExperimentalComposeUiApi::class)
private fun FocusTargetModifierNode.requestFocusForOwner(): Boolean {
return coordinator?.layoutNode?.owner?.requestFocus() ?: error("Owner not initialized.")
}
internal enum class CustomDestinationResult { None, Cancelled, Redirected, RedirectCancelled }
-@OptIn(ExperimentalComposeUiApi::class)
internal fun FocusTargetModifierNode.performCustomRequestFocus(
focusDirection: FocusDirection
): CustomDestinationResult {
@@ -276,7 +267,6 @@
}
}
-@OptIn(ExperimentalComposeUiApi::class)
internal fun FocusTargetModifierNode.performCustomClearFocus(
focusDirection: FocusDirection
): CustomDestinationResult = when (focusStateImpl) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index ac3985f..3b72b28 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -122,7 +122,6 @@
* Returns the bounding box of the focus layout area in the root or [Rect.Zero] if the
* FocusModifier has not had a layout.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.focusRect(): Rect = coordinator?.let {
it.findRootCoordinates().localBoundingBoxOf(it, clipBounds = false)
} ?: Rect.Zero
@@ -130,12 +129,10 @@
/**
* Whether this node should be considered when searching for the next item during a traversal.
*/
-@ExperimentalComposeUiApi
internal val FocusTargetModifierNode.isEligibleForFocusSearch: Boolean
get() = coordinator?.layoutNode?.isPlaced == true &&
coordinator?.layoutNode?.isAttached == true
-@ExperimentalComposeUiApi
internal val FocusTargetModifierNode.activeChild: FocusTargetModifierNode?
get() {
if (!node.isAttached) return null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
index 70b237c..2c91c1c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
@@ -35,7 +35,6 @@
private const val InvalidFocusDirection = "This function should only be used for 1-D focus search"
private const val NoActiveChild = "ActiveParent must have a focusedChild"
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.oneDimensionalFocusSearch(
direction: FocusDirection,
onFound: (FocusTargetModifierNode) -> Boolean
@@ -45,7 +44,6 @@
else -> error(InvalidFocusDirection)
}
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.forwardFocusSearch(
onFound: (FocusTargetModifierNode) -> Boolean
): Boolean = when (focusStateImpl) {
@@ -62,7 +60,6 @@
}
}
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.backwardFocusSearch(
onFound: (FocusTargetModifierNode) -> Boolean
): Boolean = when (focusStateImpl) {
@@ -98,7 +95,6 @@
// Search among your children for the next child.
// If the next child is not found, generate more children by requesting a beyondBoundsLayout.
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.generateAndSearchChildren(
focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
@@ -120,7 +116,6 @@
}
// Search for the next sibling that should be granted focus.
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.searchChildren(
focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
@@ -152,7 +147,6 @@
return onFound.invoke(this)
}
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.pickChildForForwardSearch(
onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
@@ -163,7 +157,6 @@
return children.any { it.isEligibleForFocusSearch && it.forwardFocusSearch(onFound) }
}
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.pickChildForBackwardSearch(
onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index 423616e..25e9c36 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -48,7 +48,6 @@
* found, and null if focus search was cancelled using [FocusRequester.Cancel] or if a custom
* focus search destination didn't point to any [focusTarget].
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.twoDimensionalFocusSearch(
direction: FocusDirection,
onFound: (FocusTargetModifierNode) -> Boolean
@@ -95,7 +94,6 @@
* @param onFound the callback that is run when the child is found.
* @return true if we find a suitable child, false otherwise.
*/
-@ExperimentalComposeUiApi
internal fun FocusTargetModifierNode.findChildCorrespondingToFocusEnter(
direction: FocusDirection,
onFound: (FocusTargetModifierNode) -> Boolean
@@ -132,7 +130,6 @@
// Search among your children for the next child.
// If the next child is not found, generate more children by requesting a beyondBoundsLayout.
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.generateAndSearchChildren(
focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
@@ -153,7 +150,6 @@
} ?: false
}
-@ExperimentalComposeUiApi
private fun FocusTargetModifierNode.searchChildren(
focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
@@ -186,7 +182,6 @@
* child that is deactivated will add activated children instead, unless the deactivated
* node has a custom Enter specified.
*/
-@ExperimentalComposeUiApi
private fun DelegatableNode.collectAccessibleChildren(
accessibleChildren: MutableVector<FocusTargetModifierNode>
) {
@@ -203,7 +198,6 @@
// TODO(b/182319711): For Left/Right focus moves, Consider finding the first candidate in the beam
// and then only comparing candidates in the beam. If nothing is in the beam, then consider all
// valid candidates.
-@ExperimentalComposeUiApi
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
private fun MutableVector<FocusTargetModifierNode>.findBestCandidate(
focusRect: Rect,
@@ -379,7 +373,6 @@
private fun Rect.bottomRight() = Rect(right, bottom, right, bottom)
// Find the active descendant.
-@ExperimentalComposeUiApi
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
private fun FocusTargetModifierNode.activeNode(): FocusTargetModifierNode {
check(focusState == ActiveParent)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index 6117373..cc534a5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -18,7 +18,6 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
@@ -241,7 +240,6 @@
* @param ambientShadowColor see [GraphicsLayerScope.ambientShadowColor]
* @param spotShadowColor see [GraphicsLayerScope.spotShadowColor]
*/
-@OptIn(ExperimentalComposeUiApi::class)
@Deprecated(
"Replace with graphicsLayer that consumes a compositing strategy",
replaceWith = ReplaceWith(
@@ -343,7 +341,6 @@
* @param spotShadowColor see [GraphicsLayerScope.spotShadowColor]
* @param compositingStrategy see [GraphicsLayerScope.compositingStrategy]
*/
-@OptIn(ExperimentalComposeUiApi::class)
@Stable
fun Modifier.graphicsLayer(
scaleX: Float = 1f,
@@ -383,7 +380,6 @@
compositingStrategy
)
-@ExperimentalComposeUiApi
private data class GraphicsLayerModifierNodeElement(
val scaleX: Float,
val scaleY: Float,
@@ -544,7 +540,6 @@
fun Modifier.toolingGraphicsLayer() =
if (isDebugInspectorInfoEnabled) this.then(Modifier.graphicsLayer()) else this
-@OptIn(ExperimentalComposeUiApi::class)
private data class BlockGraphicsLayerElement(
val block: GraphicsLayerScope.() -> Unit
) : ModifierNodeElement<BlockGraphicsLayerModifier>() {
@@ -560,7 +555,6 @@
}
}
-@OptIn(ExperimentalComposeUiApi::class)
private class BlockGraphicsLayerModifier(
var layerBlock: GraphicsLayerScope.() -> Unit,
) : LayoutModifierNode, Modifier.Node() {
@@ -580,7 +574,6 @@
"block=$layerBlock)"
}
-@OptIn(ExperimentalComposeUiApi::class)
private class SimpleGraphicsLayerModifier(
var scaleX: Float,
var scaleY: Float,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
index 8aeb4b9..8521dec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.input.key
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.ModifierNodeElement
@@ -30,7 +29,6 @@
* is called for the focused item. If the event is still not consumed, [onKeyEvent]() is called on
* the focused item's parents.
*/
-@ExperimentalComposeUiApi
interface KeyInputModifierNode : DelegatableNode {
/**
@@ -50,7 +48,6 @@
fun onPreKeyEvent(event: KeyEvent): Boolean
}
-@OptIn(ExperimentalComposeUiApi::class)
internal data class OnKeyEventElement(
val onKeyEvent: (KeyEvent) -> Boolean
) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
@@ -70,7 +67,6 @@
}
}
-@OptIn(ExperimentalComposeUiApi::class)
internal data class OnPreviewKeyEvent(
val onPreviewKeyEvent: (KeyEvent) -> Boolean
) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
@@ -89,7 +85,6 @@
}
}
-@ExperimentalComposeUiApi
internal class KeyInputInputModifierNodeImpl(
var onEvent: ((KeyEvent) -> Boolean)?,
var onPreEvent: ((KeyEvent) -> Boolean)?
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 228d8b2..001261d 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
@@ -30,9 +30,6 @@
import androidx.compose.ui.materialize
import androidx.compose.ui.node.ComposeUiNode
import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
@@ -71,16 +68,12 @@
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
- val density = LocalDensity.current
- val layoutDirection = LocalLayoutDirection.current
- val viewConfiguration = LocalViewConfiguration.current
+ val localMap = currentComposer.currentCompositionLocalMap
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
- set(density, ComposeUiNode.SetDensity)
- set(layoutDirection, ComposeUiNode.SetLayoutDirection)
- set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
+ set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
},
skippableUpdate = materializerOf(modifier),
content = content
@@ -117,17 +110,13 @@
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
- val density = LocalDensity.current
- val layoutDirection = LocalLayoutDirection.current
- val viewConfiguration = LocalViewConfiguration.current
val materialized = currentComposer.materialize(modifier)
+ val localMap = currentComposer.currentCompositionLocalMap
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
- set(density, ComposeUiNode.SetDensity)
- set(layoutDirection, ComposeUiNode.SetLayoutDirection)
- set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
+ set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
set(materialized, ComposeUiNode.SetModifier)
},
)
@@ -207,20 +196,16 @@
measurePolicy: MeasurePolicy
) {
val materialized = currentComposer.materialize(modifier)
- val density = LocalDensity.current
- val layoutDirection = LocalLayoutDirection.current
- val viewConfiguration = LocalViewConfiguration.current
+ val localMap = currentComposer.currentCompositionLocalMap
ReusableComposeNode<LayoutNode, Applier<Any>>(
factory = LayoutNode.Constructor,
update = {
- set(materialized, ComposeUiNode.SetModifier)
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
- set(density, ComposeUiNode.SetDensity)
- set(layoutDirection, ComposeUiNode.SetLayoutDirection)
- set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
+ set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
@Suppress("DEPRECATION")
init { this.canMultiMeasure = true }
+ set(materialized, ComposeUiNode.SetModifier)
},
content = content
)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
index 6a3891b..c51cdcc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutId.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.layout
import androidx.compose.runtime.Stable
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
@@ -33,7 +32,6 @@
@Stable
fun Modifier.layoutId(layoutId: Any) = this then LayoutIdModifierElement(layoutId = layoutId)
-@OptIn(ExperimentalComposeUiApi::class)
private data class LayoutIdModifierElement(
private val layoutId: Any
) : ModifierNodeElement<LayoutIdModifier>() {
@@ -54,7 +52,6 @@
* will act as parent data, and can be used for example by parent layouts to associate
* composable children to [Measurable]s when doing layout, as shown below.
*/
-@OptIn(ExperimentalComposeUiApi::class)
private class LayoutIdModifier(
layoutId: Any,
) : ParentDataModifierNode, LayoutIdParentData, Modifier.Node() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
index a63eee4..1483930 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayout.kt
@@ -29,9 +29,6 @@
import androidx.compose.ui.node.ComposeUiNode
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.NodeCoordinator
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -70,23 +67,18 @@
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
+ val localMap = currentComposer.currentCompositionLocalMap
val materialized = currentComposer.materialize(modifier)
- val density = LocalDensity.current
- val layoutDirection = LocalLayoutDirection.current
- val viewConfiguration = LocalViewConfiguration.current
-
val scope = remember { LookaheadLayoutScopeImpl() }
ReusableComposeNode<LayoutNode, Applier<Any>>(
factory = LayoutNode.Constructor,
update = {
- set(materialized, ComposeUiNode.SetModifier)
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
- set(density, ComposeUiNode.SetDensity)
- set(layoutDirection, ComposeUiNode.SetLayoutDirection)
- set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
+ set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
set(scope) { scope ->
scope.root = innerCoordinator
}
+ set(materialized, ComposeUiNode.SetModifier)
init {
isLookaheadRoot = true
}
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 024ee31..032a313 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
@@ -41,9 +41,6 @@
import androidx.compose.ui.node.LayoutNode.LayoutState
import androidx.compose.ui.node.LayoutNode.UsageByParent
import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.createSubcomposition
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
@@ -108,18 +105,14 @@
) {
val compositionContext = rememberCompositionContext()
val materialized = currentComposer.materialize(modifier)
- val density = LocalDensity.current
- val layoutDirection = LocalLayoutDirection.current
- val viewConfiguration = LocalViewConfiguration.current
+ val localMap = currentComposer.currentCompositionLocalMap
ComposeNode<LayoutNode, Applier<Any>>(
factory = LayoutNode.Constructor,
update = {
set(state, state.setRoot)
set(compositionContext, state.setCompositionContext)
set(measurePolicy, state.setMeasurePolicy)
- set(density, ComposeUiNode.SetDensity)
- set(layoutDirection, ComposeUiNode.SetLayoutDirection)
- set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
+ set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
set(materialized, ComposeUiNode.SetModifier)
}
)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalNode.kt
index 09a5ff1..2d5b125 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalNode.kt
@@ -20,7 +20,6 @@
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.visitAncestors
@@ -30,14 +29,12 @@
*
* @see modifierLocalMapOf
*/
-@ExperimentalComposeUiApi
sealed class ModifierLocalMap() {
internal abstract operator fun <T> set(key: ModifierLocal<T>, value: T)
internal abstract operator fun <T> get(key: ModifierLocal<T>): T?
internal abstract operator fun contains(key: ModifierLocal<*>): Boolean
}
-@OptIn(ExperimentalComposeUiApi::class)
internal class SingleLocalMap(
private val key: ModifierLocal<*>
) : ModifierLocalMap() {
@@ -60,7 +57,6 @@
override operator fun contains(key: ModifierLocal<*>): Boolean = key === this.key
}
-@OptIn(ExperimentalComposeUiApi::class)
internal class BackwardsCompatLocalMap(
var element: ModifierLocalProvider<*>
) : ModifierLocalMap() {
@@ -77,7 +73,6 @@
override operator fun contains(key: ModifierLocal<*>): Boolean = key === element.key
}
-@OptIn(ExperimentalComposeUiApi::class)
internal class MultiLocalMap(
vararg entries: Pair<ModifierLocal<*>, Any?>
) : ModifierLocalMap() {
@@ -99,7 +94,6 @@
override operator fun contains(key: ModifierLocal<*>): Boolean = map.containsKey(key)
}
-@OptIn(ExperimentalComposeUiApi::class)
internal object EmptyMap : ModifierLocalMap() {
override fun <T> set(key: ModifierLocal<T>, value: T) = error("")
override fun <T> get(key: ModifierLocal<T>): T? = error("")
@@ -119,7 +113,6 @@
* @see ModifierLocal
* @see androidx.compose.runtime.CompositionLocal
*/
-@ExperimentalComposeUiApi
interface ModifierLocalNode : ModifierLocalReadScope, DelegatableNode {
/**
* The map of provided ModifierLocal <-> value pairs that this node is providing. This value
@@ -180,13 +173,11 @@
/**
* Creates an empty [ModifierLocalMap]
*/
-@ExperimentalComposeUiApi
fun modifierLocalMapOf(): ModifierLocalMap = EmptyMap
/**
* Creates a [ModifierLocalMap] with a single key and value initialized to null.
*/
-@ExperimentalComposeUiApi
fun <T> modifierLocalMapOf(
key: ModifierLocal<T>
): ModifierLocalMap = SingleLocalMap(key)
@@ -195,7 +186,6 @@
* Creates a [ModifierLocalMap] with a single key and value. The provided [entry] should have
* [Pair::first] be the [ModifierLocal] key, and the [Pair::second] be the corresponding value.
*/
-@ExperimentalComposeUiApi
fun <T> modifierLocalMapOf(
entry: Pair<ModifierLocal<T>, T>
): ModifierLocalMap = SingleLocalMap(entry.first).also { it[entry.first] = entry.second }
@@ -203,7 +193,6 @@
/**
* Creates a [ModifierLocalMap] with several keys, all initialized with values of null
*/
-@ExperimentalComposeUiApi
fun modifierLocalMapOf(
vararg keys: ModifierLocal<*>
): ModifierLocalMap = MultiLocalMap(*keys.map { it to null }.toTypedArray())
@@ -213,7 +202,6 @@
* each item's [Pair::first] be the [ModifierLocal] key, and the [Pair::second] be the
* corresponding value.
*/
-@ExperimentalComposeUiApi
fun modifierLocalMapOf(
vararg entries: Pair<ModifierLocal<*>, Any>
): ModifierLocalMap = MultiLocalMap(*entries)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
index 747c1df..c808fdb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.node
+import androidx.compose.runtime.CompositionLocalMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.platform.ViewConfiguration
@@ -32,6 +33,7 @@
var density: Density
var modifier: Modifier
var viewConfiguration: ViewConfiguration
+ var compositionLocalMap: CompositionLocalMap
/**
* Object of pre-allocated lambdas used to make use with ComposeNode allocation-less.
@@ -41,6 +43,8 @@
val VirtualConstructor: () -> ComposeUiNode = { LayoutNode(isVirtual = true) }
val SetModifier: ComposeUiNode.(Modifier) -> Unit = { this.modifier = it }
val SetDensity: ComposeUiNode.(Density) -> Unit = { this.density = it }
+ val SetResolvedCompositionLocals: ComposeUiNode.(CompositionLocalMap) -> Unit =
+ { this.compositionLocalMap = it }
val SetMeasurePolicy: ComposeUiNode.(MeasurePolicy) -> Unit =
{ this.measurePolicy = it }
val SetLayoutDirection: ComposeUiNode.(LayoutDirection) -> Unit =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt
new file mode 100644
index 0000000..647b2dc
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/CompositionLocalConsumerModifierNode.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.node
+
+import androidx.compose.runtime.CompositionLocal
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+
+/**
+ * Implementing this interface allows your [Modifier.Node] subclass to read
+ * [CompositionLocals][CompositionLocal] via the [currentValueOf] function. The values of each
+ * CompositionLocal will be resolved based on the context of the layout node that the modifier is
+ * attached to, meaning that the modifier will see the same values of each CompositionLocal as its
+ * corresponding layout node.
+ *
+ * @sample androidx.compose.ui.samples.CompositionLocalConsumingModifierSample
+ *
+ * @see Modifier.Node
+ * @see CompositionLocal
+ */
+@ExperimentalComposeUiApi
+interface CompositionLocalConsumerModifierNode : DelegatableNode
+
+/**
+ * Returns the current value of [local] at the position in the composition hierarchy of this
+ * modifier's attached layout node.
+ *
+ * CompositionLocals should only be read with [currentValueOf] during the main phase of your
+ * modifier's operations. This main phase of a modifier is defined as the timeframe of your Modifier
+ * after it has been [attached][Modifier.Node.onAttach] and before it is
+ * [detached][Modifier.Node.onDetach]. The main phase is when you will receive calls to your
+ * modifier's primary hooks like [DrawModifierNode.draw], [LayoutModifierNode.measure],
+ * [PointerInputModifierNode.onPointerEvent], etc. Every callback of a modifier that influences the
+ * composable and is called after `onAttach()` and before `onDetach()` is considered part of the
+ * main phase.
+ *
+ * Unlike [CompositionLocal.current], reads via this function are not automatically tracked by
+ * Compose. Modifiers are not able to recompose in the same way that a Composable can, and therefore
+ * can't receive updates arbitrarily for a CompositionLocal.
+ *
+ * Avoid reading CompositionLocals in [onAttach()][Modifier.Node.onAttach] and
+ * [onDetach()][Modifier.Node.onDetach]. These lifecycle callbacks only happen once, meaning that
+ * any reads in a lifecycle event will yield the value of the CompositionLocal as it was during the
+ * event, and then never again. This can lead to Modifiers using stale CompositionLocal values and
+ * unexpected behaviors in the UI.
+ *
+ * This function will fail with an [IllegalStateException] if you attempt to read a CompositionLocal
+ * before the node is [attached][Modifier.Node.onAttach] or after the node is
+ * [detached][Modifier.Node.onDetach].
+ */
+@ExperimentalComposeUiApi
+fun <T> CompositionLocalConsumerModifierNode.currentValueOf(local: CompositionLocal<T>): T {
+ check(node.isAttached) {
+ "Cannot read CompositionLocal because the Modifier node is not currently attached. Make " +
+ "sure to only invoke currentValueOf() in the main phase of your modifier. See " +
+ "currentValueOf()'s documentation for more information."
+ }
+ return requireLayoutNode().compositionLocalMap[local]
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 6399d1d..bb271ac 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -31,7 +31,6 @@
* @see DelegatingNode.delegated
*/
// TODO(lmr): this interface needs a better name
-@ExperimentalComposeUiApi
interface DelegatableNode {
/**
* A reference of the [Modifier.Node] that holds this node's position in the node hierarchy. If
@@ -46,7 +45,6 @@
// Some internal modifiers, such as Focus, PointerInput, etc. will all need to utilize this
// a bit, but I think we want to avoid giving this power to public API just yet. We can
// introduce this as valid cases arise
-@ExperimentalComposeUiApi
internal fun DelegatableNode.localChild(mask: Int): Modifier.Node? {
val child = node.child ?: return null
if (child.aggregateChildKindSet and mask == 0) return null
@@ -60,7 +58,6 @@
return null
}
-@ExperimentalComposeUiApi
internal fun DelegatableNode.localParent(mask: Int): Modifier.Node? {
var next = node.parent
while (next != null) {
@@ -72,7 +69,6 @@
return null
}
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitAncestors(mask: Int, block: (Modifier.Node) -> Unit) {
// TODO(lmr): we might want to add some safety wheels to prevent this from being called
// while one of the chains is being diffed / updated. Although that might only be
@@ -95,7 +91,6 @@
}
}
-@ExperimentalComposeUiApi
internal fun DelegatableNode.ancestors(mask: Int): List<Modifier.Node>? {
check(node.isAttached)
var ancestors: MutableList<Modifier.Node>? = null
@@ -118,7 +113,6 @@
return ancestors
}
-@ExperimentalComposeUiApi
internal fun DelegatableNode.nearestAncestor(mask: Int): Modifier.Node? {
check(node.isAttached)
var node: Modifier.Node? = node.parent
@@ -139,7 +133,6 @@
return null
}
-@ExperimentalComposeUiApi
internal fun DelegatableNode.firstChild(mask: Int): Modifier.Node? {
check(node.isAttached)
val branches = mutableVectorOf<Modifier.Node>()
@@ -166,7 +159,6 @@
return null
}
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
// TODO(lmr): we might want to add some safety wheels to prevent this from being called
// while one of the chains is being diffed / updated.
@@ -203,7 +195,6 @@
}
}
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
check(node.isAttached)
val branches = mutableVectorOf<Modifier.Node>()
@@ -234,7 +225,6 @@
* visit the shallow tree of children of a given mask, but if block returns true, we will continue
* traversing below it
*/
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
check(node.isAttached)
val branches = mutableVectorOf<Modifier.Node>()
@@ -259,7 +249,6 @@
}
}
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitLocalChildren(mask: Int, block: (Modifier.Node) -> Unit) {
check(node.isAttached)
val self = node
@@ -273,7 +262,6 @@
}
}
-@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitLocalParents(mask: Int, block: (Modifier.Node) -> Unit) {
check(node.isAttached)
var next = node.parent
@@ -285,7 +273,6 @@
}
}
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitLocalChildren(
type: NodeKind<T>,
block: (T) -> Unit
@@ -293,7 +280,6 @@
if (it is T) block(it)
}
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitLocalParents(
type: NodeKind<T>,
block: (T) -> Unit
@@ -301,57 +287,46 @@
if (it is T) block(it)
}
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.localParent(type: NodeKind<T>): T? =
localParent(type.mask) as? T
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.localChild(type: NodeKind<T>): T? =
localChild(type.mask) as? T
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitAncestors(
type: NodeKind<T>,
block: (T) -> Unit
) = visitAncestors(type.mask) { if (it is T) block(it) }
@Suppress("UNCHECKED_CAST") // Type info lost due to erasure.
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.ancestors(
type: NodeKind<T>
): List<T>? = ancestors(type.mask) as? List<T>
-@ExperimentalComposeUiApi
internal inline fun <reified T : Any> DelegatableNode.nearestAncestor(type: NodeKind<T>): T? =
nearestAncestor(type.mask) as? T
-@ExperimentalComposeUiApi
internal inline fun <reified T : Any> DelegatableNode.firstChild(type: NodeKind<T>): T? =
firstChild(type.mask) as? T
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitSubtree(
type: NodeKind<T>,
block: (T) -> Unit
) = visitSubtree(type.mask) { if (it is T) block(it) }
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitChildren(
type: NodeKind<T>,
block: (T) -> Unit
) = visitChildren(type.mask) { if (it is T) block(it) }
-@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
type: NodeKind<T>,
block: (T) -> Boolean
) = visitSubtreeIf(type.mask) { if (it is T) block(it) else true }
-@ExperimentalComposeUiApi
internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
node.aggregateChildKindSet and type.mask != 0
-@ExperimentalComposeUiApi
internal fun DelegatableNode.requireCoordinator(kind: NodeKind<*>): NodeCoordinator {
val coordinator = node.coordinator!!
return if (coordinator.tail !== this)
@@ -362,27 +337,23 @@
coordinator
}
-@ExperimentalComposeUiApi
internal fun DelegatableNode.requireLayoutNode(): LayoutNode =
checkNotNull(node.coordinator) {
"Cannot obtain node coordinator. Is the Modifier.Node attached?"
}.layoutNode
-@ExperimentalComposeUiApi
internal fun DelegatableNode.requireOwner(): Owner = checkNotNull(requireLayoutNode().owner)
/**
* Returns the current [Density] of the LayoutNode that this [DelegatableNode] is attached to.
* If the node is not attached, this function will throw an [IllegalStateException].
*/
-@ExperimentalComposeUiApi
fun DelegatableNode.requireDensity(): Density = requireLayoutNode().density
/**
* Returns the current [LayoutDirection] of the LayoutNode that this [DelegatableNode] is attached
* to. If the node is not attached, this function will throw an [IllegalStateException].
*/
-@ExperimentalComposeUiApi
fun DelegatableNode.requireLayoutDirection(): LayoutDirection = requireLayoutNode().layoutDirection
/**
@@ -392,7 +363,6 @@
* entire subtree to relayout and redraw instead of just parts that
* are otherwise invalidated. Its use should be limited to structural changes.
*/
-@ExperimentalComposeUiApi
fun DelegatableNode.invalidateSubtree() {
if (node.isAttached) {
requireLayoutNode().invalidateSubtree()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 8bbe3a8..d8ae34d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
/**
@@ -28,7 +27,6 @@
*
* @see DelegatingNode
*/
-@ExperimentalComposeUiApi
abstract class DelegatingNode : Modifier.Node() {
override fun updateCoordinator(coordinator: NodeCoordinator?) {
super.updateCoordinator(coordinator)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
index e633f82..99cd8b2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -28,7 +27,6 @@
*
* @sample androidx.compose.ui.samples.DrawModifierNodeSample
*/
-@ExperimentalComposeUiApi
interface DrawModifierNode : DelegatableNode {
fun ContentDrawScope.draw()
fun onMeasureResultChanged() {}
@@ -38,7 +36,6 @@
* Invalidates this modifier's draw layer, ensuring that a draw pass will
* be run on the next frame.
*/
-@ExperimentalComposeUiApi
fun DrawModifierNode.invalidateDraw() {
if (node.isAttached) {
requireCoordinator(Nodes.Any).invalidateLayer()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/GlobalPositionAwareModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/GlobalPositionAwareModifierNode.kt
index 4248b5a..cc280a3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/GlobalPositionAwareModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/GlobalPositionAwareModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
@@ -34,7 +33,6 @@
*
* @see LayoutCoordinates
*/
-@ExperimentalComposeUiApi
interface GlobalPositionAwareModifierNode : DelegatableNode {
/**
* Called with the final LayoutCoordinates of the Layout after measuring.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
index 8c795e9..d165da8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutAwareModifierNode.kt
@@ -37,7 +37,6 @@
* @sample androidx.compose.ui.samples.OnPlaced
* @sample androidx.compose.ui.samples.LayoutAwareModifierNodeSample
*/
-@ExperimentalComposeUiApi
interface LayoutAwareModifierNode : DelegatableNode {
/**
* [onPlaced] is called after the parent [LayoutModifier] and parent layout has
@@ -55,6 +54,7 @@
* [LookaheadLayoutCoordinates.localLookaheadPositionOf] and
* [LookaheadLayoutCoordinates.localPositionOf], respectively.
*/
+ @ExperimentalComposeUiApi
fun onLookaheadPlaced(coordinates: LookaheadLayoutCoordinates) {}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
index a68a547..1c39356 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.layout.AlignmentLine
@@ -46,7 +45,6 @@
*
* @see androidx.compose.ui.layout.Layout
*/
-@ExperimentalComposeUiApi
interface LayoutModifierNode : Remeasurement, DelegatableNode {
// NOTE(lmr): i guess RemeasurementModifier was created because there are some use
// cases where we want to call forceRemeasure but we don't want to implement MeasureNode.
@@ -132,7 +130,6 @@
* This will invalidate the current node's layer, and ensure that the layer is redrawn for the next
* frame.
*/
-@ExperimentalComposeUiApi
fun LayoutModifierNode.invalidateLayer() =
requireCoordinator(Nodes.Layout).invalidateLayer()
@@ -140,17 +137,14 @@
* This will invalidate the current node's layout pass, and ensure that relayout of this node will
* happen for the next frame.
*/
-@ExperimentalComposeUiApi
fun LayoutModifierNode.invalidateLayout() = requireLayoutNode().requestRelayout()
/**
* This invalidates the current node's measure result, and ensures that a remeasurement of this node
* will happen for the next frame.
*/
-@ExperimentalComposeUiApi
fun LayoutModifierNode.invalidateMeasurements() = requireLayoutNode().invalidateMeasurements()
-@ExperimentalComposeUiApi
internal fun LayoutModifierNode.requestRemeasure() = requireLayoutNode().requestRemeasure()
/**
@@ -158,12 +152,10 @@
* looking ahead. During measure pass, [measure] will be invoked with the constraints from the
* look-ahead, as well as the target size.
*/
-@ExperimentalComposeUiApi
interface IntermediateLayoutModifierNode : LayoutModifierNode {
var targetSize: IntSize
}
-@ExperimentalComposeUiApi
private object NodeMeasuringIntrinsics {
internal fun minWidth(
node: LayoutModifierNode,
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 317e511..f2a7b8e 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
@@ -16,6 +16,7 @@
package androidx.compose.ui.node
import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.CompositionLocalMap
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -47,6 +48,9 @@
import androidx.compose.ui.node.Nodes.FocusEvent
import androidx.compose.ui.node.Nodes.FocusProperties
import androidx.compose.ui.node.Nodes.FocusTarget
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.generateSemanticsId
@@ -603,6 +607,17 @@
}
override var viewConfiguration: ViewConfiguration = DummyViewConfiguration
+ override var compositionLocalMap = CompositionLocalMap.Empty
+ set(value) {
+ field = value
+ density = value[LocalDensity]
+ layoutDirection = value[LocalLayoutDirection]
+ viewConfiguration = value[LocalViewConfiguration]
+ @OptIn(ExperimentalComposeUiApi::class)
+ nodes.headToTail(Nodes.CompositionLocalConsumer) { modifierNode ->
+ autoInvalidateUpdatedNode(modifierNode as Modifier.Node)
+ }
+ }
private fun onDensityOrLayoutDirectionChanged() {
// TODO(b/242120396): it seems like we need to update some densities in the node coordinators here
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
index 463e516..311cb3c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.InspectorInfo
@@ -37,7 +36,6 @@
* @see Modifier.Node
* @see Modifier.Element
*/
-@ExperimentalComposeUiApi
abstract class ModifierNodeElement<N : Modifier.Node> : Modifier.Element, InspectableValue {
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index c57a109..4785506 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -93,6 +93,9 @@
inline val KeyInput get() = NodeKind<KeyInputModifierNode>(0b1 shl 13)
@JvmStatic
inline val RotaryInput get() = NodeKind<RotaryInputModifierNode>(0b1 shl 14)
+ @JvmStatic
+ inline val CompositionLocalConsumer
+ get() = NodeKind<CompositionLocalConsumerModifierNode>(0b1 shl 15)
// ...
}
@@ -187,6 +190,9 @@
if (node is RotaryInputModifierNode) {
mask = mask or Nodes.RotaryInput
}
+ if (node is CompositionLocalConsumerModifierNode) {
+ mask = mask or Nodes.CompositionLocalConsumer
+ }
return mask
}
@@ -248,7 +254,6 @@
}
}
-@ExperimentalComposeUiApi
private fun FocusPropertiesModifierNode.scheduleInvalidationOfAssociatedFocusTargets() {
visitChildren(Nodes.FocusTarget) {
// Schedule invalidation for the focus target,
@@ -266,7 +271,6 @@
* called from the main thread, but if this changes in the future, replace the
* [CanFocusChecker.reset] call with a new [FocusProperties] object for every invocation.
*/
-@ExperimentalComposeUiApi
private fun FocusPropertiesModifierNode.specifiesCanFocusProperty(): Boolean {
CanFocusChecker.reset()
modifyFocusProperties(CanFocusChecker)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
index 9d884bc..207df54 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
/**
@@ -24,7 +23,6 @@
* [onObservedReadsChanged] that will be called whenever the value of read object has changed.
* To trigger [onObservedReadsChanged], read values within an [observeReads] block.
*/
-@ExperimentalComposeUiApi
interface ObserverNode : DelegatableNode {
/**
@@ -34,7 +32,6 @@
fun onObservedReadsChanged()
}
-@OptIn(ExperimentalComposeUiApi::class)
internal class ModifierNodeOwnerScope(internal val observerNode: ObserverNode) : OwnerScope {
override val isValidOwnerScope: Boolean
get() = observerNode.node.isAttached
@@ -49,7 +46,6 @@
/**
* Use this function to observe reads within the specified [block].
*/
-@ExperimentalComposeUiApi
fun <T> T.observeReads(block: () -> Unit) where T : Modifier.Node, T : ObserverNode {
val target = ownerScope ?: ModifierNodeOwnerScope(this).also { ownerScope = it }
requireOwner().snapshotObserver.observeReads(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 671f19c..cd21be2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
+import kotlin.coroutines.CoroutineContext
/**
* Owner implements the connection to the underlying view system. On Android, this connects
@@ -269,6 +270,11 @@
val modifierLocalManager: ModifierLocalManager
/**
+ * CoroutineContext for launching coroutines in Modifier Nodes.
+ */
+ val coroutineContext: CoroutineContext
+
+ /**
* Registers a call to be made when the [Applier.onEndChanges] is called. [listener]
* should be called in [onEndApplyChanges] and then removed after being called.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
index 22eec13..950f087 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Layout
@@ -31,7 +30,6 @@
* This is the [androidx.compose.ui.Modifier.Node] equivalent of
* [androidx.compose.ui.layout.ParentDataModifier]
*/
-@ExperimentalComposeUiApi
interface ParentDataModifierNode : DelegatableNode {
/**
* Provides a parentData, given the [parentData] already provided through the modifier's chain.
@@ -43,5 +41,4 @@
* This invalidates the current node's parent data, and ensures that layouts that utilize it will be
* scheduled to relayout for the next frame.
*/
-@ExperimentalComposeUiApi
fun ParentDataModifierNode.invalidateParentData() = requireLayoutNode().invalidateParentData()
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputModifierNode.kt
index bf12ea6..deb97c5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
@@ -33,7 +32,6 @@
*
* @sample androidx.compose.ui.samples.PointerInputModifierNodeSample
*/
-@ExperimentalComposeUiApi
interface PointerInputModifierNode : DelegatableNode {
/**
* Invoked when pointers that previously hit this [PointerInputModifierNode] have changed. It is
@@ -84,10 +82,8 @@
fun sharePointerInputWithSiblings(): Boolean = false
}
-@OptIn(ExperimentalComposeUiApi::class)
internal val PointerInputModifierNode.isAttached: Boolean
get() = node.isAttached
-@OptIn(ExperimentalComposeUiApi::class)
internal val PointerInputModifierNode.layoutCoordinates: LayoutCoordinates
get() = requireCoordinator(Nodes.PointerInput)
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 d83da1e..cd01e8b 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
@@ -16,7 +16,6 @@
package androidx.compose.ui.node
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInRoot
@@ -31,7 +30,6 @@
* This is the [androidx.compose.ui.Modifier.Node] equivalent of
* [androidx.compose.ui.semantics.SemanticsModifier]
*/
-@ExperimentalComposeUiApi
interface SemanticsModifierNode : DelegatableNode {
/**
* The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
@@ -40,10 +38,8 @@
val semanticsConfiguration: SemanticsConfiguration
}
-@ExperimentalComposeUiApi
fun SemanticsModifierNode.invalidateSemantics() = requireOwner().onSemanticsChange()
-@ExperimentalComposeUiApi
fun SemanticsModifierNode.collapsedSemanticsConfiguration(): SemanticsConfiguration {
val next = localChild(Nodes.Semantics)
if (next == null || semanticsConfiguration.isClearingSemantics) {
@@ -55,11 +51,9 @@
return config
}
-@OptIn(ExperimentalComposeUiApi::class)
internal val SemanticsModifierNode.useMinimumTouchTarget: Boolean
get() = semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
-@OptIn(ExperimentalComposeUiApi::class)
internal fun SemanticsModifierNode.touchBoundsInRoot(): Rect {
if (!node.isAttached) {
return Rect.Zero
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
index 35bacae..f654184 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
@@ -143,12 +143,11 @@
* This is a low-level API for code that talks directly to the platform input method framework.
* Higher-level text input APIs in the Foundation library are more appropriate for most cases.
*/
-@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-@ExperimentalTextApi
-@get:ExperimentalTextApi
+// Experimental in desktop.
+@OptIn(ExperimentalTextApi::class)
val LocalPlatformTextInputPluginRegistry =
staticCompositionLocalOf<PlatformTextInputPluginRegistry> {
- error("No PlatformTextInputServiceProvider provided")
+ error("No PlatformTextInputPluginRegistry provided")
}
/**
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 e4dde74..4c340c7 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,7 +47,6 @@
val semanticsConfiguration: SemanticsConfiguration
}
-@ExperimentalComposeUiApi
internal object EmptySemanticsModifierNodeElement :
ModifierNodeElement<CoreSemanticsModifierNode>() {
@@ -113,7 +112,6 @@
)
// Implement SemanticsModifier to allow tooling to inspect the semantics configuration
-@ExperimentalComposeUiApi
internal data class AppendedSemanticsModifierNodeElement(
override val semanticsConfiguration: SemanticsConfiguration
) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
@@ -164,7 +162,6 @@
): Modifier = this then ClearAndSetSemanticsModifierNodeElement(properties)
// Implement SemanticsModifier to allow tooling to inspect the semantics configuration
-@ExperimentalComposeUiApi
internal data class ClearAndSetSemanticsModifierNodeElement(
override val semanticsConfiguration: SemanticsConfiguration
) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 1bd632b..ac6025d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -273,6 +273,11 @@
val SetText = ActionPropertyKey<(AnnotatedString) -> Boolean>("SetText")
/**
+ * @see SemanticsPropertyReceiver.performImeAction
+ */
+ val PerformImeAction = ActionPropertyKey<() -> Boolean>("PerformImeAction")
+
+ /**
* @see SemanticsPropertyReceiver.copyText
*/
val CopyText = ActionPropertyKey<() -> Boolean>("CopyText")
@@ -867,6 +872,9 @@
* Contains the IME action provided by the node.
*
* For example, "go to next form field" or "submit".
+ *
+ * A node that specifies an action should also specify a callback to perform the action via
+ * [performImeAction].
*/
var SemanticsPropertyReceiver.imeAction by SemanticsProperties.ImeAction
@@ -1021,7 +1029,7 @@
* Expected to be used on editable text fields.
*
* @param label Optional label for this action.
- * @param action Action to be performed when the [SemanticsActions.SetText] is called.
+ * @param action Action to be performed when [SemanticsActions.SetText] is called.
*/
fun SemanticsPropertyReceiver.setText(
label: String? = null,
@@ -1031,6 +1039,24 @@
}
/**
+ * Action to invoke the IME action handler configured on the node.
+ *
+ * Expected to be used on editable text fields.
+ *
+ * A node that specifies an action callback should also report what IME action it will perform via
+ * the [imeAction] property.
+ *
+ * @param label Optional label for this action.
+ * @param action Action to be performed when [SemanticsActions.PerformImeAction] is called.
+ */
+fun SemanticsPropertyReceiver.performImeAction(
+ label: String? = null,
+ action: (() -> Boolean)?
+) {
+ this[SemanticsActions.PerformImeAction] = AccessibilityAction(label, action)
+}
+
+/**
* Action to set text selection by character index range.
*
* If this action is provided, the selection data must be provided
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
index 2ae01ef..7934126 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
@@ -172,6 +172,7 @@
platformInputService = scene.platformInputService,
component = scene.component,
density = density,
+ coroutineContext = parentComposition.effectCoroutineContext,
isPopup = true,
isFocusable = focusable,
onDismissRequest = onDismissRequest,
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
index ccd6561..6f092a6 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
@@ -276,9 +276,10 @@
composition?.dispose()
mainOwner?.dispose()
val mainOwner = SkiaBasedOwner(
- platformInputService,
- component,
- density,
+ platformInputService = platformInputService,
+ component = component,
+ density = density,
+ coroutineContext = recomposer.effectCoroutineContext,
onPreviewKeyEvent = onPreviewKeyEvent,
onKeyEvent = onKeyEvent
)
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index 2a83806..f48d7bc 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -85,6 +85,7 @@
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.input.PlatformTextInputPluginRegistryImpl
+import kotlin.coroutines.CoroutineContext
private typealias Command = () -> Unit
@@ -98,6 +99,7 @@
private val platformInputService: PlatformInput,
private val component: PlatformComponent,
density: Density = Density(1f, 1f),
+ coroutineContext: CoroutineContext,
val isPopup: Boolean = false,
val isFocusable: Boolean = true,
val onDismissRequest: (() -> Unit)? = null,
@@ -185,6 +187,8 @@
.onKeyEvent(onKeyEvent)
}
+ override val coroutineContext: CoroutineContext = coroutineContext
+
override val rootForTest = this
override val snapshotObserver = OwnerSnapshotObserver { command ->
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 0957ca6..95e5dc6 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
@@ -76,6 +76,9 @@
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.zIndex
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -2496,7 +2499,9 @@
@OptIn(InternalCoreApi::class)
internal class MockOwner(
val position: IntOffset = IntOffset.Zero,
- override val root: LayoutNode = LayoutNode()
+ override val root: LayoutNode = LayoutNode(),
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
) : Owner {
val onRequestMeasureParams = mutableListOf<LayoutNode>()
val onAttachParams = mutableListOf<LayoutNode>()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 6643267..639263a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -50,6 +50,9 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -295,6 +298,8 @@
override val snapshotObserver: OwnerSnapshotObserver = OwnerSnapshotObserver { it.invoke() }
override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
+ override val coroutineContext: CoroutineContext =
+ Executors.newFixedThreadPool(3).asCoroutineDispatcher()
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
listeners += listener
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
index fdc6605..57d277d 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
@@ -18,6 +18,8 @@
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.constraintlayout.core.parser.CLArray
import androidx.constraintlayout.core.parser.CLContainer
import androidx.constraintlayout.core.parser.CLNumber
@@ -241,9 +243,10 @@
protected fun <T> addOnPropertyChange(initialValue: T, nameOverride: String? = null) =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
- val name = nameOverride ?: property.name
if (newValue != null) {
- keyFramePropertiesValue[name] = newValue
+ keyFramePropertiesValue[nameOverride ?: property.name] = newValue
+ } else {
+ keyFramePropertiesValue.remove(nameOverride ?: property.name)
}
}
}
@@ -306,6 +309,9 @@
end = stringChars.size.toLong() - 1
})
}
+ is Dp -> {
+ array.add(CLNumber(value.value))
+ }
is Number -> {
array.add(CLNumber(value.toFloat()))
}
@@ -322,9 +328,9 @@
var rotationX by addOnPropertyChange(0f, "rotationX")
var rotationY by addOnPropertyChange(0f, "rotationY")
var rotationZ by addOnPropertyChange(0f, "rotationZ")
- var translationX by addOnPropertyChange(0f, "translationX")
- var translationY by addOnPropertyChange(0f, "translationY")
- var translationZ by addOnPropertyChange(0f, "translationZ")
+ var translationX: Dp by addOnPropertyChange(0.dp, "translationX")
+ var translationY: Dp by addOnPropertyChange(0.dp, "translationY")
+ var translationZ: Dp by addOnPropertyChange(0.dp, "translationZ")
}
@ExperimentalMotionApi
@@ -344,9 +350,9 @@
var rotationX by addOnPropertyChange(0f)
var rotationY by addOnPropertyChange(0f)
var rotationZ by addOnPropertyChange(0f)
- var translationX by addOnPropertyChange(0f)
- var translationY by addOnPropertyChange(0f)
- var translationZ by addOnPropertyChange(0f)
+ var translationX: Dp by addOnPropertyChange(0.dp)
+ var translationY: Dp by addOnPropertyChange(0.dp)
+ var translationZ: Dp by addOnPropertyChange(0.dp)
var period by addOnPropertyChange(0f)
var offset by addOnPropertyChange(0f)
var phase by addOnPropertyChange(0f)
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index 3dd9159..59d5bb3 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -41,7 +41,6 @@
androidx {
name = "AndroidX bitmap scaling Sample"
- // type = LibraryType.SAMPLES
inceptionYear = "2021"
description = "Sample for the AndoridX graphics bitmap compatibility"
}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index a185de2..3521e78 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -278,6 +278,7 @@
method @RequiresApi(19) public static boolean getUsesChronometer(android.app.Notification);
method public static int getVisibility(android.app.Notification);
method public static boolean isGroupSummary(android.app.Notification);
+ method public static android.graphics.Bitmap? reduceLargeIconSize(android.content.Context, android.graphics.Bitmap?);
field public static final int BADGE_ICON_LARGE = 2; // 0x2
field public static final int BADGE_ICON_NONE = 0; // 0x0
field public static final int BADGE_ICON_SMALL = 1; // 0x1
@@ -559,6 +560,7 @@
method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(int);
method public androidx.core.app.NotificationCompat.Builder setGroupSummary(boolean);
method public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.Bitmap?);
+ method @RequiresApi(23) public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.drawable.Icon?);
method public androidx.core.app.NotificationCompat.Builder setLights(@ColorInt int, int, int);
method public androidx.core.app.NotificationCompat.Builder setLocalOnly(boolean);
method public androidx.core.app.NotificationCompat.Builder setLocusId(androidx.core.content.LocusIdCompat?);
@@ -1079,6 +1081,7 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
+ method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 2c115f6..eb3d438 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -278,6 +278,7 @@
method @RequiresApi(19) public static boolean getUsesChronometer(android.app.Notification);
method public static int getVisibility(android.app.Notification);
method public static boolean isGroupSummary(android.app.Notification);
+ method public static android.graphics.Bitmap? reduceLargeIconSize(android.content.Context, android.graphics.Bitmap?);
field public static final int BADGE_ICON_LARGE = 2; // 0x2
field public static final int BADGE_ICON_NONE = 0; // 0x0
field public static final int BADGE_ICON_SMALL = 1; // 0x1
@@ -559,6 +560,7 @@
method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(int);
method public androidx.core.app.NotificationCompat.Builder setGroupSummary(boolean);
method public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.Bitmap?);
+ method @RequiresApi(23) public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.drawable.Icon?);
method public androidx.core.app.NotificationCompat.Builder setLights(@ColorInt int, int, int);
method public androidx.core.app.NotificationCompat.Builder setLocalOnly(boolean);
method public androidx.core.app.NotificationCompat.Builder setLocusId(androidx.core.content.LocusIdCompat?);
@@ -1079,6 +1081,7 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
+ method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index f67e3fb..a15cb01 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -324,6 +324,7 @@
method @RequiresApi(19) public static boolean getUsesChronometer(android.app.Notification);
method @androidx.core.app.NotificationCompat.NotificationVisibility public static int getVisibility(android.app.Notification);
method public static boolean isGroupSummary(android.app.Notification);
+ method public static android.graphics.Bitmap? reduceLargeIconSize(android.content.Context, android.graphics.Bitmap?);
field public static final int BADGE_ICON_LARGE = 2; // 0x2
field public static final int BADGE_ICON_NONE = 0; // 0x0
field public static final int BADGE_ICON_SMALL = 1; // 0x1
@@ -620,6 +621,7 @@
method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(@androidx.core.app.NotificationCompat.GroupAlertBehavior int);
method public androidx.core.app.NotificationCompat.Builder setGroupSummary(boolean);
method public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.Bitmap?);
+ method @RequiresApi(23) public androidx.core.app.NotificationCompat.Builder setLargeIcon(android.graphics.drawable.Icon?);
method public androidx.core.app.NotificationCompat.Builder setLights(@ColorInt int, int, int);
method public androidx.core.app.NotificationCompat.Builder setLocalOnly(boolean);
method public androidx.core.app.NotificationCompat.Builder setLocusId(androidx.core.content.LocusIdCompat?);
@@ -1194,6 +1196,7 @@
ctor protected FileProvider(@XmlRes int);
method public int delete(android.net.Uri, String?, String![]?);
method public String? getType(android.net.Uri);
+ method public String? getTypeAnonymous(android.net.Uri);
method public static android.net.Uri! getUriForFile(android.content.Context, String, java.io.File);
method public static android.net.Uri getUriForFile(android.content.Context, String, java.io.File, String);
method public android.net.Uri! insert(android.net.Uri, android.content.ContentValues);
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 5ffca2f..c8fb297 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -47,6 +47,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.media.AudioManager;
@@ -1139,6 +1140,162 @@
}
@Test
+ public void testSetNotification_setLargeIconNull() {
+ Notification n = new NotificationCompat.Builder(mContext, "channelId")
+ .setSmallIcon(1)
+ .setLargeIcon((Bitmap) null)
+ .build();
+
+ // Extras are not populated before API 19.
+ if (Build.VERSION.SDK_INT >= 19) {
+ Bundle extras = NotificationCompat.getExtras(n);
+ assertNotNull(extras);
+ if (Build.VERSION.SDK_INT <= 23) {
+ assertFalse(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ } else {
+ assertTrue(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ assertNull(extras.get(NotificationCompat.EXTRA_LARGE_ICON));
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ assertNull(n.getLargeIcon());
+ }
+ }
+
+ @Test
+ public void testSetNotification_setLargeIconBitmap() {
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ R.drawable.notification_bg_low_pressed);
+ Notification n = new NotificationCompat.Builder(mContext, "channelId")
+ .setSmallIcon(1)
+ .setLargeIcon(bitmap)
+ .build();
+
+ // Extras are not populated before API 19.
+ if (Build.VERSION.SDK_INT >= 19) {
+ Bundle extras = NotificationCompat.getExtras(n);
+ assertNotNull(extras);
+ assertTrue(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ assertNotNull(extras.get(NotificationCompat.EXTRA_LARGE_ICON));
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ assertNotNull(n.getLargeIcon());
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void testSetNotification_setLargeIconNullIcon() {
+ Notification n = new NotificationCompat.Builder(mContext, "channelId")
+ .setSmallIcon(1)
+ .setLargeIcon((Icon) null)
+ .build();
+
+ assertNull(n.getLargeIcon());
+
+ Bundle extras = NotificationCompat.getExtras(n);
+ assertNotNull(extras);
+ // Prior to API version 24, EXTRA_LARGE_ICON was not set if largeIcon was set to null.
+ // Starting in version 24, EXTRA_LARGE_ICON is set, but its value is null.
+ // Note that extras are not populated before API 19, but this test's minSdkVersion is 23,
+ // so we don't have to check that.
+ if (Build.VERSION.SDK_INT <= 23) {
+ assertFalse(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ } else {
+ assertTrue(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ assertNull(extras.get(NotificationCompat.EXTRA_LARGE_ICON));
+ }
+
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ public void testSetNotification_setLargeIconIcon() {
+ IconCompat iconCompat = IconCompat.createWithResource(mContext,
+ R.drawable.notification_bg_low_pressed);
+ Icon icon = iconCompat.toIcon(mContext);
+
+ Notification n = new NotificationCompat.Builder(mContext, "channelId")
+ .setSmallIcon(1)
+ .setLargeIcon(icon)
+ .build();
+
+ Bundle extras = NotificationCompat.getExtras(n);
+ assertNotNull(extras);
+ assertTrue(extras.containsKey(NotificationCompat.EXTRA_LARGE_ICON));
+ assertNotNull(extras.get(NotificationCompat.EXTRA_LARGE_ICON));
+ if (Build.VERSION.SDK_INT >= 28) {
+ assertEquals(n.getLargeIcon().getResId(), icon.getResId());
+ Icon recoveredIcon = extras.getParcelable(NotificationCompat.EXTRA_LARGE_ICON);
+ assertEquals(icon.getResId(), recoveredIcon.getResId());
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 26)
+ @Test
+ public void testSetNotification_setLargeIconBitmapScales() {
+ // Original icon is 860x860
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ R.drawable.notification_oversize_large_icon_bg);
+
+ Notification n = new NotificationCompat.Builder(mContext, "channelId")
+ .setSmallIcon(1)
+ .setLargeIcon(bitmap)
+ .build();
+
+ Icon recoveredIcon = n.getLargeIcon();
+ Drawable drawable = recoveredIcon.loadDrawable(mContext);
+ // Scale has reduced its height and width.
+ assertTrue(drawable.getIntrinsicHeight() < 860);
+ assertTrue(drawable.getIntrinsicWidth() < 860);
+ }
+
+ @Test
+ public void testReduceLargeIconSize_nullIcon() {
+ assertNull(NotificationCompat.reduceLargeIconSize(mContext, null));
+ }
+
+ @SdkSuppress(minSdkVersion = 27)
+ @Test
+ public void testReduceLargeIconSize_doesNotResizeInModernVersions() {
+ // We expect the function to do nothing for API 27 and higher, where scaling is not needed.
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ R.drawable.notification_oversize_large_icon_bg);
+ assertEquals(bitmap, NotificationCompat.reduceLargeIconSize(mContext, bitmap));
+ }
+
+ @SdkSuppress(maxSdkVersion = 26)
+ @Test
+ public void testReduceLargeIconSize() {
+ // Original icon is 860x860; set inScaled to false to validate the unscaled size.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ R.drawable.notification_oversize_large_icon_bg, options);
+
+ assertEquals(860, bitmap.getWidth());
+ assertEquals(860, bitmap.getHeight());
+
+ // In the case that the bitmap is larger than the max allowable width or height, we expect
+ // reduceLargeIconSize scales it down.
+ // Because each device sets this differently, we only want to test the expectation that
+ // the size is reduced on the devices where it's appropriate.
+ int maxWidth =
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.compat_notification_large_icon_max_width);
+ int maxHeight =
+ mContext.getResources().getDimensionPixelSize(
+ R.dimen.compat_notification_large_icon_max_height);
+ if (maxWidth < 860 || maxHeight < 860) {
+ // We don't check the exact size because it varies based on the device scaling factor.
+ Bitmap newBitmap = NotificationCompat.reduceLargeIconSize(mContext, bitmap);
+ assertTrue(newBitmap.getWidth() < 860);
+ assertTrue(newBitmap.getHeight() < 860);
+ }
+ }
+
+ @Test
public void testSetNotificationSilent() {
Notification nSummary = new NotificationCompat.Builder(mActivityTestRule.getActivity())
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 473949b..1aeaaf3 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -1034,7 +1034,7 @@
PendingIntent mContentIntent;
PendingIntent mFullScreenIntent;
RemoteViews mTickerView;
- Bitmap mLargeIcon;
+ IconCompat mLargeIcon;
CharSequence mContentInfo;
int mNumber;
int mPriority;
@@ -1589,39 +1589,25 @@
}
/**
- * Set the large icon that is shown in the notification.
+ * Sets the large icon that is shown in the notification. Icons will be scaled on versions
+ * before API 27. Starting in API 27, the framework does this automatically.
*/
public @NonNull Builder setLargeIcon(@Nullable Bitmap icon) {
- mLargeIcon = reduceLargeIconSize(icon);
+ mLargeIcon = icon == null ? null : IconCompat.createWithBitmap(
+ reduceLargeIconSize(mContext, icon));
return this;
}
/**
- * Reduce the size of a notification icon if it's overly large. The framework does
- * this automatically starting from API 27.
+ * Sets the large icon that is shown in the notification. Starting in API 27, the framework
+ * scales icons automatically. Before API 27, for safety, {@code #reduceLargeIconSize}
+ * should be called on bitmaps before putting them in an {@code Icon} and passing them
+ * into this function.
*/
- private @Nullable Bitmap reduceLargeIconSize(@Nullable Bitmap icon) {
- if (icon == null || Build.VERSION.SDK_INT >= 27) {
- return icon;
- }
-
- Resources res = mContext.getResources();
- int maxWidth =
- res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
- int maxHeight =
- res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
- if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
- return icon;
- }
-
- double scale = Math.min(
- maxWidth / (double) Math.max(1, icon.getWidth()),
- maxHeight / (double) Math.max(1, icon.getHeight()));
- return Bitmap.createScaledBitmap(
- icon,
- (int) Math.ceil(icon.getWidth() * scale),
- (int) Math.ceil(icon.getHeight() * scale),
- true /* filtered */);
+ @RequiresApi(23)
+ public @NonNull Builder setLargeIcon(@Nullable Icon icon) {
+ mLargeIcon = icon == null ? null : IconCompat.createFromIcon(icon);
+ return this;
}
/**
@@ -2995,7 +2981,8 @@
// to hide it here.
if (Build.VERSION.SDK_INT >= 16) {
contentView.setViewVisibility(R.id.icon, View.VISIBLE);
- contentView.setImageViewBitmap(R.id.icon, mBuilder.mLargeIcon);
+ contentView.setImageViewBitmap(R.id.icon,
+ createColoredBitmap(mBuilder.mLargeIcon, Color.TRANSPARENT));
} else {
contentView.setViewVisibility(R.id.icon, View.GONE);
}
@@ -3044,7 +3031,8 @@
showLine3 = true;
}
// If there is a large icon we have a right side
- boolean hasRightSide = !(Build.VERSION.SDK_INT >= 21) && mBuilder.mLargeIcon != null;
+ boolean hasRightSide =
+ !(Build.VERSION.SDK_INT >= 21) && mBuilder.mLargeIcon != null;
if (mBuilder.mContentInfo != null) {
contentView.setTextViewText(R.id.info, mBuilder.mContentInfo);
contentView.setViewVisibility(R.id.info, View.VISIBLE);
@@ -9602,6 +9590,36 @@
}
}
+ /**
+ * Reduces the size of a provided {@code icon} if it's larger than the maximum allowed
+ * for a notification large icon; returns the resized icon. Note that the framework does this
+ * scaling automatically starting from API 27.
+ */
+ public static @Nullable Bitmap reduceLargeIconSize(@NonNull Context context,
+ @Nullable Bitmap icon) {
+ if (icon == null || Build.VERSION.SDK_INT >= 27) {
+ return icon;
+ }
+
+ Resources res = context.getResources();
+ int maxWidth =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+ int maxHeight =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
+ if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
+ return icon;
+ }
+
+ double scale = Math.min(
+ maxWidth / (double) Math.max(1, icon.getWidth()),
+ maxHeight / (double) Math.max(1, icon.getHeight()));
+ return Bitmap.createScaledBitmap(
+ icon,
+ (int) Math.ceil(icon.getWidth() * scale),
+ (int) Math.ceil(icon.getHeight() * scale),
+ true /* filtered */);
+ }
+
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index a0181d8..72af80f 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -98,9 +98,14 @@
.setDeleteIntent(n.deleteIntent)
.setFullScreenIntent(b.mFullScreenIntent,
(n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
- .setLargeIcon(b.mLargeIcon)
.setNumber(b.mNumber)
.setProgress(b.mProgressMax, b.mProgress, b.mProgressIndeterminate);
+ if (Build.VERSION.SDK_INT < 23) {
+ mBuilder.setLargeIcon(b.mLargeIcon == null ? null : b.mLargeIcon.getBitmap());
+ } else {
+ Api23Impl.setLargeIcon(mBuilder,
+ b.mLargeIcon == null ? null : b.mLargeIcon.toIcon(mContext));
+ }
if (Build.VERSION.SDK_INT < 21) {
mBuilder.setSound(n.sound, n.audioStreamType);
}
@@ -733,6 +738,11 @@
Object icon /** Icon **/) {
return builder.setSmallIcon((Icon) icon);
}
+
+ @DoNotInline
+ static Notification.Builder setLargeIcon(Notification.Builder builder, Icon icon) {
+ return builder.setLargeIcon(icon);
+ }
}
/**
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 95df1f8..58ce929 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -570,6 +570,17 @@
}
/**
+ * Unrestricted version of getType
+ * called, when caller does not have corresponding permissions
+ */
+ //@Override
+ @SuppressWarnings("MissingOverride")
+ @Nullable
+ public String getTypeAnonymous(@NonNull Uri uri) {
+ return "application/octet-stream";
+ }
+
+ /**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
diff --git a/core/core/src/main/res/drawable-hdpi/notification_oversize_large_icon_bg.png b/core/core/src/main/res/drawable-hdpi/notification_oversize_large_icon_bg.png
new file mode 100644
index 0000000..383433d
--- /dev/null
+++ b/core/core/src/main/res/drawable-hdpi/notification_oversize_large_icon_bg.png
Binary files differ
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 88305ed..0ccc2b2 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -6,6 +6,8 @@
}
public abstract class CreateCredentialRequest {
+ method public final String? getOrigin();
+ property public final String? origin;
}
public static final class CreateCredentialRequest.DisplayInfo {
@@ -21,6 +23,7 @@
}
public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
method public final android.os.Bundle getCandidateQueryData();
@@ -44,6 +47,7 @@
}
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
method public String getId();
method public String getPassword();
@@ -56,36 +60,18 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
- ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
method public String getRegistrationResponseJson();
@@ -134,9 +120,12 @@
}
public final class GetCredentialRequest {
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ method public String? getOrigin();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ property public final String? origin;
}
public static final class GetCredentialRequest.Builder {
@@ -144,6 +133,7 @@
method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
method public androidx.credentials.GetCredentialRequest build();
method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+ method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
}
public final class GetCredentialResponse {
@@ -175,36 +165,17 @@
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
- ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class PasswordCredential extends androidx.credentials.Credential {
ctor public PasswordCredential(String id, String password);
method public String getId();
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index 88305ed..0ccc2b2 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -6,6 +6,8 @@
}
public abstract class CreateCredentialRequest {
+ method public final String? getOrigin();
+ property public final String? origin;
}
public static final class CreateCredentialRequest.DisplayInfo {
@@ -21,6 +23,7 @@
}
public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
method public final android.os.Bundle getCandidateQueryData();
@@ -44,6 +47,7 @@
}
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
method public String getId();
method public String getPassword();
@@ -56,36 +60,18 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
- ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
method public String getRegistrationResponseJson();
@@ -134,9 +120,12 @@
}
public final class GetCredentialRequest {
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ method public String? getOrigin();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ property public final String? origin;
}
public static final class GetCredentialRequest.Builder {
@@ -144,6 +133,7 @@
method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
method public androidx.credentials.GetCredentialRequest build();
method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+ method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
}
public final class GetCredentialResponse {
@@ -175,36 +165,17 @@
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
- ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class PasswordCredential extends androidx.credentials.Credential {
ctor public PasswordCredential(String id, String password);
method public String getId();
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 88305ed..0ccc2b2 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -6,6 +6,8 @@
}
public abstract class CreateCredentialRequest {
+ method public final String? getOrigin();
+ property public final String? origin;
}
public static final class CreateCredentialRequest.DisplayInfo {
@@ -21,6 +23,7 @@
}
public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
method public final android.os.Bundle getCandidateQueryData();
@@ -44,6 +47,7 @@
}
public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
+ ctor public CreatePasswordRequest(String id, String password, optional String? origin);
ctor public CreatePasswordRequest(String id, String password);
method public String getId();
method public String getPassword();
@@ -56,36 +60,18 @@
}
public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public CreatePublicKeyCredentialRequest(String requestJson, optional String? clientDataHash);
ctor public CreatePublicKeyCredentialRequest(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
- ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
method public String getRegistrationResponseJson();
@@ -134,9 +120,12 @@
}
public final class GetCredentialRequest {
+ ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+ method public String? getOrigin();
property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
+ property public final String? origin;
}
public static final class GetCredentialRequest.Builder {
@@ -144,6 +133,7 @@
method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
method public androidx.credentials.GetCredentialRequest build();
method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
+ method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
}
public final class GetCredentialResponse {
@@ -175,36 +165,17 @@
}
public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
+ ctor public GetPublicKeyCredentialOption(String requestJson, optional String? clientDataHash);
ctor public GetPublicKeyCredentialOption(String requestJson);
+ method public String? getClientDataHash();
method public boolean getPreferImmediatelyAvailableCredentials();
method public String getRequestJson();
+ property public final String? clientDataHash;
property public final boolean preferImmediatelyAvailableCredentials;
property public final String requestJson;
}
- public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.CredentialOption {
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
- ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
- method public String getClientDataHash();
- method public boolean getPreferImmediatelyAvailableCredentials();
- method public String getRelyingParty();
- method public String getRequestJson();
- property public final String clientDataHash;
- property public final boolean preferImmediatelyAvailableCredentials;
- property public final String relyingParty;
- property public final String requestJson;
- }
-
- public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
- ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
- method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
- }
-
public final class PasswordCredential extends androidx.credentials.Credential {
ctor public PasswordCredential(String id, String password);
method public String getId();
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
index c07e895..d002fbd 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -127,7 +127,8 @@
CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
request.getType(), getFinalCreateCredentialData(
request, mContext),
- request.getCandidateQueryData(), request.isSystemProviderRequired()
+ request.getCandidateQueryData(), request.isSystemProviderRequired(),
+ request.getOrigin()
);
assertThat(convertedRequest).isInstanceOf(CreatePasswordRequest.class);
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 0588e533..6aeb65d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -103,12 +103,14 @@
fun frameworkConversion_success() {
val idExpected = "id"
val request = CreatePasswordRequest(idExpected, "password")
+ val origin = "origin"
val convertedRequest = createFrom(
request.type, getFinalCreateCredentialData(
request, mContext
),
- request.candidateQueryData, request.isSystemProviderRequired
+ request.candidateQueryData, request.isSystemProviderRequired,
+ origin
)
assertThat(convertedRequest).isInstanceOf(
@@ -121,5 +123,6 @@
assertThat(convertedCreatePasswordRequest.displayInfo.userId).isEqualTo(idExpected)
assertThat(convertedCreatePasswordRequest.displayInfo.credentialTypeIcon?.resId)
.isEqualTo(R.drawable.ic_password)
+ assertThat(convertedCreatePasswordRequest.origin).isEqualTo(origin)
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index f70ebed..d3af566 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -93,8 +93,10 @@
@Test
public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
boolean preferImmediatelyAvailableCredentialsExpected = true;
+ String clientDataHash = "hash";
CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON,
+ clientDataHash,
preferImmediatelyAvailableCredentialsExpected);
boolean preferImmediatelyAvailableCredentialsActual =
createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials();
@@ -117,6 +119,7 @@
@Test
public void getter_frameworkProperties_success() {
String requestJsonExpected = TEST_REQUEST_JSON;
+ String clientDataHash = "hash";
boolean preferImmediatelyAvailableCredentialsExpected = false;
Bundle expectedData = new Bundle();
expectedData.putString(
@@ -125,6 +128,8 @@
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST);
expectedData.putString(
BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+ expectedData.putString(CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash);
expectedData.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected);
@@ -135,7 +140,7 @@
expectedQuery.remove(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED);
CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
- requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
+ requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected);
assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
assertThat(TestUtilsKt.equals(request.getCandidateQueryData(), expectedQuery)).isTrue();
@@ -164,13 +169,15 @@
@SdkSuppress(minSdkVersion = 28)
@Test
public void frameworkConversion_success() {
+ String clientDataHash = "hash";
CreatePublicKeyCredentialRequest request =
- new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, true);
+ new CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, clientDataHash, true);
CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
request.getType(), getFinalCreateCredentialData(
request, mContext),
- request.getCandidateQueryData(), request.isSystemProviderRequired()
+ request.getCandidateQueryData(), request.isSystemProviderRequired(),
+ request.getOrigin()
);
assertThat(convertedRequest).isInstanceOf(CreatePublicKeyCredentialRequest.class);
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
deleted file mode 100644
index 7ef9840..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials;
-
-import static org.junit.Assert.assertThrows;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Arrays;
-
-/**
- * Combines with {@link CreatePublicKeyCredentialRequestPrivilegedJavaTest} for full tests.
- */
-@RunWith(Parameterized.class)
-public class CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest {
- private String mRequestJson;
- private String mRp;
- private String mClientDataHash;
-
- private String mNullRequestJson;
- private String mNullRp;
- private String mNullClientDataHash;
-
- public CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest(String requestJson,
- String rp, String clientDataHash, String mNullRequestJson, String mNullRp,
- String mNullClientDataHash) {
- this.mRequestJson = requestJson;
- this.mRp = rp;
- this.mClientDataHash = clientDataHash;
- this.mNullRequestJson = mNullRequestJson;
- this.mNullRp = mNullRp;
- this.mNullClientDataHash = mNullClientDataHash;
- }
-
- @Parameterized.Parameters
- public static Iterable<String[]> failureCases() {
- // Allows checking improper formations for builder and normal construction.
- // Handles both null and empty cases.
- // For successful cases, see the non parameterized tests.
- return Arrays.asList(new String[][] {
- { "{\"hi\":21}", "rp", "", null, "rp", "hash"},
- { "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
- { "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
- });
- }
-
- @Test
- public void constructor_emptyInput_throwsIllegalArgumentException() {
- assertThrows("If at least one arg empty, should throw IllegalArgumentException",
- IllegalArgumentException.class,
- () -> new CreatePublicKeyCredentialRequestPrivileged(this.mRequestJson, this.mRp,
- this.mClientDataHash)
- );
- }
-
- @Test
- public void builder_build_emptyInput_IllegalArgumentException() {
- CreatePublicKeyCredentialRequestPrivileged.Builder builder =
- new CreatePublicKeyCredentialRequestPrivileged.Builder(mRequestJson, mRp,
- mClientDataHash);
- assertThrows("If at least one arg empty to builder, should throw "
- + "IllegalArgumentException",
- IllegalArgumentException.class,
- () -> builder.build()
- );
- }
-
- @Test
- public void constructor_nullInput_throwsNullPointerException() {
- convertAPIIssueToProperNull();
- assertThrows("If at least one arg null, should throw NullPointerException",
- NullPointerException.class,
- () -> new CreatePublicKeyCredentialRequestPrivileged(this.mNullRequestJson,
- this.mNullRp,
- this.mNullClientDataHash)
- );
- }
-
- @Test
- public void builder_build_nullInput_throwsNullPointerException() {
- convertAPIIssueToProperNull();
- assertThrows(
- "If at least one arg null to builder, should throw NullPointerException",
- NullPointerException.class,
- () -> new CreatePublicKeyCredentialRequestPrivileged.Builder(mNullRequestJson,
- mNullRp, mNullClientDataHash).build()
- );
- }
-
- // Certain API levels have parameterized tests that automatically convert null to a string
- // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
- // workaround. Note this is *not* always the case but only for certain API levels (we have
- // recorded 21, 22, and 23 as such levels).
- private void convertAPIIssueToProperNull() {
- if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
- mNullRequestJson = null;
- }
- if (mNullRp != null && mNullRp.equals("null")) {
- mNullRp = null;
- }
- if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
- mNullClientDataHash = null;
- }
- }
-}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsTest.kt
deleted file mode 100644
index 7708b22..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsTest.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import androidx.testutils.assertThrows
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * Combines with [CreatePublicKeyCredentialRequestPrivilegedTest] for full tests.
- */
-@RunWith(Parameterized::class)
-class CreatePublicKeyCredentialRequestPrivilegedFailureInputsTest(
- val requestJson: String,
- val rp: String,
- val clientDataHash: String
- ) {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters
- fun failureCases(): Collection<Array<Any>> {
- return listOf(
- arrayOf("{\"hi\":21}", "rp", ""),
- arrayOf("{\"hi\":21}", "", "clientDataHash"),
- arrayOf("", "rp", "clientDataHash")
- ) // coverage is complete, null is not a problem in Kotlin.
- }
- }
-
- @Test
- fun constructor_emptyInput_throwsIllegalArgumentException() {
- assertThrows<IllegalArgumentException> {
- CreatePublicKeyCredentialRequestPrivileged(requestJson, rp, clientDataHash)
- }
- }
-
- @Test
- fun builder_build_emptyInput_throwsIllegalArgumentException() {
- var builder = CreatePublicKeyCredentialRequestPrivileged.Builder(requestJson,
- rp, clientDataHash)
- assertThrows<IllegalArgumentException> {
- builder.build()
- }
- }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
deleted file mode 100644
index 692cc50..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials;
-
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
-import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
-import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
-import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY;
-import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON;
-import static androidx.credentials.internal.FrameworkImplHelper.getFinalCreateCredentialData;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.graphics.drawable.Icon;
-import android.os.Bundle;
-
-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.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Combines with {@link CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest}
- * for full tests.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class CreatePublicKeyCredentialRequestPrivilegedJavaTest {
- private static final String TEST_USERNAME = "[email protected]";
- private static final String TEST_USER_DISPLAYNAME = "Test User";
- private static final String TEST_REQUEST_JSON = String.format("{\"rp\":{\"name\":true,"
- + "\"id\":\"app-id\"},\"user\":{\"name\":\"%s\",\"id\":\"id-value\","
- + "\"displayName\":\"%s\",\"icon\":true}, \"challenge\":true,"
- + "\"pubKeyCredParams\":true,\"excludeCredentials\":true,"
- + "\"attestation\":true}", TEST_USERNAME,
- TEST_USER_DISPLAYNAME);
- private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
-
- @Test
- public void constructor_success() {
- new CreatePublicKeyCredentialRequestPrivileged(
- "{\"user\":{\"name\":{\"lol\":\"Value\"}}}",
- "relyingParty", "ClientDataHash");
- }
-
- @Test
- public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
- new CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "relyingParty", "HASH");
- boolean preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
- }
-
- @Test
- public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
- boolean preferImmediatelyAvailableCredentialsExpected = true;
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
- new CreatePublicKeyCredentialRequestPrivileged(TEST_REQUEST_JSON,
- "relyingParty",
- "HASH",
- preferImmediatelyAvailableCredentialsExpected);
- boolean preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected);
- }
-
- @Test
- public void builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
- CreatePublicKeyCredentialRequestPrivileged defaultPrivilegedRequest = new
- CreatePublicKeyCredentialRequestPrivileged.Builder(TEST_REQUEST_JSON,
- "relyingParty", "HASH").build();
- assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
- }
-
- @Test
- public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
- boolean preferImmediatelyAvailableCredentialsExpected = true;
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
- new CreatePublicKeyCredentialRequestPrivileged.Builder(TEST_REQUEST_JSON,
- "relyingParty", "HASH")
- .setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentialsExpected).build();
- boolean preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected);
- }
-
- @Test
- public void getter_requestJson_success() {
- String testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}";
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialReqPriv =
- new CreatePublicKeyCredentialRequestPrivileged(testJsonExpected,
- "relyingParty", "HASH");
- String testJsonActual = createPublicKeyCredentialReqPriv.getRequestJson();
- assertThat(testJsonActual).isEqualTo(testJsonExpected);
- }
-
- @Test
- public void getter_relyingParty_success() {
- String testRelyingPartyExpected = "relyingParty";
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
- new CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, testRelyingPartyExpected, "X342%4dfd7&");
- String testRelyingPartyActual = createPublicKeyCredentialRequestPrivileged
- .getRelyingParty();
- assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected);
- }
-
- @Test
- public void getter_clientDataHash_success() {
- String clientDataHashExpected = "X342%4dfd7&";
- CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
- new CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "relyingParty", clientDataHashExpected);
- String clientDataHashActual =
- createPublicKeyCredentialRequestPrivileged.getClientDataHash();
- assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
- }
-
- @SdkSuppress(minSdkVersion = 28)
- @SuppressWarnings("deprecation") // bundle.get(key)
- @Test
- public void getter_frameworkProperties_success() {
- String requestJsonExpected = TEST_REQUEST_JSON;
- String relyingPartyExpected = "relyingParty";
- String clientDataHashExpected = "X342%4dfd7&";
- boolean preferImmediatelyAvailableCredentialsExpected = false;
- boolean expectedAutoSelect = true;
- Bundle expectedData = new Bundle();
- expectedData.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- CreatePublicKeyCredentialRequestPrivileged
- .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV);
- expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
- expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
- expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
- expectedData.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected);
- expectedData.putBoolean(
- BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- expectedAutoSelect);
- Bundle expectedCandidateQuery = expectedData.deepCopy();
- expectedCandidateQuery.remove(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED);
-
- CreatePublicKeyCredentialRequestPrivileged request =
- new CreatePublicKeyCredentialRequestPrivileged(
- requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
- preferImmediatelyAvailableCredentialsExpected);
-
- assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
- assertThat(TestUtilsKt.equals(request.getCandidateQueryData(),
- expectedCandidateQuery)).isTrue();
- assertThat(request.isSystemProviderRequired()).isFalse();
- Bundle credentialData = getFinalCreateCredentialData(
- request, mContext);
- assertThat(credentialData.keySet())
- .hasSize(expectedData.size() + /* added request info */ 1);
- for (String key : expectedData.keySet()) {
- assertThat(credentialData.get(key)).isEqualTo(credentialData.get(key));
- }
- Bundle displayInfoBundle =
- credentialData.getBundle(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO);
- assertThat(displayInfoBundle.keySet()).hasSize(3);
- assertThat(displayInfoBundle.getString(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID)).isEqualTo(TEST_USERNAME);
- assertThat(displayInfoBundle.getString(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME)).isEqualTo(
- TEST_USER_DISPLAYNAME);
- assertThat(((Icon) (displayInfoBundle.getParcelable(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON))).getResId()
- ).isEqualTo(R.drawable.ic_passkey);
- }
-
- @SdkSuppress(minSdkVersion = 28)
- @Test
- public void frameworkConversion_success() {
- CreatePublicKeyCredentialRequestPrivileged request =
- new CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "rp", "clientDataHash", true);
-
- CreateCredentialRequest convertedRequest = CreateCredentialRequest.createFrom(
- request.getType(), getFinalCreateCredentialData(
- request, mContext),
- request.getCandidateQueryData(), request.isSystemProviderRequired()
- );
-
- assertThat(convertedRequest).isInstanceOf(CreatePublicKeyCredentialRequestPrivileged.class);
- CreatePublicKeyCredentialRequestPrivileged convertedSubclassRequest =
- (CreatePublicKeyCredentialRequestPrivileged) convertedRequest;
- assertThat(convertedSubclassRequest.getRequestJson()).isEqualTo(request.getRequestJson());
- assertThat(convertedSubclassRequest.getRelyingParty()).isEqualTo(request.getRelyingParty());
- assertThat(convertedSubclassRequest.getClientDataHash())
- .isEqualTo(request.getClientDataHash());
- assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials()).isEqualTo(
- request.preferImmediatelyAvailableCredentials());
- CreateCredentialRequest.DisplayInfo displayInfo =
- convertedRequest.getDisplayInfo();
- assertThat(displayInfo.getUserDisplayName()).isEqualTo(TEST_USER_DISPLAYNAME);
- assertThat(displayInfo.getUserId()).isEqualTo(TEST_USERNAME);
- assertThat(displayInfo.getCredentialTypeIcon().getResId())
- .isEqualTo(R.drawable.ic_passkey);
- }
-}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
deleted file mode 100644
index 29dd13a..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import android.graphics.drawable.Icon
-import android.os.Bundle
-import android.os.Parcelable
-import androidx.credentials.CreateCredentialRequest.Companion.createFrom
-import androidx.credentials.internal.FrameworkImplHelper.Companion.getFinalCreateCredentialData
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Combines with [CreatePublicKeyCredentialRequestPrivilegedFailureInputsTest] for full tests.
- */
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class CreatePublicKeyCredentialRequestPrivilegedTest {
- private val mContext = InstrumentationRegistry.getInstrumentation().context
-
- companion object Constant {
- private const val TEST_USERNAME = "[email protected]"
- private const val TEST_USER_DISPLAYNAME = "Test User"
- private const val TEST_REQUEST_JSON = "{\"rp\":{\"name\":true,\"id\":\"app-id\"}," +
- "\"user\":{\"name\":\"$TEST_USERNAME\",\"id\":\"id-value\",\"displayName" +
- "\":\"$TEST_USER_DISPLAYNAME\",\"icon\":true}, \"challenge\":true," +
- "\"pubKeyCredParams\":true,\"excludeCredentials\":true," + "\"attestation\":true}"
- }
-
- @Test
- fun constructor_success() {
- CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "RelyingParty", "ClientDataHash"
- )
- }
-
- @Test
- fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "RelyingParty", "HASH"
- )
- val preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
- }
-
- @Test
- fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
- val preferImmediatelyAvailableCredentialsExpected = true
- val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON,
- "RelyingParty",
- "Hash",
- preferImmediatelyAvailableCredentialsExpected
- )
- val preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected
- )
- }
-
- @Test
- fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
- val defaultPrivilegedRequest = CreatePublicKeyCredentialRequestPrivileged.Builder(
- TEST_REQUEST_JSON, "RelyingParty", "HASH"
- ).build()
- assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
- }
-
- @Test
- fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
- val preferImmediatelyAvailableCredentialsExpected = true
- val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged
- .Builder(
- TEST_REQUEST_JSON, "RelyingParty", "Hash"
- )
- .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
- .build()
- val preferImmediatelyAvailableCredentialsActual =
- createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected
- )
- }
-
- @Test
- fun getter_requestJson_success() {
- val testJsonExpected = "{\"user\":{\"name\":{\"lol\":\"Value\"}}}"
- val createPublicKeyCredentialReqPriv =
- CreatePublicKeyCredentialRequestPrivileged(
- testJsonExpected, "RelyingParty",
- "HASH"
- )
- val testJsonActual = createPublicKeyCredentialReqPriv.requestJson
- assertThat(testJsonActual).isEqualTo(testJsonExpected)
- }
-
- @Test
- fun getter_relyingParty_success() {
- val testRelyingPartyExpected = "RelyingParty"
- val createPublicKeyCredentialRequestPrivileged =
- CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, testRelyingPartyExpected, "X342%4dfd7&"
- )
- val testRelyingPartyActual = createPublicKeyCredentialRequestPrivileged.relyingParty
- assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected)
- }
-
- @Test
- fun getter_clientDataHash_success() {
- val clientDataHashExpected = "X342%4dfd7&"
- val createPublicKeyCredentialRequestPrivileged =
- CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "RelyingParty", clientDataHashExpected
- )
- val clientDataHashActual = createPublicKeyCredentialRequestPrivileged.clientDataHash
- assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected)
- }
-
- @SdkSuppress(minSdkVersion = 28)
- @Suppress("DEPRECATION") // bundle.get(key)
- @Test
- fun getter_frameworkProperties_success() {
- val requestJsonExpected = TEST_REQUEST_JSON
- val relyingPartyExpected = "RelyingParty"
- val clientDataHashExpected = "X342%4dfd7&"
- val preferImmediatelyAvailableCredentialsExpected = false
- val expectedAutoSelect = true
- val expectedData = Bundle()
- expectedData.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- CreatePublicKeyCredentialRequestPrivileged
- .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
- )
- expectedData.putString(
- CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON,
- requestJsonExpected
- )
- expectedData.putString(
- CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY,
- relyingPartyExpected
- )
- expectedData.putString(
- CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
- clientDataHashExpected
- )
- expectedData.putBoolean(
- CreateCredentialRequest.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- expectedAutoSelect
- )
- expectedData.putBoolean(
- CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected
- )
- val expectedCandidateQuery = expectedData.deepCopy()
- expectedCandidateQuery.remove(CreateCredentialRequest
- .BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
-
- val request = CreatePublicKeyCredentialRequestPrivileged(
- requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
- preferImmediatelyAvailableCredentialsExpected
- )
-
- assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
- assertThat(
- equals(
- request.candidateQueryData,
- expectedCandidateQuery
- )
- ).isTrue()
- assertThat(request.isSystemProviderRequired).isFalse()
- val credentialData = getFinalCreateCredentialData(
- request, mContext
- )
- assertThat(credentialData.keySet())
- .hasSize(expectedData.size() + /* added request info */1)
- for (key in expectedData.keySet()) {
- assertThat(credentialData[key]).isEqualTo(credentialData[key])
- }
- val displayInfoBundle = credentialData.getBundle(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_REQUEST_DISPLAY_INFO
- )!!
- assertThat(displayInfoBundle.keySet()).hasSize(3)
- assertThat(
- displayInfoBundle.getString(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_ID
- )
- ).isEqualTo(TEST_USERNAME)
- assertThat(
- displayInfoBundle.getString(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_USER_DISPLAY_NAME
- )
- ).isEqualTo(TEST_USER_DISPLAYNAME)
- assertThat(
- (displayInfoBundle.getParcelable<Parcelable>(
- CreateCredentialRequest.DisplayInfo.BUNDLE_KEY_CREDENTIAL_TYPE_ICON
- ) as Icon?)!!.resId
- ).isEqualTo(R.drawable.ic_passkey)
- }
-
- @SdkSuppress(minSdkVersion = 28)
- @Test
- fun frameworkConversion_success() {
- val request = CreatePublicKeyCredentialRequestPrivileged(
- TEST_REQUEST_JSON, "rp", "clientDataHash", true
- )
-
- val convertedRequest = createFrom(
- request.type, getFinalCreateCredentialData(
- request, mContext
- ),
- request.candidateQueryData, request.isSystemProviderRequired
- )
-
- assertThat(convertedRequest).isInstanceOf(
- CreatePublicKeyCredentialRequestPrivileged::class.java
- )
- val convertedSubclassRequest =
- convertedRequest as CreatePublicKeyCredentialRequestPrivileged
- assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
- assertThat(convertedSubclassRequest.relyingParty).isEqualTo(request.relyingParty)
- assertThat(convertedSubclassRequest.clientDataHash)
- .isEqualTo(request.clientDataHash)
- assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
- .isEqualTo(request.preferImmediatelyAvailableCredentials)
- val displayInfo = convertedRequest.displayInfo
- assertThat(displayInfo.userDisplayName).isEqualTo(TEST_USER_DISPLAYNAME)
- assertThat(displayInfo.userId).isEqualTo(TEST_USERNAME)
- assertThat(displayInfo.credentialTypeIcon?.resId)
- .isEqualTo(R.drawable.ic_passkey)
- }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index b405022..538da6a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -73,14 +73,19 @@
@Test
fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
val preferImmediatelyAvailableCredentialsExpected = true
+ val origin = "origin"
+ val clientDataHash = "hash"
val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
TEST_REQUEST_JSON,
- preferImmediatelyAvailableCredentialsExpected
+ clientDataHash,
+ preferImmediatelyAvailableCredentialsExpected,
+ origin
)
val preferImmediatelyAvailableCredentialsActual =
createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
assertThat(preferImmediatelyAvailableCredentialsActual)
.isEqualTo(preferImmediatelyAvailableCredentialsExpected)
+ assertThat(createPublicKeyCredentialRequest.origin).isEqualTo(origin)
}
@Test
@@ -98,6 +103,8 @@
val requestJsonExpected = TEST_REQUEST_JSON
val preferImmediatelyAvailableCredentialsExpected = false
val expectedAutoSelect = true
+ val origin = "origin"
+ val clientDataHash = "hash"
val expectedData = Bundle()
expectedData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -107,6 +114,8 @@
expectedData.putString(
BUNDLE_KEY_REQUEST_JSON, requestJsonExpected
)
+ expectedData.putString(CreatePublicKeyCredentialRequest.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash)
expectedData.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected
@@ -123,12 +132,15 @@
val request = CreatePublicKeyCredentialRequest(
requestJsonExpected,
- preferImmediatelyAvailableCredentialsExpected
+ clientDataHash,
+ preferImmediatelyAvailableCredentialsExpected,
+ origin
)
assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
assertThat(equals(request.candidateQueryData, expectedCandidateQueryBundle)).isTrue()
assertThat(request.isSystemProviderRequired).isFalse()
+ assertThat(request.origin).isEqualTo(origin)
val credentialData = getFinalCreateCredentialData(
request, mContext
)
@@ -161,18 +173,23 @@
@SdkSuppress(minSdkVersion = 28)
@Test
fun frameworkConversion_success() {
- val request = CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, true)
+ val origin = "origin"
+ val clientDataHash = "hash"
+ val request = CreatePublicKeyCredentialRequest(TEST_REQUEST_JSON, clientDataHash,
+ true, origin)
val convertedRequest = createFrom(
request.type, getFinalCreateCredentialData(
request, mContext
),
- request.candidateQueryData, request.isSystemProviderRequired
+ request.candidateQueryData, request.isSystemProviderRequired,
+ request.origin
)
assertThat(convertedRequest).isInstanceOf(
CreatePublicKeyCredentialRequest::class.java
)
+ assertThat(convertedRequest?.origin).isEqualTo(origin)
val convertedSubclassRequest = convertedRequest as CreatePublicKeyCredentialRequest
assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
index d162aac..fafee67 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
@@ -42,9 +42,11 @@
val expectedCredentialOptions = ArrayList<CredentialOption>()
expectedCredentialOptions.add(GetPasswordOption())
expectedCredentialOptions.add(GetPublicKeyCredentialOption("json"))
+ val origin = "origin"
val request = GetCredentialRequest(
- expectedCredentialOptions
+ expectedCredentialOptions,
+ origin
)
assertThat(request.credentialOptions).hasSize(expectedCredentialOptions.size)
@@ -53,16 +55,19 @@
expectedCredentialOptions[i]
)
}
+ assertThat(request.origin).isEqualTo(origin)
}
@Test
fun constructor_defaultAutoSelect() {
val options = ArrayList<CredentialOption>()
options.add(GetPasswordOption())
+ val origin = "origin"
- val request = GetCredentialRequest(options)
+ val request = GetCredentialRequest(options, origin)
assertThat(request.credentialOptions[0].isAutoSelectAllowed).isFalse()
+ assertThat(request.origin).isEqualTo(origin)
}
@Test
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
index 507cf8f..da4e145 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
@@ -72,9 +72,10 @@
@Test
public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
boolean preferImmediatelyAvailableCredentialsExpected = true;
+ String clientDataHash = "hash";
GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
new GetPublicKeyCredentialOption(
- "JSON", preferImmediatelyAvailableCredentialsExpected);
+ "JSON", clientDataHash, preferImmediatelyAvailableCredentialsExpected);
boolean preferImmediatelyAvailableCredentialsActual =
getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
@@ -95,18 +96,21 @@
String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
boolean preferImmediatelyAvailableCredentialsExpected = false;
boolean expectedIsAutoSelect = true;
+ String clientDataHash = "hash";
Bundle expectedData = new Bundle();
expectedData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
GetPublicKeyCredentialOption.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+ expectedData.putString(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash);
expectedData.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected);
expectedData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, expectedIsAutoSelect);
GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(
- requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
+ requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected);
assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
@@ -117,8 +121,9 @@
@Test
public void frameworkConversion_success() {
+ String clientDataHash = "hash";
GetPublicKeyCredentialOption option =
- new GetPublicKeyCredentialOption("json", true);
+ new GetPublicKeyCredentialOption("json", clientDataHash, true);
CredentialOption convertedOption = CredentialOption.createFrom(
option.getType(), option.getRequestData(),
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
deleted file mode 100644
index be01f1c..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials;
-
-import static org.junit.Assert.assertThrows;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Arrays;
-
-/**
- * Combines with {@link GetPublicKeyCredentialOptionPrivilegedJavaTest} for full tests.
- */
-@RunWith(Parameterized.class)
-public class GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest {
- private String mRequestJson;
- private String mRp;
- private String mClientDataHash;
-
- private String mNullRequestJson;
- private String mNullRp;
- private String mNullClientDataHash;
-
- public GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest(String requestJson,
- String rp, String clientDataHash, String mNullRequestJson, String mNullRp,
- String mNullClientDataHash) {
- this.mRequestJson = requestJson;
- this.mRp = rp;
- this.mClientDataHash = clientDataHash;
- this.mNullRequestJson = mNullRequestJson;
- this.mNullRp = mNullRp;
- this.mNullClientDataHash = mNullClientDataHash;
- }
-
- @Parameterized.Parameters
- public static Iterable<String[]> failureCases() {
- // Allows checking improper formations for builder and normal construction.
- // Handles both null and empty cases.
- // For successful cases, see the non parameterized privileged tests.
- return Arrays.asList(new String[][] {
- { "{\"hi\":21}", "rp", "", null, "rp", "hash"},
- { "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
- { "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
- });
- }
-
- @Test
- public void constructor_emptyInput_throwsIllegalArgumentException() {
- assertThrows("If at least one arg empty, should throw IllegalArgumentException",
- IllegalArgumentException.class,
- () -> new GetPublicKeyCredentialOptionPrivileged(this.mRequestJson, this.mRp,
- this.mClientDataHash)
- );
- }
-
- @Test
- public void builder_build_emptyInput_IllegalArgumentException() {
- GetPublicKeyCredentialOptionPrivileged.Builder builder =
- new GetPublicKeyCredentialOptionPrivileged.Builder(mRequestJson, mRp,
- mClientDataHash);
- assertThrows("If at least one arg empty to builder, should throw "
- + "IllegalArgumentException",
- IllegalArgumentException.class,
- () -> builder.build()
- );
- }
-
- @Test
- public void constructor_nullInput_throwsNullPointerException() {
- convertAPIIssueToProperNull();
- assertThrows(
- "If at least one arg null, should throw NullPointerException",
- NullPointerException.class,
- () -> new GetPublicKeyCredentialOptionPrivileged(this.mNullRequestJson,
- this.mNullRp,
- this.mNullClientDataHash)
- );
- }
-
- @Test
- public void builder_build_nullInput_throwsNullPointerException() {
- convertAPIIssueToProperNull();
- assertThrows(
- "If at least one arg null to builder, should throw NullPointerException",
- NullPointerException.class,
- () -> new GetPublicKeyCredentialOptionPrivileged.Builder(mNullRequestJson,
- mNullRp, mNullClientDataHash).build()
- );
- }
-
- // Certain API levels have parameterized tests that automatically convert null to a string
- // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
- // workaround. Note this is *not* always the case but only for certain API levels (we have
- // recorded 21, 22, and 23 as such levels).
- private void convertAPIIssueToProperNull() {
- if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
- mNullRequestJson = null;
- }
- if (mNullRp != null && mNullRp.equals("null")) {
- mNullRp = null;
- }
- if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
- mNullClientDataHash = null;
- }
- }
-}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsTest.kt
deleted file mode 100644
index 5490b0c..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsTest.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import androidx.testutils.assertThrows
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * Combines with [GetPublicKeyCredentialOptionPrivilegedTest] for full tests.
- */
-@RunWith(Parameterized::class)
-class GetPublicKeyCredentialOptionPrivilegedFailureInputsTest(
- val requestJson: String,
- val rp: String,
- val clientDataHash: String
- ) {
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters
- fun failureCases(): Collection<Array<Any>> {
- return listOf(
- arrayOf("{\"hi\":21}", "rp", ""),
- arrayOf("{\"hi\":21}", "", "clientDataHash"),
- arrayOf("", "rp", "clientDataHash")
- ) // coverage is complete
- }
- }
-
- @Test
- fun constructor_emptyInput_throwsIllegalArgumentException() {
- assertThrows<IllegalArgumentException> {
- GetPublicKeyCredentialOptionPrivileged(requestJson, rp, clientDataHash)
- }
- }
-
- @Test
- fun builder_build_emptyInput_throwsIllegalArgumentException() {
- var builder = GetPublicKeyCredentialOptionPrivileged.Builder(requestJson,
- rp, clientDataHash)
- assertThrows<IllegalArgumentException> {
- builder.build()
- }
- }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
deleted file mode 100644
index 15bb0cb..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials;
-
-import static androidx.credentials.CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED;
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY;
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Bundle;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Combines with {@link GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest}
- * for full tests.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class GetPublicKeyCredentialOptionPrivilegedJavaTest {
-
- @Test
- public void constructor_success() {
- new GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- "RelyingParty", "ClientDataHash");
- }
-
- @Test
- public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged(
- "JSON", "RelyingParty", "HASH");
- boolean preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
- }
-
- @Test
- public void constructor_setPreferImmediatelyAvailableCredentialsTrue() {
- boolean preferImmediatelyAvailableCredentialsExpected = true;
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged(
- "testJson",
- "RelyingParty",
- "Hash",
- preferImmediatelyAvailableCredentialsExpected
- );
- boolean preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected);
- }
-
- @Test
- public void builder_build_defaultPreferImmediatelyAvailableCredentials_success() {
- GetPublicKeyCredentialOptionPrivileged defaultPrivilegedRequest = new
- GetPublicKeyCredentialOptionPrivileged.Builder("{\"Data\":5}",
- "RelyingParty", "HASH").build();
- assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
- }
-
- @Test
- public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_success() {
- boolean preferImmediatelyAvailableCredentialsExpected = true;
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged.Builder("testJson",
- "RelyingParty", "Hash")
- .setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentialsExpected).build();
- boolean preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected);
- }
-
- @Test
- public void getter_requestJson_success() {
- String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged(testJsonExpected,
- "RelyingParty", "HASH");
- String testJsonActual = getPublicKeyCredentialOptionPrivileged.getRequestJson();
- assertThat(testJsonActual).isEqualTo(testJsonExpected);
- }
-
- @Test
- public void getter_relyingParty_success() {
- String testRelyingPartyExpected = "RelyingParty";
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- testRelyingPartyExpected, "X342%4dfd7&");
- String testRelyingPartyActual = getPublicKeyCredentialOptionPrivileged.getRelyingParty();
- assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected);
- }
-
- @Test
- public void getter_clientDataHash_success() {
- String clientDataHashExpected = "X342%4dfd7&";
- GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
- new GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- "RelyingParty", clientDataHashExpected);
- String clientDataHashActual = getPublicKeyCredentialOptionPrivileged.getClientDataHash();
- assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
- }
-
- @Test
- public void getter_frameworkProperties_success() {
- String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
- String relyingPartyExpected = "RelyingParty";
- String clientDataHashExpected = "X342%4dfd7&";
- boolean preferImmediatelyAvailableCredentialsExpected = false;
- boolean expectedIsAutoSelect = true;
- Bundle expectedData = new Bundle();
- expectedData.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED);
- expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
- expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
- expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
- expectedData.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, expectedIsAutoSelect);
- expectedData.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected);
-
- GetPublicKeyCredentialOptionPrivileged option =
- new GetPublicKeyCredentialOptionPrivileged(
- requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
- preferImmediatelyAvailableCredentialsExpected);
-
- assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
- assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
- expectedData.remove(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED);
- assertThat(TestUtilsKt.equals(option.getCandidateQueryData(), expectedData)).isTrue();
- assertThat(option.isSystemProviderRequired()).isFalse();
- }
-
- @Test
- public void frameworkConversion_success() {
- GetPublicKeyCredentialOptionPrivileged option =
- new GetPublicKeyCredentialOptionPrivileged("json", "rp", "clientDataHash", true);
-
- CredentialOption convertedOption = CredentialOption.createFrom(
- option.getType(), option.getRequestData(),
- option.getCandidateQueryData(), option.isSystemProviderRequired());
-
- assertThat(convertedOption).isInstanceOf(GetPublicKeyCredentialOptionPrivileged.class);
- GetPublicKeyCredentialOptionPrivileged convertedSubclassOption =
- (GetPublicKeyCredentialOptionPrivileged) convertedOption;
- assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
- assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
- option.preferImmediatelyAvailableCredentials());
- assertThat(convertedSubclassOption.getClientDataHash())
- .isEqualTo(option.getClientDataHash());
- assertThat(convertedSubclassOption.getRelyingParty()).isEqualTo(option.getRelyingParty());
- }
-}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
deleted file mode 100644
index b08e938..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import android.os.Bundle
-import androidx.credentials.CredentialOption.Companion.createFrom
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Combines with [GetPublicKeyCredentialOptionPrivilegedFailureInputsTest] for full tests.
- */
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class GetPublicKeyCredentialOptionPrivilegedTest {
-
- @Test
- fun constructor_success() {
- GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- "RelyingParty", "ClientDataHash"
- )
- }
-
- @Test
- fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
- val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged(
- "JSON", "RelyingParty", "HASH"
- )
- val preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
- }
-
- @Test
- fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
- val preferImmediatelyAvailableCredentialsExpected = true
- val getPublicKeyCredentialOptPriv = GetPublicKeyCredentialOptionPrivileged(
- "JSON",
- "RelyingParty",
- "HASH",
- preferImmediatelyAvailableCredentialsExpected
- )
- val preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptPriv.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected
- )
- }
-
- @Test
- fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
- val preferImmediatelyAvailableCredentialsExpected = true
- val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged
- .Builder(
- "testJson",
- "RelyingParty", "Hash",
- )
- .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
- .build()
- val preferImmediatelyAvailableCredentialsActual =
- getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
- assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
- preferImmediatelyAvailableCredentialsExpected
- )
- }
-
- @Test
- fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
- val defaultPrivilegedRequest = GetPublicKeyCredentialOptionPrivileged.Builder(
- "{\"Data\":5}",
- "RelyingParty", "HASH"
- ).build()
- assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
- }
-
- @Test
- fun getter_requestJson_success() {
- val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
- val getPublicKeyCredentialOptionPrivileged =
- GetPublicKeyCredentialOptionPrivileged(
- testJsonExpected, "RelyingParty",
- "HASH"
- )
- val testJsonActual = getPublicKeyCredentialOptionPrivileged.requestJson
- assertThat(testJsonActual).isEqualTo(testJsonExpected)
- }
-
- @Test
- fun getter_relyingParty_success() {
- val testRelyingPartyExpected = "RelyingParty"
- val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- testRelyingPartyExpected, "X342%4dfd7&"
- )
- val testRelyingPartyActual = getPublicKeyCredentialOptionPrivileged.relyingParty
- assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected)
- }
-
- @Test
- fun getter_clientDataHash_success() {
- val clientDataHashExpected = "X342%4dfd7&"
- val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged(
- "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
- "RelyingParty", clientDataHashExpected
- )
- val clientDataHashActual = getPublicKeyCredentialOptionPrivileged.clientDataHash
- assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected)
- }
-
- @Test
- fun getter_frameworkProperties_success() {
- val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
- val relyingPartyExpected = "RelyingParty"
- val clientDataHashExpected = "X342%4dfd7&"
- val preferImmediatelyAvailableCredentialsExpected = false
- val expectedIsAutoSelect = true
- val expectedData = Bundle()
- expectedData.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
- )
- expectedData.putString(
- GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON,
- requestJsonExpected
- )
- expectedData.putString(
- GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY,
- relyingPartyExpected
- )
- expectedData.putString(
- GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
- clientDataHashExpected
- )
- expectedData.putBoolean(CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED,
- expectedIsAutoSelect)
- expectedData.putBoolean(
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentialsExpected
- )
-
- val option = GetPublicKeyCredentialOptionPrivileged(
- requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
- preferImmediatelyAvailableCredentialsExpected
- )
-
- assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
- assertThat(equals(option.requestData, expectedData)).isTrue()
- expectedData.remove(CredentialOption.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED)
- assertThat(equals(option.candidateQueryData, expectedData)).isTrue()
- assertThat(option.isSystemProviderRequired).isFalse()
- }
-
- @Test
- fun frameworkConversion_success() {
- val option = GetPublicKeyCredentialOptionPrivileged("json", "rp", "clientDataHash", true)
-
- val convertedOption = createFrom(
- option.type,
- option.requestData,
- option.candidateQueryData,
- option.isSystemProviderRequired
- )
-
- assertThat(convertedOption).isInstanceOf(
- GetPublicKeyCredentialOptionPrivileged::class.java
- )
- val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOptionPrivileged
- assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
- assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
- .isEqualTo(option.preferImmediatelyAvailableCredentials)
- assertThat(convertedSubclassOption.clientDataHash)
- .isEqualTo(option.clientDataHash)
- assertThat(convertedSubclassOption.relyingParty).isEqualTo(option.relyingParty)
- }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index 9c0a039..a4648fa 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -57,8 +57,9 @@
@Test
fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
val preferImmediatelyAvailableCredentialsExpected = true
+ val clientDataHash = "hash"
val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
- "JSON", preferImmediatelyAvailableCredentialsExpected
+ "JSON", clientDataHash, preferImmediatelyAvailableCredentialsExpected
)
val preferImmediatelyAvailableCredentialsActual =
getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
@@ -79,6 +80,7 @@
val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
val preferImmediatelyAvailableCredentialsExpected = false
val expectedAutoSelectAllowed = true
+ val clientDataHash = "hash"
val expectedData = Bundle()
expectedData.putString(
PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -88,6 +90,8 @@
GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON,
requestJsonExpected
)
+ expectedData.putString(GetPublicKeyCredentialOption.BUNDLE_KEY_CLIENT_DATA_HASH,
+ clientDataHash)
expectedData.putBoolean(
GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentialsExpected
@@ -98,7 +102,7 @@
)
val option = GetPublicKeyCredentialOption(
- requestJsonExpected, preferImmediatelyAvailableCredentialsExpected
+ requestJsonExpected, clientDataHash, preferImmediatelyAvailableCredentialsExpected
)
assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
@@ -111,7 +115,8 @@
@Test
fun frameworkConversion_success() {
- val option = GetPublicKeyCredentialOption("json", true)
+ val clientDataHash = "hash"
+ val option = GetPublicKeyCredentialOption("json", clientDataHash, true)
val convertedOption = createFrom(
option.type,
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index 10589d6..4334cc0 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -22,7 +22,6 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
-import androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Companion.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
import androidx.credentials.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
import androidx.credentials.internal.FrameworkClassParsingException
@@ -51,6 +50,7 @@
open val isAutoSelectAllowed: Boolean,
/** @hide */
val displayInfo: DisplayInfo,
+ val origin: String?,
) {
init {
@@ -165,7 +165,7 @@
/**
* Attempts to parse the raw data into one of [CreatePasswordRequest],
- * [CreatePublicKeyCredentialRequest], [CreatePublicKeyCredentialRequestPrivileged], and
+ * [CreatePublicKeyCredentialRequest], and
* [CreateCustomCredentialRequest]. Otherwise returns null.
*
* @hide
@@ -176,22 +176,19 @@
type: String,
credentialData: Bundle,
candidateQueryData: Bundle,
- requireSystemProvider: Boolean
+ requireSystemProvider: Boolean,
+ origin: String? = null,
): CreateCredentialRequest? {
return try {
when (type) {
PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
- CreatePasswordRequest.createFrom(credentialData)
+ CreatePasswordRequest.createFrom(credentialData, origin)
PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
CreatePublicKeyCredentialRequest
.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
- CreatePublicKeyCredentialRequest.createFrom(credentialData)
-
- BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV ->
- CreatePublicKeyCredentialRequestPrivileged
- .createFrom(credentialData)
+ CreatePublicKeyCredentialRequest.createFrom(credentialData, origin)
else -> throw FrameworkClassParsingException()
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
index 14d24a5..09c6b6f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCustomCredentialRequest.kt
@@ -33,37 +33,45 @@
* Note: The Bundle keys for [credentialData] and [candidateQueryData] should not be in the form
* of androidx.credentials.*` as they are reserved for internal use by this androidx library.
*
- * @property type the credential type determined by the credential-type-specific subclass for
+ * @param type the credential type determined by the credential-type-specific subclass for
* custom use cases
- * @property credentialData the data of this [CreateCustomCredentialRequest] in the [Bundle]
+ * @param credentialData the data of this [CreateCustomCredentialRequest] in the [Bundle]
* format (note: bundle keys in the form of `androidx.credentials.*` are reserved for internal
* library use)
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent
+ * @param candidateQueryData the partial request data in the [Bundle] format that will be sent
* to the provider during the initial candidate query stage, which should not contain sensitive
* user credential information (note: bundle keys in the form of `androidx.credentials.*` are
* reserved for internal library use)
- * @property isSystemProviderRequired true if must only be fulfilled by a system provider and
+ * @param isSystemProviderRequired true if must only be fulfilled by a system provider and
* false otherwise
- * @property isAutoSelectAllowed defines if a create entry will be automatically chosen if it is
+ * @param isAutoSelectAllowed defines if a create entry will be automatically chosen if it is
* the only one available option, false by default
+ * @param displayInfo the information to be displayed on the screen
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw
+ * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
* @throws IllegalArgumentException If [type] is empty
* @throws NullPointerException If [type], [credentialData], or [candidateQueryData] is null
*/
-open class CreateCustomCredentialRequest @JvmOverloads constructor(
+open class CreateCustomCredentialRequest
+@JvmOverloads constructor(
final override val type: String,
final override val credentialData: Bundle,
final override val candidateQueryData: Bundle,
final override val isSystemProviderRequired: Boolean,
displayInfo: DisplayInfo,
final override val isAutoSelectAllowed: Boolean = false,
+ origin: String? = null,
) : CreateCredentialRequest(
type,
credentialData,
candidateQueryData,
isSystemProviderRequired,
isAutoSelectAllowed,
- displayInfo
+ displayInfo,
+ origin
) {
+
init {
require(type.isNotEmpty()) { "type should not be empty" }
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
index d454665..62b970b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
@@ -26,18 +26,24 @@
*
* @property id the user id associated with the password
* @property password the password
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw a
+ * SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present when
+ * API level >= 34.
*/
class CreatePasswordRequest private constructor(
val id: String,
val password: String,
displayInfo: DisplayInfo,
+ origin: String? = null,
) : CreateCredentialRequest(
type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
credentialData = toCredentialDataBundle(id, password),
candidateQueryData = toCandidateDataBundle(),
isSystemProviderRequired = false,
isAutoSelectAllowed = false,
- displayInfo
+ displayInfo,
+ origin
) {
/**
@@ -46,11 +52,16 @@
*
* @param id the user id associated with the password
* @param password the password
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw
+ * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
* @throws NullPointerException If [id] is null
* @throws NullPointerException If [password] is null
* @throws IllegalArgumentException If [password] is empty
+ * @throws SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present
*/
- constructor(id: String, password: String) : this(id, password, DisplayInfo(id, null))
+ @JvmOverloads constructor(id: String, password: String, origin: String? = null) : this(id,
+ password, DisplayInfo(id, null), origin)
init {
require(password.isNotEmpty()) { "password should not be empty" }
@@ -78,15 +89,16 @@
@JvmStatic
@RequiresApi(23)
- internal fun createFrom(data: Bundle): CreatePasswordRequest {
+ internal fun createFrom(data: Bundle, origin: String? = null): CreatePasswordRequest {
try {
val id = data.getString(BUNDLE_KEY_ID)
val password = data.getString(BUNDLE_KEY_PASSWORD)
val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
return if (displayInfo == null) CreatePasswordRequest(
id!!,
- password!!
- ) else CreatePasswordRequest(id!!, password!!, displayInfo)
+ password!!,
+ origin
+ ) else CreatePasswordRequest(id!!, password!!, displayInfo, origin)
} catch (e: Exception) {
throw FrameworkClassParsingException()
}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
index a635316..366d9f1 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
@@ -25,41 +25,57 @@
/**
* A request to register a passkey from the user's public key credential provider.
*
- * @property requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * @param requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @param clientDataHash a hash that is used to verify the origin
+ * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw
+ * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
*/
class CreatePublicKeyCredentialRequest private constructor(
val requestJson: String,
+ val clientDataHash: String?,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean,
displayInfo: DisplayInfo,
+ origin: String? = null,
) : CreateCredentialRequest(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+ credentialData = toCredentialDataBundle(requestJson, clientDataHash,
+ preferImmediatelyAvailableCredentials),
// The whole request data should be passed during the query phase.
- candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+ candidateQueryData = toCredentialDataBundle(requestJson, clientDataHash,
+ preferImmediatelyAvailableCredentials),
isSystemProviderRequired = false,
isAutoSelectAllowed = false,
- displayInfo
+ displayInfo,
+ origin
) {
/**
* Constructs a [CreatePublicKeyCredentialRequest] to register a passkey from the user's public key credential provider.
*
* @param requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
+ * @param clientDataHash a hash that is used to verify the relying party identity
* @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available passkey registration offering instead of falling back to
* discovering remote options, and false (default) otherwise
+ * @param origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw
+ * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
* @throws NullPointerException If [requestJson] is null
* @throws IllegalArgumentException If [requestJson] is empty, or if it doesn't have a valid
* `user.name` defined according to the [webauthn spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
*/
@JvmOverloads constructor(
requestJson: String,
- preferImmediatelyAvailableCredentials: Boolean = false
- ) : this(requestJson, preferImmediatelyAvailableCredentials, getRequestDisplayInfo(requestJson))
+ clientDataHash: String? = null,
+ preferImmediatelyAvailableCredentials: Boolean = false,
+ origin: String? = null
+ ) : this(requestJson, clientDataHash, preferImmediatelyAvailableCredentials,
+ getRequestDisplayInfo(requestJson), origin)
init {
require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
@@ -69,6 +85,8 @@
companion object {
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+ internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
+ "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
@@ -90,6 +108,7 @@
@JvmStatic
internal fun toCredentialDataBundle(
requestJson: String,
+ clientDataHash: String? = null,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
@@ -98,6 +117,7 @@
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+ bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
bundle.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials
@@ -108,6 +128,7 @@
@JvmStatic
internal fun toCandidateDataBundle(
requestJson: String,
+ clientDataHash: String?,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
@@ -116,6 +137,7 @@
BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+ bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
bundle.putBoolean(
BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials
@@ -123,23 +145,29 @@
return bundle
}
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
+ @Suppress("deprecation") // bundle.get() used for boolean value
+ // to prevent default boolean value from being returned.
@JvmStatic
@RequiresApi(23)
- internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
+ internal fun createFrom(data: Bundle, origin: String? = null):
+ CreatePublicKeyCredentialRequest {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+ val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
return if (displayInfo == null) CreatePublicKeyCredentialRequest(
requestJson!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean
+ clientDataHash,
+ (preferImmediatelyAvailableCredentials!!) as Boolean,
+ origin
) else CreatePublicKeyCredentialRequest(
requestJson!!,
+ clientDataHash,
(preferImmediatelyAvailableCredentials!!) as Boolean,
- displayInfo
+ displayInfo,
+ origin
)
} catch (e: Exception) {
throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
deleted file mode 100644
index af8f3f2..0000000
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import android.os.Bundle
-import androidx.annotation.RequiresApi
-import androidx.credentials.internal.FrameworkClassParsingException
-
-/**
- * A privileged request to register a passkey from the user’s public key credential provider, where
- * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
- * browser, caBLE, can use this. These permissions will be introduced in an upcoming release.
- *
- * @property requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available passkey registration offering instead of falling back to
- * discovering remote options, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
- */
-class CreatePublicKeyCredentialRequestPrivileged private constructor(
- val requestJson: String,
- val relyingParty: String,
- val clientDataHash: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean,
- displayInfo: DisplayInfo,
-) : CreateCredentialRequest(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- credentialData = toCredentialDataBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- // The whole request data should be passed during the query phase.
- candidateQueryData = toCredentialDataBundle(
- requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
- ),
- isSystemProviderRequired = false,
- isAutoSelectAllowed = false,
- displayInfo,
-) {
-
- /**
- * Constructs a privileged request to register a passkey from the user’s public key credential
- * provider, where the caller can modify the rp. Only callers with privileged permission, e.g.
- * user’s default browser, caBLE, can use this. These permissions will be introduced in an
- * upcoming release.
- *
- * @param requestJson the privileged request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson).
- * @param preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available passkey registration offering instead of falling
- * back to discovering remote options, and false (default) otherwise
- * @param relyingParty the expected true RP ID which will override the one in the
- * [requestJson], where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
- * @param clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or
- * [clientDataHash] is empty, or if [requestJson] doesn't have a valid user.name` defined
- * according to the [webauthn spec](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson)
- */
- @JvmOverloads constructor(
- requestJson: String,
- relyingParty: String,
- clientDataHash: String,
- preferImmediatelyAvailableCredentials: Boolean = false
- ) : this(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials,
- CreatePublicKeyCredentialRequest.getRequestDisplayInfo(requestJson),
- )
-
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- require(relyingParty.isNotEmpty()) { "rp must not be empty" }
- require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
- }
-
- /** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */
- class Builder(
- private var requestJson: String,
- private var relyingParty: String,
- private var clientDataHash: String
- ) {
-
- private var preferImmediatelyAvailableCredentials: Boolean = false
-
- /**
- * Sets the privileged request in JSON format.
- */
- fun setRequestJson(requestJson: String): Builder {
- this.requestJson = requestJson
- return this
- }
-
- /**
- * Sets to true if you prefer the operation to return immediately when there is no available
- * passkey registration offering instead of falling back to discovering remote options, and
- * false otherwise.
- *
- * The default value is false.
- */
- @Suppress("MissingGetterMatchingBuilder")
- fun setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentials: Boolean
- ): Builder {
- this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
- return this
- }
-
- /**
- * Sets the expected true RP ID which will override the one in the [requestJson].
- */
- fun setRelyingParty(relyingParty: String): Builder {
- this.relyingParty = relyingParty
- return this
- }
-
- /**
- * Sets a hash that is used to verify the [relyingParty] Identity.
- */
- fun setClientDataHash(clientDataHash: String): Builder {
- this.clientDataHash = clientDataHash
- return this
- }
-
- /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
- fun build(): CreatePublicKeyCredentialRequestPrivileged {
- return CreatePublicKeyCredentialRequestPrivileged(
- this.requestJson,
- this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
- )
- }
- }
-
- /** @hide */
- companion object {
- internal const val BUNDLE_KEY_RELYING_PARTY =
- "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
- internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
- "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-
- internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
- "PRIVILEGED"
-
- @JvmStatic
- internal fun toCredentialDataBundle(
- requestJson: String,
- relyingParty: String,
- clientDataHash: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
- )
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- @RequiresApi(23)
- internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- val displayInfo = DisplayInfo.parseFromCredentialDataBundle(data)
- return if (displayInfo == null) CreatePublicKeyCredentialRequestPrivileged(
- requestJson!!,
- rp!!,
- clientDataHash!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
- ) else CreatePublicKeyCredentialRequestPrivileged(
- requestJson!!,
- rp!!,
- clientDataHash!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
- displayInfo,
- )
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
index 766e3b0..4655ca9 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialOption.kt
@@ -76,9 +76,6 @@
GetPublicKeyCredentialOption
.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
GetPublicKeyCredentialOption.createFrom(requestData)
- GetPublicKeyCredentialOptionPrivileged
- .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
- GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
else -> throw FrameworkClassParsingException()
}
else -> throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index 42c34f8..225880c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -25,10 +25,15 @@
*
* @property credentialOptions the list of [CredentialOption] from which the user can choose
* one to authenticate to the app
+ * @property origin the origin of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will throw
+ * a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not present.
* @throws IllegalArgumentException If [credentialOptions] is empty
*/
-class GetCredentialRequest constructor(
+class GetCredentialRequest
+@JvmOverloads constructor(
val credentialOptions: List<CredentialOption>,
+ val origin: String? = null,
) {
init {
@@ -38,6 +43,7 @@
/** A builder for [GetCredentialRequest]. */
class Builder {
private var credentialOptions: MutableList<CredentialOption> = mutableListOf()
+ private var origin: String? = null
/** Adds a specific type of [CredentialOption]. */
fun addCredentialOption(credentialOption: CredentialOption): Builder {
@@ -51,13 +57,22 @@
return this
}
+ /** Sets the [origin] of a different application if the request is being made on behalf of
+ * that application. For API level >=34, setting a non-null value for this parameter, will
+ * throw a SecurityException if android.permission.CREDENTIAL_MANAGER_SET_ORIGIN is not
+ * present. */
+ fun setOrigin(origin: String): Builder {
+ this.origin = origin
+ return this
+ }
+
/**
* Builds a [GetCredentialRequest].
*
* @throws IllegalArgumentException If [credentialOptions] is empty
*/
fun build(): GetCredentialRequest {
- return GetCredentialRequest(credentialOptions.toList())
+ return GetCredentialRequest(credentialOptions.toList(), origin)
}
}
}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
index 94f7855..cdd6e76 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
@@ -24,6 +24,8 @@
*
* @property requestJson the request in JSON format in the standard webauthn web json
* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
+ * @property clientDataHash a hash that is used to verify the relying party identity, set only if
+ * you have set the [GetCredentialRequest.origin]
* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
* immediately when there is no available credential instead of falling back to discovering remote
* credentials, and false (default) otherwise
@@ -32,12 +34,15 @@
*/
class GetPublicKeyCredentialOption @JvmOverloads constructor(
val requestJson: String,
+ val clientDataHash: String? = null,
@get:JvmName("preferImmediatelyAvailableCredentials")
val preferImmediatelyAvailableCredentials: Boolean = false,
) : CredentialOption(
type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
- candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+ requestData = toRequestDataBundle(requestJson, clientDataHash,
+ preferImmediatelyAvailableCredentials),
+ candidateQueryData = toRequestDataBundle(requestJson, clientDataHash,
+ preferImmediatelyAvailableCredentials),
isSystemProviderRequired = false,
isAutoSelectAllowed = true,
) {
@@ -49,6 +54,8 @@
companion object {
internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
"androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+ internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
+ "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
"androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
@@ -56,6 +63,7 @@
@JvmStatic
internal fun toRequestDataBundle(
requestJson: String,
+ clientDataHash: String?,
preferImmediatelyAvailableCredentials: Boolean
): Bundle {
val bundle = Bundle()
@@ -64,6 +72,7 @@
BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
)
bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+ bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
preferImmediatelyAvailableCredentials)
return bundle
@@ -75,9 +84,11 @@
internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
try {
val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+ val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
val preferImmediatelyAvailableCredentials =
data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
return GetPublicKeyCredentialOption(requestJson!!,
+ clientDataHash,
(preferImmediatelyAvailableCredentials!!) as Boolean)
} catch (e: Exception) {
throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
deleted file mode 100644
index 2b41d8a..0000000
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.credentials
-
-import android.os.Bundle
-import androidx.credentials.internal.FrameworkClassParsingException
-
-/**
- * A privileged request to get passkeys from the user's public key credential provider. The caller
- * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
- * can use this. These permissions will be introduced in an upcoming release.
- *
- * @property requestJson the request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
- * is null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * empty
- */
-class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
- val requestJson: String,
- val relyingParty: String,
- val clientDataHash: String,
- @get:JvmName("preferImmediatelyAvailableCredentials")
- val preferImmediatelyAvailableCredentials: Boolean = false
-) : CredentialOption(
- type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
- requestData = toBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- candidateQueryData = toBundle(
- requestJson,
- relyingParty,
- clientDataHash,
- preferImmediatelyAvailableCredentials
- ),
- isSystemProviderRequired = false,
- isAutoSelectAllowed = true,
-) {
-
- init {
- require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
- require(relyingParty.isNotEmpty()) { "rp must not be empty" }
- require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
- }
-
- /** A builder for [GetPublicKeyCredentialOptionPrivileged]. */
- class Builder(
- private var requestJson: String,
- private var relyingParty: String,
- private var clientDataHash: String
- ) {
-
- private var preferImmediatelyAvailableCredentials: Boolean = false
-
- /**
- * Sets the privileged request in JSON format.
- */
- fun setRequestJson(requestJson: String): Builder {
- this.requestJson = requestJson
- return this
- }
-
- /**
- * Sets to true if you prefer the operation to return immediately when there is no available
- * credential instead of falling back to discovering remote credentials, and false
- * otherwise.
- *
- * The default value is false.
- */
- @Suppress("MissingGetterMatchingBuilder")
- fun setPreferImmediatelyAvailableCredentials(
- preferImmediatelyAvailableCredentials: Boolean
- ): Builder {
- this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
- return this
- }
-
- /**
- * Sets the expected true RP ID which will override the one in the [requestJson].
- */
- fun setRelyingParty(relyingParty: String): Builder {
- this.relyingParty = relyingParty
- return this
- }
-
- /**
- * Sets a hash that is used to verify the [relyingParty] Identity.
- */
- fun setClientDataHash(clientDataHash: String): Builder {
- this.clientDataHash = clientDataHash
- return this
- }
-
- /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
- fun build(): GetPublicKeyCredentialOptionPrivileged {
- return GetPublicKeyCredentialOptionPrivileged(
- this.requestJson,
- this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
- )
- }
- }
-
- /** @hide */
- companion object {
- internal const val BUNDLE_KEY_RELYING_PARTY =
- "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
- internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
- "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
- internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
- "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
- internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
- internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
- "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
- "_PRIVILEGED"
-
- @JvmStatic
- internal fun toBundle(
- requestJson: String,
- relyingParty: String,
- clientDataHash: String,
- preferImmediatelyAvailableCredentials: Boolean
- ): Bundle {
- val bundle = Bundle()
- bundle.putString(
- PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
- BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
- )
- bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
- bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
- bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
- bundle.putBoolean(
- BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
- preferImmediatelyAvailableCredentials
- )
- return bundle
- }
-
- @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
- // boolean value from being returned.
- @JvmStatic
- internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
- try {
- val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
- val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
- val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
- val preferImmediatelyAvailableCredentials =
- data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
- return GetPublicKeyCredentialOptionPrivileged(
- requestJson!!,
- rp!!,
- clientDataHash!!,
- (preferImmediatelyAvailableCredentials!!) as Boolean,
- )
- } catch (e: Exception) {
- throw FrameworkClassParsingException()
- }
- }
- }
-}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
index ccea9d2..ab39cd4 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -23,7 +23,6 @@
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
-import androidx.credentials.CreatePublicKeyCredentialRequestPrivileged
import androidx.credentials.R
/** @hide */
@@ -50,7 +49,6 @@
when (request) {
is CreatePasswordRequest -> R.drawable.ic_password
is CreatePublicKeyCredentialRequest -> R.drawable.ic_passkey
- is CreatePublicKeyCredentialRequestPrivileged -> R.drawable.ic_passkey
else -> R.drawable.ic_other_sign_in
}
)
diff --git a/development/importMaven/README.md b/development/importMaven/README.md
index 9ec5b89..adaabda 100644
--- a/development/importMaven/README.md
+++ b/development/importMaven/README.md
@@ -40,4 +40,7 @@
./importMaven.sh --help
./importMaven.sh import-konan-prebuilts --help
./importMaven.sh import-toml --help
-```
\ No newline at end of file
+```
+
+# Next steps:
+Gradle may not trust artifacts that are not signed with a trusted key. To ask Gradle to trust these artifacts, run `development/update-verification-metadata.sh`
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
index 2f0c708..9adf1d3 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactResolver.kt
@@ -92,7 +92,7 @@
localRepositories: List<String> = emptyList(),
explicitlyFetchInheritedDependencies: Boolean = false,
downloadObserver: DownloadObserver?,
- ): List<ResolvedArtifactResult> {
+ ): ArtifactsResolutionResult {
return SingleUseArtifactResolver(
project = ProjectService.createProject(),
artifacts = artifacts,
@@ -115,7 +115,7 @@
private val downloadObserver: DownloadObserver?,
) {
private val logger = logger("ArtifactResolver")
- fun resolveArtifacts(): List<ResolvedArtifactResult> {
+ fun resolveArtifacts(): ArtifactsResolutionResult {
logger.info {
"""--------------------------------------------------------------------------------
Resolving artifacts:
@@ -137,6 +137,7 @@
logger.trace {
"Initialized proxy servers"
}
+ var dependenciesPassedVerification = true
project.dependencies.apply {
components.all(CustomMetadataRules::class.java)
@@ -153,10 +154,13 @@
project.dependencies.create(it)
}
val resolvedArtifacts = createConfigurationsAndResolve(dependencies)
- allResolvedArtifacts.addAll(resolvedArtifacts)
+ if (!resolvedArtifacts.dependenciesPassedVerification) {
+ dependenciesPassedVerification = false
+ }
+ allResolvedArtifacts.addAll(resolvedArtifacts.artifacts)
completedComponentIds.addAll(pendingComponentIds)
pendingComponentIds.clear()
- val newComponentIds = resolvedArtifacts.mapNotNull {
+ val newComponentIds = resolvedArtifacts.artifacts.mapNotNull {
(it.id.componentIdentifier as? ModuleComponentIdentifier)?.toString()
}.filter {
!completedComponentIds.contains(it) && pendingComponentIds.add(it)
@@ -166,15 +170,16 @@
}
pendingComponentIds.addAll(newComponentIds)
} while (explicitlyFetchInheritedDependencies && pendingComponentIds.isNotEmpty())
- allResolvedArtifacts.toList()
+ ArtifactsResolutionResult(allResolvedArtifacts.toList(), dependenciesPassedVerification)
}.also { result ->
+ val artifacts = result.artifacts
logger.trace {
- "Resolved files: ${result.size}"
+ "Resolved files: ${artifacts.size}"
}
- check(result.isNotEmpty()) {
+ check(artifacts.isNotEmpty()) {
"Didn't resolve any artifacts from $artifacts . Try --verbose for more information"
}
- result.forEach { artifact ->
+ artifacts.forEach { artifact ->
logger.trace {
artifact.id.toString()
}
@@ -187,7 +192,7 @@
*/
private fun createConfigurationsAndResolve(
dependencies: List<Dependency>
- ): List<ResolvedArtifactResult> {
+ ): ArtifactsResolutionResult {
val configurations = dependencies.flatMap { dep ->
buildList {
addAll(createApiConfigurations(dep))
@@ -196,9 +201,16 @@
addAll(createKmpConfigurations(dep))
}
}
- return configurations.flatMap { configuration ->
+ val resolution = configurations.map { configuration ->
resolveArtifacts(configuration, disableVerificationOnFailure = true)
}
+ val artifacts = resolution.flatMap { resolution ->
+ resolution.artifacts
+ }
+ val dependenciesPassedVerification = resolution.map { resolution ->
+ resolution.dependenciesPassedVerification
+ }.all { it == true }
+ return ArtifactsResolutionResult(artifacts, dependenciesPassedVerification)
}
/**
@@ -211,13 +223,14 @@
private fun resolveArtifacts(
configuration: Configuration,
disableVerificationOnFailure: Boolean
- ): Set<ResolvedArtifactResult> {
+ ): ArtifactsResolutionResult {
return try {
- configuration.incoming.artifactView {
+ val artifacts = configuration.incoming.artifactView {
// We need to be lenient because we are requesting files that might not exist.
// For example source.jar or .asc.
it.lenient(true)
- }.artifacts.artifacts ?: emptySet()
+ }.artifacts.artifacts?.toList() ?: emptyList()
+ ArtifactsResolutionResult(artifacts.toList(), dependenciesPassedVerification = true)
} catch (verificationException: DependencyVerificationException) {
if (disableVerificationOnFailure) {
val copy = configuration.copyRecursive().also {
@@ -229,7 +242,8 @@
${verificationException.message?.prependIndent(" ")}
"""
}
- resolveArtifacts(copy, disableVerificationOnFailure = false)
+ val artifacts = resolveArtifacts(copy, disableVerificationOnFailure = false)
+ return ArtifactsResolutionResult(artifacts.artifacts, dependenciesPassedVerification = false)
} else {
throw verificationException
}
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactsResolutionResult.kt
similarity index 72%
rename from appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java
rename to development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactsResolutionResult.kt
index 93db9d1..1bad769 100644
--- a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/proto/package-info.java
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/ArtifactsResolutionResult.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-/** @hide */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-package androidx.appactions.interaction.service.proto;
+package androidx.build.importMaven
-import androidx.annotation.RestrictTo;
+import org.gradle.api.artifacts.result.ResolvedArtifactResult
+
+data class ArtifactsResolutionResult(val artifacts: List<ResolvedArtifactResult>, val dependenciesPassedVerification: Boolean) {
+}
diff --git a/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt b/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
index ceefeac..bd5716f 100644
--- a/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
+++ b/development/importMaven/src/main/kotlin/androidx/build/importMaven/Main.kt
@@ -225,7 +225,7 @@
repositories?.let {
extraRepositories.addAll(it)
}
- val resolvedArtifacts = ArtifactResolver.resolveArtifacts(
+ val result = ArtifactResolver.resolveArtifacts(
artifacts = artifactsToBeResolved,
additionalRepositories = extraRepositories,
explicitlyFetchInheritedDependencies = explicitlyFetchInheritedDependencies,
@@ -239,6 +239,7 @@
},
downloadObserver = downloader
)
+ val resolvedArtifacts = result.artifacts
if (cleanLocalRepo) {
downloader.cleanupLocalRepositories()
}
@@ -263,6 +264,14 @@
https://issuetracker.google.com/issues/new?component=705292[0m
""".trimIndent()
)
+ } else {
+ if (!result.dependenciesPassedVerification) {
+ logger.warn(
+ """
+ [33mOur Gradle build won't trust any artifacts that are unsigned or are signed with new keys. To trust these artifacts, run `development/update-verification-metadata.sh
+ """.trimIndent()
+ )
+ }
}
flushLogs()
}
@@ -385,4 +394,4 @@
fun main(args: Array<String>) {
createCliCommands().main(args)
exitProcess(0)
-}
\ No newline at end of file
+}
diff --git a/docs/index.md b/docs/index.md
index 6408bc9..cbdb075 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -20,6 +20,9 @@
To get started developing in AndroidX, see the
[Getting started](/company/teams/androidx/onboarding.md) guide.
+For information on library and API design, see the
+[Library API Guidelines](/company/teams/androidx/api_guidelines/index.md).
+
## Quick links
### Filing an issue
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 204c99a..dec5d83 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -87,6 +87,8 @@
private static final String JPEG_WITH_EXIF_BYTE_ORDER_II = "jpeg_with_exif_byte_order_ii.jpg";
private static final String JPEG_WITH_EXIF_BYTE_ORDER_MM = "jpeg_with_exif_byte_order_mm.jpg";
+ private static final String JPEG_WITH_EXIF_INVALID_OFFSET = "jpeg_with_exif_invalid_offset.jpg";
+
private static final String DNG_WITH_EXIF_WITH_XMP = "dng_with_exif_with_xmp.dng";
private static final String JPEG_WITH_EXIF_WITH_XMP = "jpeg_with_exif_with_xmp.jpg";
private static final String PNG_WITH_EXIF_BYTE_ORDER_II = "png_with_exif_byte_order_ii.png";
@@ -107,6 +109,7 @@
private static final int[] IMAGE_RESOURCES = new int[] {
R.raw.jpeg_with_exif_byte_order_ii,
R.raw.jpeg_with_exif_byte_order_mm,
+ R.raw.jpeg_with_exif_invalid_offset,
R.raw.dng_with_exif_with_xmp,
R.raw.jpeg_with_exif_with_xmp,
R.raw.png_with_exif_byte_order_ii,
@@ -122,6 +125,7 @@
private static final String[] IMAGE_FILENAMES = new String[] {
JPEG_WITH_EXIF_BYTE_ORDER_II,
JPEG_WITH_EXIF_BYTE_ORDER_MM,
+ JPEG_WITH_EXIF_INVALID_OFFSET,
DNG_WITH_EXIF_WITH_XMP,
JPEG_WITH_EXIF_WITH_XMP,
PNG_WITH_EXIF_BYTE_ORDER_II,
@@ -454,6 +458,14 @@
writeToFilesWithExif(JPEG_WITH_EXIF_WITH_XMP, R.array.jpeg_with_exif_with_xmp);
}
+ // https://issuetracker.google.com/264729367
+ @Test
+ @LargeTest
+ public void testJpegWithInvalidOffset() throws Throwable {
+ readFromFilesWithExif(JPEG_WITH_EXIF_INVALID_OFFSET, R.array.jpeg_with_exif_invalid_offset);
+ writeToFilesWithExif(JPEG_WITH_EXIF_INVALID_OFFSET, R.array.jpeg_with_exif_invalid_offset);
+ }
+
@Test
@LargeTest
public void testDngWithExifAndXmp() throws Throwable {
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_invalid_offset.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_invalid_offset.jpg
new file mode 100644
index 0000000..73cb1b5
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_invalid_offset.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
index 75c221c..c175dbc 100644
--- a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
+++ b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
@@ -191,6 +191,50 @@
<item>0</item>
<item>0</item>
</array>
+ <array name="jpeg_with_exif_invalid_offset">
+ <!--Whether thumbnail exists-->
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ <item>0</item>
+ <item>0</item>
+ <item>false</item>
+ <!--Whether GPS LatLong information exists-->
+ <item>true</item>
+ <item>584</item>
+ <item>24</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <item>0.0</item>
+ <!--Whether Make information exists-->
+ <item>true</item>
+ <item>414</item>
+ <item>4</item>
+ <item>LGE</item>
+ <item>Nexus 5</item>
+ <item>0.0</item>
+ <item />
+ <item>0.0</item>
+ <item>0</item>
+ <item />
+ <item>0/1000</item>
+ <item>0</item>
+ <item>1970:01:01</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>N</item>
+ <item>0/1,0/1,0/10000</item>
+ <item>E</item>
+ <item>GPS</item>
+ <item>00:00:00</item>
+ <item>176</item>
+ <item>144</item>
+ <item />
+ <item>0</item>
+ <item>0</item>
+ <item>false</item>
+ <item>0</item>
+ <item>0</item>
+ </array>
<array name="dng_with_exif_with_xmp">
<!--Whether thumbnail exists-->
<item>true</item>
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 26c577c..9427e20 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -6909,9 +6909,11 @@
}
// Check if the next IFD offset
- // 1. Is a non-negative value, and
+ // 1. Is a non-negative value (within the length of the input, if known), and
// 2. Does not point to a previously read IFD.
- if (offset > 0L) {
+ if (offset > 0L
+ && (dataInputStream.length() == ByteOrderedDataInputStream.LENGTH_UNSET
+ || offset < dataInputStream.length())) {
if (!mAttributesOffsets.contains((int) offset)) {
dataInputStream.seek(offset);
readImageFileDirectory(dataInputStream, nextIfdType);
@@ -6923,7 +6925,12 @@
}
} else {
if (DEBUG) {
- Log.d(TAG, "Skip jump into the IFD since its offset is invalid: " + offset);
+ String message =
+ "Skip jump into the IFD since its offset is invalid: " + offset;
+ if (dataInputStream.length() != ByteOrderedDataInputStream.LENGTH_UNSET) {
+ message += " (total length: " + dataInputStream.length() + ")";
+ }
+ Log.d(TAG, message);
}
}
@@ -7700,14 +7707,18 @@
// An input stream class that can parse both little and big endian order data.
private static class ByteOrderedDataInputStream extends InputStream implements DataInput {
+
+ public static final int LENGTH_UNSET = -1;
protected final DataInputStream mDataInputStream;
protected int mPosition;
private ByteOrder mByteOrder;
private byte[] mSkipBuffer;
+ private int mLength;
ByteOrderedDataInputStream(byte[] bytes) throws IOException {
this(new ByteArrayInputStream(bytes), BIG_ENDIAN);
+ this.mLength = bytes.length;
}
ByteOrderedDataInputStream(InputStream in) throws IOException {
@@ -7719,6 +7730,9 @@
mDataInputStream.mark(0);
mPosition = 0;
mByteOrder = byteOrder;
+ this.mLength = in instanceof ByteOrderedDataInputStream
+ ? ((ByteOrderedDataInputStream) in).length()
+ : LENGTH_UNSET;
}
public void setByteOrder(ByteOrder byteOrder) {
@@ -7945,6 +7959,12 @@
public void reset() {
throw new UnsupportedOperationException("Reset is currently unsupported");
}
+
+ /** Return the total length (in bytes) of the underlying stream if known, otherwise
+ * {@link #LENGTH_UNSET}. */
+ public int length() {
+ return mLength;
+ }
}
// An output stream to write EXIF data area, which can be written in either little or big endian
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 5a8a3cd..5dfc13d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -39,6 +39,8 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import leakcanary.DetectLeaksAfterTestSuccess
+import leakcanary.SkipLeakDetection
+import leakcanary.TestDescriptionHolder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@@ -57,6 +59,9 @@
val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
.around(activityTestRule)
+ @get:Rule
+ val testDescriptionHolderRule = TestDescriptionHolder
+
@Test
fun testDialogFragmentShows() {
val fragment = TestDialogFragment()
@@ -95,6 +100,8 @@
.isTrue()
}
+ // TODO(b/270722758): remove annotation once issue addressed by LeakCanary/platform
+ @SkipLeakDetection("Skip leak detection until platform ViewRootImpl leak addressed")
@Test
fun testDialogFragmentDismiss() {
val fragment = TestDialogFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
index 81cf605..0366a45 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
@@ -212,7 +212,13 @@
}
}
-class SimpleContainerActivity : FragmentActivity(R.layout.simple_container)
+class SimpleContainerActivity : FragmentActivity(R.layout.simple_container) {
+ var invalidateCount = 0
+ override fun invalidateMenu() {
+ invalidateCount++
+ super.invalidateMenu()
+ }
+}
class ContainerViewActivity : FragmentActivity(R.layout.inflated_fragment_container_view) {
var foundFragment = false
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
index 27a4ec9..eb41633 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
@@ -37,6 +37,7 @@
import androidx.test.filters.SmallTest
import androidx.testutils.withActivity
import androidx.testutils.withUse
+import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -62,9 +63,13 @@
@Test
fun fragmentWithOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = MenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
@@ -72,16 +77,29 @@
assertWithMessage("Fragment should have an options menu")
.that(fragment.hasOptionsMenu()).isTrue()
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(2)
assertWithMessage("Child fragments should not have an options menu")
.that(fragment.mChildFragmentManager.checkForMenus()).isFalse()
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(3)
}
@LargeTest
@Test
fun setMenuVisibilityShowHide() {
withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
- val fm = withActivity { supportFragmentManager }
+ withActivity {
+ /** Internal call to [SimpleContainerActivity.invalidateMenu] from [SimpleContainerActivity.addMenuProvider] upon activity creation */
+ assertThat(invalidateCount).isEqualTo(1)
+ }
+ val fm = withActivity { supportFragmentManager }
val fragment = MenuFragment()
withActivity {
@@ -93,10 +111,17 @@
assertWithMessage("Fragment should have an options menu")
.that(fragment.hasOptionsMenu()).isTrue()
+ withActivity {
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(invalidateCount).isEqualTo(2)
+ }
+
withActivity {
fm.beginTransaction()
.hide(fragment)
.commitNow()
+ assertWithMessage("Hiding fragment with options menu should invalidate menu")
+ .that(invalidateCount).isEqualTo(3)
}
fragment.onCreateOptionsMenuCountDownLatch = CountDownLatch(1)
@@ -105,19 +130,34 @@
fm.beginTransaction()
.show(fragment)
.commitNow()
+ assertWithMessage("Showing fragment with options menu should invalidate menu")
+ .that(invalidateCount).isEqualTo(4)
}
assertWithMessage("onCreateOptionsMenu was not called")
.that(fragment.onCreateOptionsMenuCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
.isTrue()
+
+ withActivity {
+ fm.beginTransaction()
+ .remove(fragment)
+ .commitNow()
+ executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(invalidateCount).isEqualTo(5)
+ }
}
}
@Test
fun fragmentWithNoOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = StrictViewFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
@@ -125,15 +165,29 @@
assertWithMessage("Fragment should not have an options menu")
.that(fragment.hasOptionsMenu()).isFalse()
+ assertWithMessage("Adding fragment without options menu should not invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(1)
assertWithMessage("Child fragments should not have an options menu")
.that(fragment.mChildFragmentManager.checkForMenus()).isFalse()
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertThat(activity.invalidateCount).isEqualTo(1)
+ assertWithMessage("Removing fragment without options menu should not invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(1)
}
@Test
fun childFragmentWithOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val parent = ParentOptionsMenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, parent, "parent")
.commit()
@@ -143,13 +197,37 @@
.that(parent.hasOptionsMenu()).isFalse()
assertWithMessage("Child fragment should have an options menu")
.that(parent.mChildFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding child fragment with options menu and parent fragment " +
+ "without should invalidate menu only once"
+ ).that(activity.invalidateCount).isEqualTo(2)
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(parent.childFragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing child fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ fm.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun childFragmentWithOptionsMenuParentMenuVisibilityFalse() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val parent = ParentOptionsMenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
parent.setMenuVisibility(false)
fm.beginTransaction()
@@ -161,19 +239,43 @@
.that(parent.hasOptionsMenu()).isFalse()
assertWithMessage("Child fragment should have an options menu")
.that(parent.childFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding child fragment with options menu and parent fragment " +
+ "without should invalidate menu only once"
+ ).that(activity.invalidateCount).isEqualTo(2)
activityRule.runOnUiThread {
assertWithMessage("child fragment onCreateOptions menu was not called")
.that(parent.childFragment.onCreateOptionsMenuCountDownLatch.count)
.isEqualTo(1)
}
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(parent.childFragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing child fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ fm.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun childFragmentWithPrepareOptionsMenuParentMenuVisibilityFalse() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val parent = ParentOptionsMenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
parent.setMenuVisibility(false)
fm.beginTransaction()
@@ -185,19 +287,43 @@
.that(parent.hasOptionsMenu()).isFalse()
assertWithMessage("Child fragment should have an options menu")
.that(parent.childFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding child fragment with options menu and parent fragment " +
+ "without should invalidate menu only once"
+ ).that(activity.invalidateCount).isEqualTo(2)
activityRule.runOnUiThread {
assertWithMessage("child fragment onCreateOptions menu was not called")
.that(parent.childFragment.onPrepareOptionsMenuCountDownLatch.count)
.isEqualTo(1)
}
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(parent.childFragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing child fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ fm.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun parentAndChildFragmentWithOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val parent = ParentOptionsMenuFragment(true)
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, parent, "parent")
.commit()
@@ -207,47 +333,108 @@
.that(parent.hasOptionsMenu()).isTrue()
assertWithMessage("Child fragment should have an options menu")
.that(parent.mChildFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding parent and child fragment both with options menu should invalidate menu twice"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(parent.childFragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing child fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(4)
+
+ fm.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(5)
}
@Test
fun grandchildFragmentWithOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
- val parent = StrictViewFragment(R.layout.double_container)
- val fm = activityRule.activity.supportFragmentManager
+ val grandParent = StrictViewFragment(R.layout.double_container)
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
- .add(R.id.fragmentContainer, parent, "parent")
+ .add(R.id.fragmentContainer, grandParent, "grandParent")
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Adding grandparent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(1)
+
+ val parent = ParentOptionsMenuFragment()
+ grandParent.childFragmentManager.beginTransaction()
+ .add(R.id.fragmentContainer1, parent)
.commit()
activityRule.executePendingTransactions()
- parent.childFragmentManager.beginTransaction()
- .add(R.id.fragmentContainer1, ParentOptionsMenuFragment())
- .commit()
- activityRule.executePendingTransactions()
-
- assertWithMessage("Fragment should not have an options menu")
- .that(parent.hasOptionsMenu()).isFalse()
+ assertWithMessage("Parent fragment should not have an options menu")
+ .that(grandParent.hasOptionsMenu()).isFalse()
assertWithMessage("Grandchild fragment should have an options menu")
- .that(parent.mChildFragmentManager.checkForMenus()).isTrue()
+ .that(grandParent.mChildFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding grandchild fragment with options menu and " +
+ "parent fragment without should invalidate menu only once"
+ ).that(activity.invalidateCount).isEqualTo(2)
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(parent.childFragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing grandchild fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ grandParent.mChildFragmentManager.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ fm.beginTransaction()
+ .remove(grandParent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing grandparent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
}
// Ensure that we check further than just the first child fragment
@Test
fun secondChildFragmentWithOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val parent = StrictViewFragment(R.layout.double_container)
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, parent, "parent")
.commit()
activityRule.executePendingTransactions()
+ val childWithoutMenu = Fragment()
parent.childFragmentManager.beginTransaction()
- .add(R.id.fragmentContainer1, Fragment())
+ .add(R.id.fragmentContainer1, childWithoutMenu)
.commit()
activityRule.executePendingTransactions()
+ val childWithMenu = MenuFragment()
parent.childFragmentManager.beginTransaction()
- .add(R.id.fragmentContainer2, MenuFragment())
+ .add(R.id.fragmentContainer2, childWithMenu)
.commit()
activityRule.executePendingTransactions()
@@ -255,76 +442,156 @@
.that(parent.hasOptionsMenu()).isFalse()
assertWithMessage("Second child fragment should have an options menu")
.that(parent.mChildFragmentManager.checkForMenus()).isTrue()
+ assertWithMessage(
+ "Adding second child fragment with options menu and the parent and " +
+ "first child fragments without should invalidate menu only once"
+ ).that(activity.invalidateCount).isEqualTo(2)
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(childWithoutMenu)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing first child fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(2)
+
+ parent.mChildFragmentManager.beginTransaction()
+ .remove(childWithMenu)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing second child fragment with options menu should invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
+
+ fm.beginTransaction()
+ .remove(parent)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage(
+ "Removing parent fragment without options menu should not invalidate menu"
+ ).that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun onPrepareOptionsMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = MenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
activityRule.executePendingTransactions()
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(2)
- openActionBarOverflowOrOptionsMenu(activityRule.activity.applicationContext)
+ openActionBarOverflowOrOptionsMenu(activity.applicationContext)
onView(ViewMatchers.withText("Item1")).perform(ViewActions.click())
assertWithMessage("onPrepareOptionsMenu was not called")
.that(fragment.onPrepareOptionsMenuCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
.isTrue()
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun inflatesMenu() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = MenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
activityRule.executePendingTransactions()
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(2)
- openActionBarOverflowOrOptionsMenu(activityRule.activity.applicationContext)
+ openActionBarOverflowOrOptionsMenu(activity.applicationContext)
onView(ViewMatchers.withText("Item1"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
onView(ViewMatchers.withText("Item2"))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun menuItemSelected() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = MenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
activityRule.executePendingTransactions()
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(2)
- openActionBarOverflowOrOptionsMenu(activityRule.activity.applicationContext)
+ openActionBarOverflowOrOptionsMenu(activity.applicationContext)
onView(ViewMatchers.withText("Item1")).perform(ViewActions.click())
- openActionBarOverflowOrOptionsMenu(activityRule.activity.applicationContext)
+ openActionBarOverflowOrOptionsMenu(activity.applicationContext)
onView(ViewMatchers.withText("Item2")).perform(ViewActions.click())
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(3)
}
@Test
fun onOptionsMenuClosed() {
+ val activity = activityRule.getActivity()
+ /** Internal call to [FragmentTestActivity.invalidateMenu] from [FragmentTestActivity.addMenuProvider] upon activity creation */
+ assertThat(activity.invalidateCount).isEqualTo(1)
+
activityRule.setContentView(R.layout.simple_container)
val fragment = MenuFragment()
- val fm = activityRule.activity.supportFragmentManager
+ val fm = activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit()
activityRule.executePendingTransactions()
+ assertWithMessage("Adding fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(2)
- openActionBarOverflowOrOptionsMenu(activityRule.activity.applicationContext)
+ openActionBarOverflowOrOptionsMenu(activity.applicationContext)
activityRule.runOnUiThread {
- activityRule.activity.closeOptionsMenu()
+ activity.closeOptionsMenu()
}
assertWithMessage("onOptionsMenuClosed was not called")
.that(fragment.onOptionsMenuClosedCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
.isTrue()
+
+ fm.beginTransaction()
+ .remove(fragment)
+ .commit()
+ activityRule.executePendingTransactions()
+ assertWithMessage("Removing fragment with options menu should invalidate menu")
+ .that(activity.invalidateCount).isEqualTo(3)
}
@Suppress("DEPRECATION")
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
index 6e80faa..ccc564c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
@@ -31,6 +31,7 @@
class FragmentTestActivity : FragmentActivity(R.layout.activity_content) {
val finishCountDownLatch = CountDownLatch(1)
+ var invalidateCount = 0
override fun finish() {
super.finish()
@@ -44,6 +45,11 @@
.commitNow()
}
+ override fun invalidateMenu() {
+ invalidateCount++
+ super.invalidateMenu()
+ }
+
class ParentFragment : Fragment() {
var wasAttachedInTime: Boolean = false
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index c373049..be8ae42 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -390,7 +390,7 @@
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
public void supportInvalidateOptionsMenu() {
- invalidateOptionsMenu();
+ invalidateMenu();
}
/**
@@ -753,7 +753,7 @@
@Override
public void invalidateMenu() {
- FragmentActivity.this.invalidateOptionsMenu();
+ FragmentActivity.this.invalidateMenu();
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index c84669f..01d5094 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -3007,7 +3007,7 @@
onPictureInPictureModeChangedProvider.removeOnPictureInPictureModeChangedListener(
mOnPictureInPictureModeChangedListener);
}
- if (mHost instanceof MenuHost) {
+ if (mHost instanceof MenuHost && mParent == null) {
((MenuHost) mHost).removeMenuProvider(mMenuProvider);
}
mHost = null;
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 5cbfdee..ea9eb2d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
+<!-- To regenerate this file, run development/update-verification-metadata.sh -->
+
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.2.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
index 2148f53..ea51584 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
@@ -21,6 +21,8 @@
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.units.Energy
import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.calories
+import androidx.health.connect.client.units.grams
import java.time.Instant
import java.time.ZoneOffset
@@ -128,6 +130,49 @@
init {
require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
+
+ biotin?.requireInRange(MIN_MASS, MAX_MASS_100, "biotin")
+ caffeine?.requireInRange(MIN_MASS, MAX_MASS_100, "caffeine")
+ calcium?.requireInRange(MIN_MASS, MAX_MASS_100, "calcium")
+ energy?.requireInRange(MIN_ENERGY, MAX_ENERGY, "energy")
+ energyFromFat?.requireInRange(MIN_ENERGY, MAX_ENERGY, "energyFromFat")
+ chloride?.requireInRange(MIN_MASS, MAX_MASS_100, "chloride")
+ cholesterol?.requireInRange(MIN_MASS, MAX_MASS_100, "cholesterol")
+ chromium?.requireInRange(MIN_MASS, MAX_MASS_100, "chromium")
+ copper?.requireInRange(MIN_MASS, MAX_MASS_100, "copper")
+ dietaryFiber?.requireInRange(MIN_MASS, MAX_MASS_100K, "dietaryFiber")
+ folate?.requireInRange(MIN_MASS, MAX_MASS_100, "chloride")
+ folicAcid?.requireInRange(MIN_MASS, MAX_MASS_100, "folicAcid")
+ iodine?.requireInRange(MIN_MASS, MAX_MASS_100, "iodine")
+ iron?.requireInRange(MIN_MASS, MAX_MASS_100, "iron")
+ magnesium?.requireInRange(MIN_MASS, MAX_MASS_100, "magnesium")
+ manganese?.requireInRange(MIN_MASS, MAX_MASS_100, "manganese")
+ molybdenum?.requireInRange(MIN_MASS, MAX_MASS_100, "molybdenum")
+ monounsaturatedFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "monounsaturatedFat")
+ niacin?.requireInRange(MIN_MASS, MAX_MASS_100, "niacin")
+ pantothenicAcid?.requireInRange(MIN_MASS, MAX_MASS_100, "pantothenicAcid")
+ phosphorus?.requireInRange(MIN_MASS, MAX_MASS_100, "phosphorus")
+ polyunsaturatedFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "polyunsaturatedFat")
+ potassium?.requireInRange(MIN_MASS, MAX_MASS_100, "potassium")
+ protein?.requireInRange(MIN_MASS, MAX_MASS_100K, "protein")
+ riboflavin?.requireInRange(MIN_MASS, MAX_MASS_100, "riboflavin")
+ saturatedFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "saturatedFat")
+ selenium?.requireInRange(MIN_MASS, MAX_MASS_100, "selenium")
+ sodium?.requireInRange(MIN_MASS, MAX_MASS_100, "sodium")
+ sugar?.requireInRange(MIN_MASS, MAX_MASS_100K, "sugar")
+ thiamin?.requireInRange(MIN_MASS, MAX_MASS_100, "thiamin")
+ totalCarbohydrate?.requireInRange(MIN_MASS, MAX_MASS_100K, "totalCarbohydrate")
+ totalFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "totalFat")
+ transFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "transFat")
+ unsaturatedFat?.requireInRange(MIN_MASS, MAX_MASS_100K, "unsaturatedFat")
+ vitaminA?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminA")
+ vitaminB12?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminB12")
+ vitaminB6?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminB6")
+ vitaminC?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminC")
+ vitaminD?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminD")
+ vitaminE?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminE")
+ vitaminK?.requireInRange(MIN_MASS, MAX_MASS_100, "vitaminK")
+ zinc?.requireInRange(MIN_MASS, MAX_MASS_100, "zinc")
}
/*
@@ -249,6 +294,13 @@
companion object {
private const val TYPE_NAME = "Nutrition"
+ private val MIN_MASS = 0.grams
+ private val MAX_MASS_100 = 100.grams
+ private val MAX_MASS_100K = 100_000.grams
+
+ private val MIN_ENERGY = 0.calories
+ private val MAX_ENERGY = 100_000_000.calories
+
/**
* Metric identifier to retrieve the total biotin from
* [androidx.health.connect.client.aggregate.AggregationResult].
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
index a6cb11b..f3b87d4 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
@@ -39,3 +39,8 @@
internal fun Map<String, Int>.reverse(): Map<Int, String> {
return entries.associateBy({ it.value }, { it.key })
}
+
+internal fun <T : Comparable<T>> T.requireInRange(min: T, max: T, name: String) {
+ requireNotLess(min, name)
+ requireNotMore(max, name)
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/NutritionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/NutritionRecordTest.kt
index c1e0940..d2ffee1 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/NutritionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/NutritionRecordTest.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client.records
+import androidx.health.connect.client.units.Energy
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import java.time.Instant
@@ -34,6 +35,7 @@
startZoneOffset = null,
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
+ energy = Energy.calories(5.0)
)
)
.isEqualTo(
@@ -42,11 +44,34 @@
startZoneOffset = null,
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
+ energy = Energy.calories(5.0)
)
)
}
@Test
+ fun invalidNutritionValue_throws() {
+ assertFailsWith<IllegalArgumentException> {
+ NutritionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ energy = Energy.calories(-1.0)
+ )
+ }
+ assertFailsWith<IllegalArgumentException> {
+ NutritionRecord(
+ startTime = Instant.ofEpochMilli(1234L),
+ startZoneOffset = null,
+ endTime = Instant.ofEpochMilli(1236L),
+ endZoneOffset = null,
+ energy = Energy.calories(100000001.0)
+ )
+ }
+ }
+
+ @Test
fun invalidTimes_throws() {
assertFailsWith<IllegalArgumentException> {
NutritionRecord(
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 0ab9380..7fe685c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -78,7 +78,6 @@
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getContainingUMethod
-import org.jetbrains.uast.isNullLiteral
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
import org.jetbrains.uast.toUElement
@@ -165,7 +164,6 @@
* This can be fixed by adding an explicit cast to the expected type.
*/
override fun visitExpression(node: UExpression) {
- val apiDatabase = apiDatabase ?: return
val psi = node.sourcePsi
// Some PsiElements are multiple UElements. Only inspect one to prevent duplicate issues
if (psi != null && psi.toUElement() != node) return
@@ -173,26 +171,18 @@
// Get the expression type and expected type based on the expression's usage
val actualType = node.getExpressionType() ?: return
val actualTypeStr = actualType.canonicalText
- if (!apiDatabase.containsClass(actualTypeStr)) return
- val actualTypeApi = apiDatabase.getClassVersions(actualTypeStr).min()
+ val actualTypeApi = getMinApiOfClass(actualTypeStr) ?: return
// Not an issue if this is contained within a class annotated with @RequiresApi
if (node.containedInRequiresApiClass(actualTypeApi)) return
val expectedType = getExpectedTypeByParent(node) ?: return
val expectedTypeStr = expectedType.canonicalText
+ val expectedTypeApi = getMinApiOfClass(expectedTypeStr) ?: return
- // Casting to object is always safe
- if (expectedTypeStr == "java.lang.Object") return
-
- // If the types aren't equal, we are casting from actualType to expectedType implicitly
- if (actualTypeStr == expectedTypeStr) return
-
- if (!apiDatabase.containsClass(expectedTypeStr)) return
- val expectedTypeApi = apiDatabase.getClassVersions(expectedTypeStr).min()
-
- // Only an issue if there's an API where expectedType exists but actualType doesn't
- if (actualTypeApi <= expectedTypeApi) return
+ if (!isInvalidCast(actualTypeStr, expectedTypeStr, actualTypeApi, expectedTypeApi)) {
+ return
+ }
// This can currently only do an autofix for Java source, not Kotlin.
val fix = if (isKotlin(node.lang)) {
@@ -595,7 +585,8 @@
generateWrapperMethod(
method,
// Find what type the result of this call is used as
- getExpectedTypeByParent(call)
+ getExpectedTypeByParent(call),
+ call.valueArguments.map { it.getExpressionType() }
) ?: return null
val (wrapperClassName, insertionPoint, insertionSource) = generateInsertionSource(
@@ -611,8 +602,7 @@
call.receiver,
call.valueArguments,
wrapperClassName,
- wrapperMethodName,
- wrapperMethodParams
+ wrapperMethodName
)
return createCompositeFix(call, replacementCall, insertionPoint, insertionSource)
@@ -707,7 +697,14 @@
val paramIndex = parent.expressions.indexOf(childOfParent)
if (paramIndex < 0) return null
val method = grandparent.resolveMethod() ?: return null
- return method.parameterList.getParameter(paramIndex)?.type
+ val finalParamIndex = method.parameters.size - 1
+ // Special case the last arguments to a varargs method
+ return if (method.isVarArgs && paramIndex >= finalParamIndex) {
+ (method.parameterList.getParameter(finalParamIndex)?.type
+ as? PsiEllipsisType)?.componentType
+ } else {
+ method.parameterList.getParameter(paramIndex)?.type
+ }
}
} else if (parent is KtValueArgument) {
// Handles the case when the element is an argument in a Kotlin method call.
@@ -841,7 +838,6 @@
* @param callValueArguments Arguments of the call to the platform method
* @param wrapperClassName Name of the generated wrapper class
* @param wrapperMethodName Name of the generated wrapper method
- * @param wrapperMethodParams Param types of the wrapper method
* @return Source code for a call to the static wrapper method
*/
private fun generateWrapperCall(
@@ -849,8 +845,7 @@
callReceiver: UExpression?,
callValueArguments: List<UExpression>,
wrapperClassName: String,
- wrapperMethodName: String,
- wrapperMethodParams: List<PsiType>
+ wrapperMethodName: String
): String {
val callReceiverStr = when {
// Static method
@@ -863,34 +858,15 @@
// it must be a call to an instance method using `this` implicitly.
callReceiver == null ->
"this"
- // Use the original call receiver string (removing extra parens), casting if needed
+ // Otherwise, use the original call receiver string (removing extra parens)
else ->
- createArgumentString(unwrapExpression(callReceiver), wrapperMethodParams[0])
+ unwrapExpression(callReceiver).asSourceString()
}
val callValues = if (callValueArguments.isNotEmpty()) {
- // The first element in the wrapperMethodParams is the receiver, so drop that.
- // Also drop the last parameter, because it is special-cased later.
- val paramTypesWithoutReceiverAndFinal = wrapperMethodParams.drop(1).dropLast(1)
- // For varargs methods, what we care about for the last type is the type of each
- // vararg, not the containing type.
- val finalParamType = if (method.isVarArgs) {
- (wrapperMethodParams.last() as PsiEllipsisType).componentType
- } else {
- wrapperMethodParams.last()
+ callValueArguments.joinToString(separator = ", ") { argument ->
+ argument.asSourceString()
}
-
- callValueArguments.mapIndexed { argIndex, arg ->
- // The number of args might be greater than the number of param types due to
- // varargs, repeat the final param type for all args after exhausting the
- // paramTypesWithoutReceiverAndFinal list.
- val expectedType = if (argIndex < paramTypesWithoutReceiverAndFinal.size) {
- paramTypesWithoutReceiverAndFinal[argIndex]
- } else {
- finalParamType
- }
- createArgumentString(arg, expectedType)
- }.joinToString(separator = ", ")
} else {
null
}
@@ -901,24 +877,6 @@
}
/**
- * Creates the string representation of an argument in the wrapper call. If the type of the
- * arg is not identical to the parameter type of the wrapper method, casts to that type.
- */
- private fun createArgumentString(arg: UExpression, expectedType: PsiType): String {
- val argType = arg.getExpressionType()
- val expectedTypeText = expectedType.canonicalText
- // If the arg is the expected type, use as normal, otherwise, cast to expected type.
- // Uses text-base equality instead of directly comparing types because certain types
- // (eq. java.lang.Class<T>) are not necessarily equal to instances of the same type.
- // There isn't really a point in casting if the arg is null.
- return if (argType?.equalsToText(expectedTypeText) == true || arg.isNullLiteral()) {
- arg.asSourceString()
- } else {
- "($expectedTypeText) ${arg.asSourceString()}"
- }
- }
-
- /**
* Remove parentheses from the expression (unwrap the expression until it is no longer a
* UParenthesizedExpression).
*/
@@ -950,7 +908,8 @@
*/
private fun generateWrapperMethod(
method: PsiMethod,
- expectedReturnType: PsiType?
+ expectedReturnType: PsiType?,
+ expectedParamTypes: List<PsiType?>
): Triple<String, List<PsiType>, String>? {
val evaluator = context.evaluator
val isStatic = evaluator.isStatic(method)
@@ -972,13 +931,26 @@
""
}
- val typedParams = method.parameters.map { param ->
- "${(param.type as? PsiType)?.canonicalText} ${param.name}"
+ val paramsWithTypes = method.parameters.mapIndexed { i, param ->
+ // It's possible for i to be out of bounds due to varargs
+ val expectedType = expectedParamTypes.getOrNull(i)
+ val actualType = (param.type as? PsiType) ?: return null
+ // If the actual type isn't a PsiEllipsisType (the method is varargs) and casting
+ // from the expected to the actual type would be an invalid implicit cast, use
+ // the expected type. Otherwise, use the actual type.
+ val typeToUse = if (expectedType != null && actualType !is PsiEllipsisType &&
+ isInvalidCast(expectedType.canonicalText, actualType.canonicalText)) {
+ expectedType
+ } else {
+ actualType
+ }
+ Pair(typeToUse, param.name)
}
- val typedParamsStr = (listOfNotNull(hostParam) + typedParams).joinToString(", ")
+ val paramStrings = paramsWithTypes.map { (type, name) -> "${type.canonicalText} $name" }
+ val typedParamsStr = (listOfNotNull(hostParam) + paramStrings).joinToString(", ")
val paramTypes = listOf(PsiTypesUtil.getClassType(containingClass)) +
- getParameterTypes(method)
+ paramsWithTypes.map { (type, _) -> type }
val namedParamsStr = method.parameters.joinToString(separator = ", ") { param ->
"${param.name}"
@@ -1059,6 +1031,33 @@
}
/**
+ * Checks if a cast from [fromType] to [toType] would be invalid, which is true when there
+ * is an API level where [toType] exists but [fromType] does now.
+ *
+ * Allows optionally passing in [knownFromTypeApi] and [knownToTypeApi], the API levels
+ * when [fromType] and [toType] were introduced, respectively, if that has already been
+ * computed. The values will be looked up if not passed in.
+ */
+ private fun isInvalidCast(
+ fromType: String,
+ toType: String,
+ knownFromTypeApi: Int? = null,
+ knownToTypeApi: Int? = null
+ ): Boolean {
+ // Casting to object is always safe
+ if (toType == "java.lang.Object") return false
+
+ // If the types aren't equal, we are casting from actualType to expectedType implicitly
+ if (fromType == toType) return false
+
+ val fromTypeApi = knownFromTypeApi ?: getMinApiOfClass(fromType) ?: return false
+ val toTypeApi = knownToTypeApi ?: getMinApiOfClass(toType) ?: return false
+
+ // Only an issue if there's an API where expectedType exists but actualType doesn't
+ return fromTypeApi > toTypeApi
+ }
+
+ /**
* Returns a list of the method's parameter types.
*/
private fun getParameterTypes(method: PsiMethod): List<PsiType> =
@@ -1075,6 +1074,15 @@
return version <= minSdk
}
+ /**
+ * Returns the API level this class was introduced at, or null if unknown.
+ */
+ private fun getMinApiOfClass(className: String): Int? {
+ val apiDatabase = apiDatabase ?: return null
+ if (!apiDatabase.containsClass(className)) return null
+ return apiDatabase.getClassVersions(className).min()
+ }
+
private fun getInheritanceChain(
derivedClass: PsiClassType,
baseClass: PsiClassType?
diff --git a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
index 76843dc..706cd32 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
@@ -82,7 +82,7 @@
Fix for src/androidx/sample/core/widget/ListViewCompat.java line 39: Extract to static inner class:
@@ -39 +39
- listView.scrollListBy(y);
-+ Api19Impl.scrollListBy((android.widget.AbsListView) listView, y);
++ Api19Impl.scrollListBy(listView, y);
@@ -91 +91
+ @androidx.annotation.RequiresApi(19)
+ static class Api19Impl {
@@ -100,7 +100,7 @@
Fix for src/androidx/sample/core/widget/ListViewCompat.java line 69: Extract to static inner class:
@@ -69 +69
- return listView.canScrollList(direction);
-+ return Api19Impl.canScrollList((android.widget.AbsListView) listView, direction);
++ return Api19Impl.canScrollList(listView, direction);
@@ -91 +91
+ @androidx.annotation.RequiresApi(19)
+ static class Api19Impl {
@@ -530,7 +530,7 @@
Fix for src/androidx/AutofixUnsafeCallToThis.java line 48: Extract to static inner class:
@@ -48 +48
- this.getClipToPadding();
-+ Api21Impl.getClipToPadding((ViewGroup) this);
++ Api21Impl.getClipToPadding(this);
@@ -60 +60
+ @androidx.annotation.RequiresApi(21)
+ static class Api21Impl {
@@ -792,7 +792,7 @@
Fix for src/androidx/AutofixUnsafeCallWithImplicitParamCast.java line 34: Extract to static inner class:
@@ -34 +34
- style.setBuilder(builder);
-+ Api16Impl.setBuilder((Notification.Style) style, builder);
++ Api16Impl.setBuilder(style, builder);
@@ -45 +45
+ @RequiresApi(16)
+ static class Api16Impl {
@@ -810,7 +810,7 @@
Fix for src/androidx/AutofixUnsafeCallWithImplicitParamCast.java line 43: Extract to static inner class:
@@ -43 +43
- builder.extend(extender);
-+ Api20Impl.extend(builder, (Notification.Extender) extender);
++ Api20Impl.extend(builder, extender);
@@ -45 +45
+ @RequiresApi(20)
+ static class Api20Impl {
@@ -819,7 +819,7 @@
+ }
+
+ @DoNotInline
-+ static Notification.Builder extend(Notification.Builder builder, Notification.Extender extender) {
++ static Notification.Builder extend(Notification.Builder builder, Notification.CarExtender extender) {
+ return builder.extend(extender);
+ }
+
@@ -874,7 +874,7 @@
Fix for src/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java line 43: Extract to static inner class:
@@ -43 +43
- adapter.setAutofillOptions(vararg);
-+ Api27Impl.setAutofillOptions(adapter, (java.lang.CharSequence) vararg);
++ Api27Impl.setAutofillOptions(adapter, vararg);
@@ -54 +54
+ @RequiresApi(27)
+ static class Api27Impl {
@@ -892,7 +892,7 @@
Fix for src/androidx/AutofixOnUnsafeCallWithImplicitVarArgsCast.java line 52: Extract to static inner class:
@@ -52 +52
- adapter.setAutofillOptions(vararg1, vararg2, vararg3);
-+ Api27Impl.setAutofillOptions(adapter, (java.lang.CharSequence) vararg1, (java.lang.CharSequence) vararg2, (java.lang.CharSequence) vararg3);
++ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
@@ -54 +54
+ @RequiresApi(27)
+ static class Api27Impl {
diff --git a/lint-checks/src/test/java/androidx/build/lint/ImplicitCastVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ImplicitCastVerificationFailureDetectorTest.kt
index 1689e1f..fd1e150 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ImplicitCastVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ImplicitCastVerificationFailureDetectorTest.kt
@@ -667,4 +667,107 @@
check(*input).expectClean()
}
+
+ @Test
+ fun `Unsafe implicit cast to varargs method`() {
+ val input = arrayOf(
+ java("""
+ package java.androidx;
+
+ import android.icu.number.FormattedNumber;
+ import android.widget.BaseAdapter;
+ import androidx.annotation.DoNotInline;
+ import androidx.annotation.RequiresApi;
+
+ public class UnsafeCastToVarargs() {
+ @RequiresApi(30)
+ public void callVarArgsMethod(BaseAdapter adapter, FormattedNumber vararg1, FormattedNumber vararg2, FormattedNumber vararg3) {
+ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
+ }
+
+ @RequiresApi(27)
+ static class Api27Impl {
+ private Api27Impl() {}
+ @DoNotInline
+ static void setAutofillOptions(BaseAdapter baseAdapter, CharSequence... options) {
+ baseAdapter.setAutofillOptions(baseAdapter, options);
+ }
+ }
+ }
+ """.trimIndent())
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/java/androidx/UnsafeCastToVarargs.java:11: Error: This expression has type android.icu.number.FormattedNumber (introduced in API level 30) but it used as type java.lang.CharSequence (introduced in API level 1). Run-time class verification will not be able to validate this implicit cast on devices between these API levels. [ImplicitCastClassVerificationFailure]
+ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
+ ~~~~~~~
+src/java/androidx/UnsafeCastToVarargs.java:11: Error: This expression has type android.icu.number.FormattedNumber (introduced in API level 30) but it used as type java.lang.CharSequence (introduced in API level 1). Run-time class verification will not be able to validate this implicit cast on devices between these API levels. [ImplicitCastClassVerificationFailure]
+ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
+ ~~~~~~~
+src/java/androidx/UnsafeCastToVarargs.java:11: Error: This expression has type android.icu.number.FormattedNumber (introduced in API level 30) but it used as type java.lang.CharSequence (introduced in API level 1). Run-time class verification will not be able to validate this implicit cast on devices between these API levels. [ImplicitCastClassVerificationFailure]
+ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
+ ~~~~~~~
+3 errors, 0 warnings
+ """
+ val expectedFixDiffs = """
+Fix for src/java/androidx/UnsafeCastToVarargs.java line 11: Extract to static inner class:
+@@ -11 +11
+- Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
++ Api27Impl.setAutofillOptions(adapter, Api30Impl.castToCharSequence(vararg1), vararg2, vararg3);
+@@ -22 +22
++ @RequiresApi(30)
++ static class Api30Impl {
++ private Api30Impl() {
++ // This class is not instantiable.
++ }
++
++ @DoNotInline
++ static java.lang.CharSequence castToCharSequence(FormattedNumber formattedNumber) {
++ return formattedNumber;
++ }
++
+@@ -23 +34
++ }
+Fix for src/java/androidx/UnsafeCastToVarargs.java line 11: Extract to static inner class:
+@@ -11 +11
+- Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
++ Api27Impl.setAutofillOptions(adapter, vararg1, Api30Impl.castToCharSequence(vararg2), vararg3);
+@@ -22 +22
++ @RequiresApi(30)
++ static class Api30Impl {
++ private Api30Impl() {
++ // This class is not instantiable.
++ }
++
++ @DoNotInline
++ static java.lang.CharSequence castToCharSequence(FormattedNumber formattedNumber) {
++ return formattedNumber;
++ }
++
+@@ -23 +34
++ }
+Fix for src/java/androidx/UnsafeCastToVarargs.java line 11: Extract to static inner class:
+@@ -11 +11
+- Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, vararg3);
++ Api27Impl.setAutofillOptions(adapter, vararg1, vararg2, Api30Impl.castToCharSequence(vararg3));
+@@ -22 +22
++ @RequiresApi(30)
++ static class Api30Impl {
++ private Api30Impl() {
++ // This class is not instantiable.
++ }
++
++ @DoNotInline
++ static java.lang.CharSequence castToCharSequence(FormattedNumber formattedNumber) {
++ return formattedNumber;
++ }
++
+@@ -23 +34
++ }
+ """
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
}
\ No newline at end of file
diff --git a/metrics/integration-tests/build.gradle b/metrics/integration-tests/build.gradle
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/metrics/integration-tests/build.gradle
diff --git a/navigation/integration-tests/build.gradle b/navigation/integration-tests/build.gradle
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/integration-tests/build.gradle
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index b3bfeb2e..5ff82ae 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -661,7 +661,7 @@
@UiThreadTest
@Test
- fun entryResumedWhenRestoredState() {
+ fun testEntryResumedWhenRestoredState() {
val entry = createBackStackEntry()
// First push an initial Fragment
@@ -719,6 +719,270 @@
assertThat(restoredEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
}
+ @LargeTest
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ fun testEntryStatesWithAnimationAfterReconfiguration() {
+ withUse(ActivityScenario.launch(NavigationActivity::class.java)) {
+ val navController1 = withActivity { findNavController(R.id.nav_host) }
+ val fragNavigator1 = navController1.navigatorProvider.getNavigator(
+ FragmentNavigator::class.java
+ )
+
+ // navigated to startDestination -- assert states
+ assertThat(fragNavigator1.backStack.value.size).isEqualTo(1)
+ val entry1 = fragNavigator1.backStack.value[0]
+ val fm1 = withActivity {
+ supportFragmentManager.findFragmentById(R.id.nav_host)!!.childFragmentManager
+ .also { it.executePendingTransactions() }
+ }
+
+ val fragment1 = fm1.findFragmentByTag(entry1.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment1)
+ .isNotNull()
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // use animation
+ val options = navOptions {
+ anim {
+ enter = R.anim.fade_enter
+ exit = R.anim.fade_exit
+ popEnter = R.anim.fade_enter
+ popExit = R.anim.fade_exit
+ }
+ }
+
+ // navigate to second destination -- assert states
+ onActivity {
+ navController1.navigate(R.id.empty_fragment, null, options)
+ fm1.executePendingTransactions()
+ }
+ assertThat(fragNavigator1.backStack.value.size).isEqualTo(2)
+ val entry2 = fragNavigator1.backStack.value[1]
+ val fragment2 = fm1.findFragmentByTag(entry2.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment2)
+ .isNotNull()
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // recreate activity - imitate configuration change
+ recreate()
+
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+ // get restored components
+ val fm2 = withActivity {
+ supportFragmentManager.findFragmentById(R.id.nav_host)!!.childFragmentManager
+ .also { it.executePendingTransactions() }
+ }
+ val navController2 = withActivity { findNavController(R.id.nav_host) }
+ val fragNavigator2 = navController2.navigatorProvider.getNavigator(
+ FragmentNavigator::class.java
+ )
+ assertThat(fm2).isNotEqualTo(fm1)
+ assertThat(navController2).isNotEqualTo(navController1)
+ assertThat(fragNavigator2).isNotEqualTo(fragNavigator1)
+ assertThat(fragNavigator2.backStack.value.size).isEqualTo(2)
+
+ // check that entries are restored to correct states
+ val entry1Restored = fragNavigator2.backStack.value[0]
+ val entry2Restored = fragNavigator2.backStack.value[1]
+ assertThat(entry1Restored.id).isEqualTo(entry1.id)
+ assertThat(entry2Restored.id).isEqualTo(entry2.id)
+ assertThat(entry1Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(entry2Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // check that fragments have been restored
+ val fragment1Restored = fm2.findFragmentByTag(entry1.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment1Restored)
+ .isNotNull()
+ val fragment2Restored = fm2.findFragmentByTag(entry2.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment2Restored)
+ .isNotNull()
+
+ // attach ON_DESTROY listeners which should be triggered when we pop
+ var entry2RestoredDestroyed = false
+ val countDownLatch = CountDownLatch(1)
+ onActivity {
+ fragment2Restored?.viewLifecycleOwner?.lifecycle?.addObserver(
+ object : LifecycleEventObserver {
+ override fun onStateChanged(
+ source: LifecycleOwner,
+ event: Lifecycle.Event
+ ) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ countDownLatch.countDown()
+ }
+ }
+ }
+ )
+ entry2Restored.lifecycle.addObserver(object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ entry2RestoredDestroyed = true
+ }
+ }
+ })
+ }
+
+ // pop backstack
+ onActivity {
+ navController2.popBackStack(entry2Restored.destination.id, true)
+ }
+
+ assertThat(countDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+ onActivity {
+ fm2.executePendingTransactions()
+ }
+
+ // assert popped states
+ assertThat(entry2RestoredDestroyed).isTrue()
+ assertThat(entry1Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(fragNavigator2.backStack.value).containsExactly(entry1Restored)
+ // navController backstack is updated properly. Contains graph root entry & entry1
+ assertThat(navController2.currentBackStack.value.size).isEqualTo(2)
+ assertThat(navController2.currentBackStack.value.last()).isEqualTo(entry1Restored)
+ }
+ }
+
+ @LargeTest
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ fun testEntryStatesWithAnimatorAfterReconfiguration() {
+ withUse(ActivityScenario.launch(NavigationActivity::class.java)) {
+ val navController1 = withActivity { findNavController(R.id.nav_host) }
+ val fragNavigator1 = navController1.navigatorProvider.getNavigator(
+ FragmentNavigator::class.java
+ )
+
+ // navigated to startDestination -- assert states
+ assertThat(fragNavigator1.backStack.value.size).isEqualTo(1)
+ val entry1 = fragNavigator1.backStack.value[0]
+ val fm1 = withActivity {
+ supportFragmentManager.findFragmentById(R.id.nav_host)!!.childFragmentManager
+ .also { it.executePendingTransactions() }
+ }
+
+ val fragment1 = fm1.findFragmentByTag(entry1.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment1)
+ .isNotNull()
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // use animator
+ val options = navOptions {
+ anim {
+ enter = R.animator.fade_enter
+ exit = R.animator.fade_exit
+ popEnter = R.animator.fade_enter
+ popExit = R.animator.fade_exit
+ }
+ }
+
+ // navigate to second destination -- assert states
+ onActivity {
+ navController1.navigate(R.id.empty_fragment, null, options)
+ fm1.executePendingTransactions()
+ }
+ assertThat(fragNavigator1.backStack.value.size).isEqualTo(2)
+ val entry2 = fragNavigator1.backStack.value[1]
+ val fragment2 = fm1.findFragmentByTag(entry2.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment2)
+ .isNotNull()
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // recreate activity - imitate configuration change
+ recreate()
+
+ assertThat(entry1.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ assertThat(entry2.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+ // get restored components
+ val fm2 = withActivity {
+ supportFragmentManager.findFragmentById(R.id.nav_host)!!.childFragmentManager
+ .also { it.executePendingTransactions() }
+ }
+ val navController2 = withActivity { findNavController(R.id.nav_host) }
+ val fragNavigator2 = navController2.navigatorProvider.getNavigator(
+ FragmentNavigator::class.java
+ )
+ assertThat(fm2).isNotEqualTo(fm1)
+ assertThat(navController2).isNotEqualTo(navController1)
+ assertThat(fragNavigator2).isNotEqualTo(fragNavigator1)
+ assertThat(fragNavigator2.backStack.value.size).isEqualTo(2)
+
+ // check that entries are restored to correct states
+ val entry1Restored = fragNavigator2.backStack.value[0]
+ val entry2Restored = fragNavigator2.backStack.value[1]
+ assertThat(entry1Restored.id).isEqualTo(entry1.id)
+ assertThat(entry2Restored.id).isEqualTo(entry2.id)
+ assertThat(entry1Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ assertThat(entry2Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // check that fragments have been restored
+ val fragment1Restored = fm2.findFragmentByTag(entry1.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment1Restored)
+ .isNotNull()
+ val fragment2Restored = fm2.findFragmentByTag(entry2.id)
+ assertWithMessage("Fragment should be added")
+ .that(fragment2Restored)
+ .isNotNull()
+
+ // attach ON_DESTROY listeners which should be triggered when we pop
+ var entry2RestoredDestroyed = false
+ val countDownLatch = CountDownLatch(1)
+ onActivity {
+ fragment2Restored?.viewLifecycleOwner?.lifecycle?.addObserver(
+ object : LifecycleEventObserver {
+ override fun onStateChanged(
+ source: LifecycleOwner,
+ event: Lifecycle.Event
+ ) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ countDownLatch.countDown()
+ }
+ }
+ }
+ )
+ entry2Restored.lifecycle.addObserver(object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ entry2RestoredDestroyed = true
+ }
+ }
+ })
+ }
+
+ // pop backstack
+ onActivity {
+ navController2.popBackStack(entry2Restored.destination.id, true)
+ }
+
+ assertThat(countDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+ onActivity {
+ fm2.executePendingTransactions()
+ }
+
+ // assert popped states
+ assertThat(entry2RestoredDestroyed).isTrue()
+ assertThat(entry1Restored.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(fragNavigator2.backStack.value).containsExactly(entry1Restored)
+ // navController backstack is updated properly. Contains graph root entry & entry1
+ assertThat(navController2.currentBackStack.value.size).isEqualTo(2)
+ assertThat(navController2.currentBackStack.value.last()).isEqualTo(entry1Restored)
+ }
+ }
+
@UiThreadTest
@Test
fun testPopUpToDestroysIntermediateEntries() {
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 66de182..c9eda60 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -17,19 +17,23 @@
package androidx.navigation.fragment
import android.app.Dialog
-import androidx.fragment.app.DialogFragment
import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavOptions
import androidx.navigation.fragment.test.NavigationActivity
+import androidx.navigation.fragment.test.R
+import androidx.navigation.navOptions
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.navigation.fragment.test.R
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -60,6 +64,59 @@
}
@Test
+ fun fragmentNavigateClearBackStack() = withNavigationActivity {
+ navController.setGraph(R.navigation.nav_simple)
+
+ val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+ fm?.executePendingTransactions()
+
+ val navigator = navController.navigatorProvider.getNavigator(FragmentNavigator::class.java)
+ assertThat(navigator.backStack.value.size).isEqualTo(1)
+
+ navController.navigate(
+ R.id.empty_fragment,
+ null,
+ )
+ fm?.executePendingTransactions()
+
+ assertThat(navigator.backStack.value.size).isEqualTo(2)
+ val originalBackStackEntry = navController.currentBackStackEntry!!
+ val originalEntryViewModel = ViewModelProvider(originalBackStackEntry)[
+ TestClearViewModel::class.java
+ ]
+ val originalFragment = fm?.findFragmentById(R.id.nav_host) as Fragment
+ val originalFragmentViewModel = ViewModelProvider(originalFragment)[
+ TestClearViewModel::class.java
+ ]
+
+ navController.navigate(
+ R.id.empty_fragment_2,
+ null,
+ navOptions {
+ popUpTo(R.id.empty_fragment) {
+ inclusive = true
+ saveState = true
+ }
+ }
+ )
+ fm.executePendingTransactions()
+
+ val currentTopFragment = fm.findFragmentById(R.id.nav_host)
+
+ assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.empty_fragment_2)
+ assertThat(navigator.backStack.value.size).isEqualTo(2)
+
+ navController.clearBackStack(R.id.empty_fragment)
+ fm.executePendingTransactions()
+ // clearing the back stack does not change the current fragment
+ assertThat(fm.findFragmentById(R.id.nav_host)).isEqualTo(currentTopFragment)
+ assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.empty_fragment_2)
+ assertThat(navigator.backStack.value.size).isEqualTo(2)
+ assertThat(originalFragmentViewModel.cleared).isTrue()
+ assertThat(originalEntryViewModel.cleared).isTrue()
+ }
+
+ @Test
fun dialogFragmentNavigate_singleTop() = withNavigationActivity {
val navigator =
navController.navigatorProvider.getNavigator(DialogFragmentNavigator::class.java)
@@ -235,4 +292,12 @@
dialogs.add(dialog)
return dialog
}
-}
\ No newline at end of file
+}
+
+class TestClearViewModel : ViewModel() {
+ var cleared = false
+
+ override fun onCleared() {
+ cleared = true
+ }
+}
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index 9b31739..2c9cbe7 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -30,6 +30,8 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
@@ -37,6 +39,7 @@
import androidx.navigation.NavigatorProvider
import androidx.navigation.NavigatorState
import androidx.navigation.fragment.FragmentNavigator.Destination
+import java.lang.ref.WeakReference
/**
* Navigator that navigates through [fragment transactions][FragmentTransaction]. Every
@@ -103,6 +106,13 @@
}
}
fragment.lifecycle.addObserver(fragmentObserver)
+ // We need to ensure that if the fragment has its state saved and then that state
+ // later cleared without the restoring the fragment that we also clear the state
+ // of the associated entry.
+ val viewModel = ViewModelProvider(fragment)[ClearEntryStateViewModel::class.java]
+ val entry = state.backStack.value.lastOrNull { it.id == fragment.tag }
+ viewModel.completeTransition =
+ WeakReference { entry?.let { state.markTransitionComplete(it) } }
}
fragmentManager.addOnBackStackChangedListener(object : OnBackStackChangedListener {
@@ -567,4 +577,12 @@
private const val TAG = "FragmentNavigator"
private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
}
+
+ internal class ClearEntryStateViewModel : ViewModel() {
+ lateinit var completeTransition: WeakReference<() -> Unit>
+ override fun onCleared() {
+ super.onCleared()
+ completeTransition.get()?.invoke()
+ }
+ }
}
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 5a1b31d..bce8cb2 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -807,7 +807,8 @@
navigatorState.values.forEach { state ->
state.isNavigating = true
}
- val restored = restoreStateInternal(destinationId, null, null, null)
+ val restored = restoreStateInternal(destinationId, null,
+ navOptions { restoreState = true }, null)
navigatorState.values.forEach { state ->
state.isNavigating = false
}
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 50a5f9e..d57ea74 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
# Disable docs
androidx.enableDocumentation=false
androidx.playground.snapshotBuildId=9614968
-androidx.playground.metalavaBuildId=9652637
+androidx.playground.metalavaBuildId=9663318
androidx.studio.type=playground
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 22e7245..ff9891a 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -1017,6 +1017,12 @@
Log.d(TAG, "reAttach " + vh);
}
vh.clearTmpDetachFlag();
+ } else {
+ if (sDebugAssertionsEnabled) {
+ throw new IllegalArgumentException(
+ "No ViewHolder found for child: " + child + ", index: " + index
+ + exceptionLabel());
+ }
}
RecyclerView.this.attachViewToParent(child, index, layoutParams);
}
@@ -1036,6 +1042,11 @@
}
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
+ } else {
+ if (sDebugAssertionsEnabled) {
+ throw new IllegalArgumentException(
+ "No view at offset " + offset + exceptionLabel());
+ }
}
RecyclerView.this.detachViewFromParent(offset);
}
@@ -4844,6 +4855,11 @@
throw new IllegalArgumentException("Called removeDetachedView with a view which"
+ " is not flagged as tmp detached." + vh + exceptionLabel());
}
+ } else {
+ if (sDebugAssertionsEnabled) {
+ throw new IllegalArgumentException(
+ "No ViewHolder found for child: " + child + exceptionLabel());
+ }
}
// Clear any android.view.animation.Animation that may prevent the item from
@@ -6615,7 +6631,30 @@
// abort - we have a deadline we can't meet
return false;
}
+
+ // Holders being bound should be either fully attached or fully detached.
+ // We don't want to bind with views that are temporarily detached, because that
+ // creates a situation in which they are unable to reason about their attach state
+ // properly.
+ // For example, isAttachedToWindow will return true, but the itemView will lack a
+ // parent. This breaks, among other possible issues, anything involving traversing
+ // the view tree, such as ViewTreeLifecycleOwner.
+ // Thus, we temporarily reattach any temp-detached holders for the bind operation.
+ // See https://issuetracker.google.com/265347515 for additional details on problems
+ // resulting from this
+ boolean reattachedForBind = false;
+ if (holder.isTmpDetached()) {
+ attachViewToParent(holder.itemView, getChildCount(),
+ holder.itemView.getLayoutParams());
+ reattachedForBind = true;
+ }
+
mAdapter.bindViewHolder(holder, offsetPosition);
+
+ if (reattachedForBind) {
+ detachViewFromParent(holder.itemView);
+ }
+
long endBindNs = getNanoTime();
mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
attachAccessibilityDelegateOnBind(holder);
@@ -7794,6 +7833,23 @@
TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
}
holder.mBindingAdapter = this;
+ if (sDebugAssertionsEnabled) {
+ if (holder.itemView.getParent() == null
+ && (ViewCompat.isAttachedToWindow(holder.itemView)
+ != holder.isTmpDetached())) {
+ throw new IllegalStateException("Temp-detached state out of sync with reality. "
+ + "holder.isTmpDetached(): " + holder.isTmpDetached()
+ + ", attached to window: "
+ + ViewCompat.isAttachedToWindow(holder.itemView)
+ + ", holder: " + holder);
+ }
+ if (holder.itemView.getParent() == null
+ && ViewCompat.isAttachedToWindow(holder.itemView)) {
+ throw new IllegalStateException(
+ "Attempting to bind attached holder with no parent"
+ + " (AKA temp detached): " + holder);
+ }
+ }
onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
if (rootBind) {
holder.clearPayload();
@@ -12197,6 +12253,11 @@
}
void resetInternal() {
+ if (sDebugAssertionsEnabled && isTmpDetached()) {
+ throw new IllegalStateException("Attempting to reset temp-detached ViewHolder: "
+ + this + ". ViewHolders should be fully detached before resetting.");
+ }
+
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
index 5269b23..0c6ab9a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
@@ -55,6 +55,7 @@
import androidx.room.compiler.processing.ksp.KspType
import androidx.room.compiler.processing.ksp.KspTypeElement
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
+import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
@@ -170,6 +171,9 @@
fun XTypeElement.toKS(): KSClassDeclaration = (this as KspTypeElement).declaration
@JvmStatic
+ fun XElement.toKS(): KSAnnotated = (this as KspElement).declaration
+
+ @JvmStatic
fun XExecutableElement.toKS(): KSFunctionDeclaration =
when (this) {
is KspExecutableElement -> this.declaration
@@ -278,4 +282,4 @@
else -> error("Unexpected executable type: $this")
}
}
-}
\ No newline at end of file
+}
diff --git a/room/room-runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/room-runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index e8775c7..ceaacd1 100644
--- a/room/room-runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/room-runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -29,6 +29,7 @@
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -114,6 +115,7 @@
countingTaskExecutorRule.drainTasks(10, TimeUnit.MILLISECONDS)
}
+ @Ignore // b/271325600
@Test
public fun executeRefCountingFunctionPropagatesFailure() {
assertThrows<IOException> {
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 693d7bf..7d956be 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
@@ -24,7 +24,6 @@
import android.app.UiAutomation;
import android.graphics.Point;
-import android.os.SystemClock;
import android.view.KeyEvent;
import android.widget.TextView;
@@ -363,13 +362,12 @@
try {
assertTrue(mDevice.isNaturalOrientation());
assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+
mDevice.setOrientationLeft();
- // Make the device wait for 1 sec for the rotation animation to finish.
- SystemClock.sleep(1_000);
assertFalse(mDevice.isNaturalOrientation());
assertEquals(UiAutomation.ROTATION_FREEZE_90, mDevice.getDisplayRotation());
+
mDevice.setOrientationNatural();
- SystemClock.sleep(1_000);
assertTrue(mDevice.isNaturalOrientation());
} finally {
mDevice.unfreezeRotation();
@@ -382,12 +380,12 @@
try {
assertTrue(mDevice.isNaturalOrientation());
assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+
mDevice.setOrientationRight();
- SystemClock.sleep(1_000);
assertFalse(mDevice.isNaturalOrientation());
assertEquals(UiAutomation.ROTATION_FREEZE_270, mDevice.getDisplayRotation());
+
mDevice.setOrientationNatural();
- SystemClock.sleep(1_000);
assertTrue(mDevice.isNaturalOrientation());
} finally {
mDevice.unfreezeRotation();
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 3d6eee1..379d940 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -480,60 +480,6 @@
}
/**
- * Rotates right and also freezes rotation in that position by
- * disabling the sensors. If you want to un-freeze the rotation
- * and re-enable the sensors see {@link #unfreezeRotation()}. Note
- * that doing so may cause the screen contents to rotate
- * depending on the current physical position of the test device.
- * @throws RemoteException
- */
- public void setRotationRight() {
- getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_270);
- }
-
- /**
- * Rotates left and also freezes rotation in that position by
- * disabling the sensors. If you want to un-freeze the rotation
- * and re-enable the sensors see {@link #unfreezeRotation()}. Note
- * that doing so may cause the screen contents to rotate
- * depending on the current physical position of the test device.
- * @throws RemoteException
- */
- public void setRotationLeft() {
- getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_90);
- }
-
- /**
- * Rotates up and also freezes rotation in that position by
- * disabling the sensors. If you want to un-freeze the rotation
- * and re-enable the sensors see {@link #unfreezeRotation()}. Note
- * that doing so may cause the screen contents to rotate
- * depending on the current physical position of the test device.
- * @throws RemoteException
- */
- public void setRotationNatural() {
- getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
- }
-
- /**
- * Disables the sensors and freezes the device rotation at its
- * current rotation state.
- * @throws RemoteException
- */
- public void freezeRotation() {
- getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
- }
-
- /**
- * Re-enables the sensors and un-freezes the device rotation
- * allowing its contents to rotate with the device physical rotation.
- * @throws RemoteException
- */
- public void unfreezeRotation() {
- getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
- }
-
- /**
* This method simply presses the power button if the screen is OFF else
* it does nothing if the screen is already ON.
* On API 20 or later devices, this will press the wakeup button instead.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index a15835f..a39869e 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -76,6 +76,7 @@
// Use a short timeout after HOME or BACK key presses, as no events might be generated if
// already on the home page or if there is nothing to go back to.
private static final long KEY_PRESS_EVENT_TIMEOUT = 1_000; // ms
+ private static final long ROTATION_TIMEOUT = 1_000; // ms
// Singleton instance.
private static UiDevice sInstance;
@@ -751,19 +752,16 @@
}
/**
- * Check if the device is in its natural orientation. This is determined by checking if the
- * orientation is at 0 or 180 degrees.
- * @return true if it is in natural orientation
+ * @return true if device is in its natural orientation (0 or 180 degrees)
*/
public boolean isNaturalOrientation() {
- waitForIdle();
int ret = getDisplayRotation();
return ret == UiAutomation.ROTATION_FREEZE_0 ||
ret == UiAutomation.ROTATION_FREEZE_180;
}
/**
- * Returns the current rotation of the display, as defined in {@link Surface}
+ * @return the current rotation of the display, as defined in {@link Surface}
*/
public int getDisplayRotation() {
waitForIdle();
@@ -771,66 +769,72 @@
}
/**
- * Disables the sensors and freezes the device rotation at its
- * current rotation state.
- * @throws RemoteException
+ * Freezes the device rotation at its current state.
+ * @throws RemoteException never
*/
public void freezeRotation() throws RemoteException {
Log.d(TAG, "Freezing rotation.");
- getInteractionController().freezeRotation();
+ getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
}
/**
- * Re-enables the sensors and un-freezes the device rotation allowing its contents
- * to rotate with the device physical rotation. During a test execution, it is best to
- * keep the device frozen in a specific orientation until the test case execution has completed.
- * @throws RemoteException
+ * Un-freezes the device rotation allowing its contents to rotate with the device physical
+ * rotation. During testing, it is best to keep the device frozen in a specific orientation.
+ * @throws RemoteException never
*/
public void unfreezeRotation() throws RemoteException {
Log.d(TAG, "Unfreezing rotation.");
- getInteractionController().unfreezeRotation();
+ getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
}
/**
- * Simulates orienting the device to the left and also freezes rotation
- * by disabling the sensors.
- *
- * If you want to un-freeze the rotation and re-enable the sensors
- * see {@link #unfreezeRotation()}.
- * @throws RemoteException
+ * Orients the device to the left and freezes rotation. Use {@link #unfreezeRotation()} to
+ * un-freeze the rotation.
+ * @throws RemoteException never
*/
public void setOrientationLeft() throws RemoteException {
Log.d(TAG, "Setting orientation to left.");
- getInteractionController().setRotationLeft();
- waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ rotate(UiAutomation.ROTATION_FREEZE_90);
}
/**
- * Simulates orienting the device to the right and also freezes rotation
- * by disabling the sensors.
- *
- * If you want to un-freeze the rotation and re-enable the sensors
- * see {@link #unfreezeRotation()}.
- * @throws RemoteException
+ * Orients the device to the right and freezes rotation. Use {@link #unfreezeRotation()} to
+ * un-freeze the rotation.
+ * @throws RemoteException never
*/
public void setOrientationRight() throws RemoteException {
Log.d(TAG, "Setting orientation to right.");
- getInteractionController().setRotationRight();
- waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ rotate(UiAutomation.ROTATION_FREEZE_270);
}
/**
- * Simulates orienting the device into its natural orientation and also freezes rotation
- * by disabling the sensors.
- *
- * If you want to un-freeze the rotation and re-enable the sensors
- * see {@link #unfreezeRotation()}.
- * @throws RemoteException
+ * Orients the device to its natural orientation (0 or 180 degrees) and freezes rotation. Use
+ * {@link #unfreezeRotation()} to un-freeze the rotation.
+ * @throws RemoteException never
*/
public void setOrientationNatural() throws RemoteException {
Log.d(TAG, "Setting orientation to natural.");
- getInteractionController().setRotationNatural();
- waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ rotate(UiAutomation.ROTATION_FREEZE_0);
+ }
+
+ // Rotates the device and waits for the rotation to be detected.
+ private void rotate(int rotation) {
+ getUiAutomation().setRotation(rotation);
+ Condition<UiDevice, Boolean> rotationCondition = new Condition<UiDevice, Boolean>() {
+ @Override
+ public Boolean apply(UiDevice device) {
+ return device.getDisplayRotation() == rotation;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("Condition[displayRotation=%d]", rotation);
+ }
+ };
+ if (!wait(rotationCondition, ROTATION_TIMEOUT)) {
+ Log.w(TAG, String.format("Didn't detect rotation within %dms.", ROTATION_TIMEOUT));
+ }
}
/**
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/UpdatableAnimationState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/UpdatableAnimationState.kt
index 6255031..903ee4c 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/UpdatableAnimationState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/UpdatableAnimationState.kt
@@ -16,11 +16,10 @@
package androidx.tv.foundation
-import android.view.animation.AccelerateDecelerateInterpolator
import androidx.compose.animation.core.AnimationConstants.UnspecifiedTime
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.runtime.withFrameNanos
@@ -166,15 +165,13 @@
val ZeroVector = AnimationVector1D(0f)
/**
- * Spring does not work well with PivotOffset version of this class. Using
- * [AccelerateDecelerateInterpolator] that is used by RecyclerView by default
+ * Spring does not work well with PivotOffset version of this class
*/
- val AccelerateDecelerateInterpolatorEasing = Easing {
- AccelerateDecelerateInterpolator().getInterpolation(it)
- }
- val RebasableAnimationSpec =
- tween<Float>(durationMillis = 30, easing = AccelerateDecelerateInterpolatorEasing)
- .vectorize(Float.VectorConverter)
+ val RebasableAnimationSpec = tween<Float>(
+ durationMillis = 125,
+ easing = CubicBezierEasing(0.25f, 0.1f, .25f, 1f)
+ )
+ .vectorize(Float.VectorConverter)
fun Float.isZeroish() = absoluteValue < VisibilityThreshold
}
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index c0fa79e..f941c63c 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -326,6 +326,7 @@
bitmap.assertAgainstGolden(screenshotRule, "active_screenshot")
}
+ @FlakyTest(bugId = 264868778)
@Test
public fun testAmbientScreenshot() {
handler.post(this::initCanvasWatchFace)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index f1e65dd..7a1e2ae 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -1025,6 +1025,11 @@
if (!forceUpdate && complicationData.value == best) return
renderer.loadData(best, loadDrawablesAsynchronous)
(complicationData as MutableStateFlow).value = best
+
+ // forceUpdate is used for screenshots, don't set the dirty flag for those.
+ if (!forceUpdate) {
+ dataDirty = true
+ }
}
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index bd4843b..8b28837 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -367,6 +367,12 @@
forceUpdate = false
)
}
+
+ // selectComplicationDataForInstant may have changed the complication, if so we need to
+ // update the content description labels.
+ if (complicationSlots.isNotEmpty()) {
+ onComplicationsUpdated()
+ }
}
/**
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 60f2a91..59e942b 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -5190,27 +5190,57 @@
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(999))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("A")
+ assertThat(
+ engineWrapper.contentDescriptionLabels[1]
+ .text
+ .getTextAt(ApplicationProvider.getApplicationContext<Context>().resources, 0)
+ )
+ .isEqualTo("A")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(1000))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("B")
+ assertThat(
+ engineWrapper.contentDescriptionLabels[1]
+ .text
+ .getTextAt(ApplicationProvider.getApplicationContext<Context>().resources, 0)
+ )
+ .isEqualTo("B")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(1999))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("B")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(2000))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("C")
+ assertThat(
+ engineWrapper.contentDescriptionLabels[1]
+ .text
+ .getTextAt(ApplicationProvider.getApplicationContext<Context>().resources, 0)
+ )
+ .isEqualTo("C")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(2999))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("C")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(3000))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("B")
+ assertThat(
+ engineWrapper.contentDescriptionLabels[1]
+ .text
+ .getTextAt(ApplicationProvider.getApplicationContext<Context>().resources, 0)
+ )
+ .isEqualTo("B")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(3999))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("B")
complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(4000))
assertThat(getLeftShortTextComplicationDataText()).isEqualTo("A")
+ assertThat(
+ engineWrapper.contentDescriptionLabels[1]
+ .text
+ .getTextAt(ApplicationProvider.getApplicationContext<Context>().resources, 0)
+ )
+ .isEqualTo("A")
}
@Test
diff --git a/window/extensions/extensions/api/1.1.0-beta01.txt b/window/extensions/extensions/api/1.1.0-beta01.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/1.1.0-beta01.txt
+++ b/window/extensions/extensions/api/1.1.0-beta01.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/api/public_plus_experimental_1.1.0-beta01.txt b/window/extensions/extensions/api/public_plus_experimental_1.1.0-beta01.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_1.1.0-beta01.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/api/restricted_1.1.0-beta01.txt b/window/extensions/extensions/api/restricted_1.1.0-beta01.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/restricted_1.1.0-beta01.txt
+++ b/window/extensions/extensions/api/restricted_1.1.0-beta01.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 6988f4b..6e04de6 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -66,16 +66,13 @@
}
public class SplitAttributes {
- method @ColorInt public int getAnimationBackgroundColor();
method public int getLayoutDirection();
method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
- field @ColorInt public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; // 0x0
}
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.extensions.embedding.SplitAttributes build();
- method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
index e4f1e81..e00e910 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -16,6 +16,7 @@
package androidx.window.extensions.embedding;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.BOTTOM_TO_TOP;
import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LEFT_TO_RIGHT;
import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LOCALE;
@@ -30,6 +31,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.window.extensions.core.util.function.Function;
import java.lang.annotation.Retention;
@@ -78,6 +80,7 @@
* @see Builder#setAnimationBackgroundColor(int)
*/
@ColorInt
+ @RestrictTo(LIBRARY)
public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0;
/**
@@ -433,6 +436,7 @@
* @return The animation background {@code ColorInt}.
*/
@ColorInt
+ @RestrictTo(LIBRARY)
public int getAnimationBackgroundColor() {
return mAnimationBackgroundColor;
}
@@ -509,6 +513,7 @@
* @return This {@code Builder}.
*/
@NonNull
+ @RestrictTo(LIBRARY)
public Builder setAnimationBackgroundColor(@ColorInt int color) {
// Any non-opaque color will be treated as the default.
mAnimationBackgroundColor = Color.alpha(color) != 255
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
deleted file mode 100644
index 69542ac..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.demo.embedding
-
-import androidx.annotation.GuardedBy
-import androidx.window.embedding.SplitAttributes
-import java.util.concurrent.locks.ReentrantLock
-import kotlin.concurrent.withLock
-
-/** A singleton controller to manage the global config. */
-class DemoActivityEmbeddingController private constructor() {
-
- private val lock = Object()
-
- @GuardedBy("lock")
- private var _animationBackgroundColor = SplitAttributes.BackgroundColor.DEFAULT
-
- /** Animation background color to use when the animation requires a background. */
- var animationBackgroundColor: SplitAttributes.BackgroundColor
- get() = synchronized(lock) {
- _animationBackgroundColor
- }
- set(value) = synchronized(lock) {
- _animationBackgroundColor = value
- }
-
- companion object {
- @Volatile
- private var globalInstance: DemoActivityEmbeddingController? = null
- private val globalLock = ReentrantLock()
-
- /**
- * Obtains the singleton instance of [DemoActivityEmbeddingController].
- */
- @JvmStatic
- fun getInstance(): DemoActivityEmbeddingController {
- if (globalInstance == null) {
- globalLock.withLock {
- if (globalInstance == null) {
- globalInstance = DemoActivityEmbeddingController()
- }
- }
- }
- return globalInstance!!
- }
- }
-}
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index 95a6ea2..ca1dd94 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -48,7 +48,6 @@
*/
@OptIn(ExperimentalWindowApi::class)
class ExampleWindowInitializer : Initializer<RuleController> {
- private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
override fun create(context: Context): RuleController {
SplitController.getInstance(context).apply {
@@ -84,11 +83,9 @@
val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
// Make a copy of the default splitAttributes, but replace the animation background
// color to what is configured in the Demo app.
- val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
val defaultSplitAttributes = SplitAttributes.Builder()
.setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
.setSplitType(params.defaultSplitAttributes.splitType)
- .setAnimationBackgroundColor(backgroundColor)
.build()
when (tag?.substringBefore(SUFFIX_REVERSED)) {
TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
@@ -114,7 +111,6 @@
TOP_TO_BOTTOM
}
)
- .setAnimationBackgroundColor(backgroundColor)
.build()
} else if (isPortrait) {
return expandContainersAttrs
@@ -131,7 +127,6 @@
TOP_TO_BOTTOM
}
)
- .setAnimationBackgroundColor(backgroundColor)
.build()
}
}
@@ -159,7 +154,6 @@
TOP_TO_BOTTOM
}
)
- .setAnimationBackgroundColor(backgroundColor)
.build()
} else {
SplitAttributes.Builder()
@@ -171,7 +165,6 @@
LEFT_TO_RIGHT
}
)
- .setAnimationBackgroundColor(backgroundColor)
.build()
}
}
@@ -195,7 +188,6 @@
if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
}
)
- .setAnimationBackgroundColor(backgroundColor)
.build()
}
}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
index 680d8d2..726726f 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
@@ -18,11 +18,9 @@
import android.content.ComponentName
import android.content.Intent
-import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
-import android.widget.ArrayAdapter
import android.widget.CompoundButton
import android.widget.RadioGroup
import android.widget.Toast
@@ -36,8 +34,8 @@
import androidx.window.embedding.EmbeddingRule
import androidx.window.embedding.RuleController
import androidx.window.embedding.SplitAttributes
-import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
+import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitController
import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
import androidx.window.embedding.SplitInfo
@@ -64,9 +62,6 @@
private lateinit var activityA: ComponentName
private lateinit var activityB: ComponentName
- /** Controller to manage the global configuration. */
- private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
-
/** The last selected split rule id. */
private var lastCheckedRuleId = 0
@@ -89,31 +84,6 @@
activityA = ComponentName(this, SplitDeviceStateActivityA::class.java.name)
activityB = ComponentName(this, SplitDeviceStateActivityB::class.java.name)
- val radioGroup = viewBinding.splitAttributesOptionsRadioGroup
- val animationBgColorDropdown = viewBinding.animationBackgroundColorDropdown
- if (componentName == activityA) {
- // Set to the first option
- demoActivityEmbeddingController.animationBackgroundColor =
- ANIMATION_BACKGROUND_COLORS_VALUE[0]
- radioGroup.check(R.id.use_default_split_attributes)
- onCheckedChanged(radioGroup, radioGroup.checkedRadioButtonId)
- radioGroup.setOnCheckedChangeListener(this)
- animationBgColorDropdown.adapter = ArrayAdapter(
- this,
- android.R.layout.simple_spinner_dropdown_item,
- ANIMATION_BACKGROUND_COLORS_TEXT
- )
- animationBgColorDropdown.onItemSelectedListener = this
- } else {
- // Only update split pair rule on the primary Activity. The secondary Activity can only
- // finish itself to prevent confusing users. We only apply the rule when the Activity is
- // launched from the primary.
- viewBinding.chooseLayoutTextView.visibility = View.GONE
- radioGroup.visibility = View.GONE
- animationBgColorDropdown.visibility = View.GONE
- viewBinding.launchActivityToSide.text = "Finish this Activity"
- }
-
viewBinding.showHorizontalLayoutInTabletopCheckBox.setOnCheckedChangeListener(this)
viewBinding.showFullscreenInBookModeCheckBox.setOnCheckedChangeListener(this)
viewBinding.swapPrimarySecondaryPositionCheckBox.setOnCheckedChangeListener(this)
@@ -129,7 +99,6 @@
hideAllSubCheckBoxes()
// Add the error message to notify the SplitAttributesCalculator is not available.
viewBinding.errorMessageTextView.text = "SplitAttributesCalculator is not supported!"
- animationBgColorDropdown.isEnabled = false
}
lifecycleScope.launch {
@@ -187,8 +156,6 @@
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
- demoActivityEmbeddingController.animationBackgroundColor =
- ANIMATION_BACKGROUND_COLORS_VALUE[position]
updateSplitPairRuleWithRadioButtonId(lastCheckedRuleId)
}
@@ -266,7 +233,6 @@
val defaultSplitAttributes = SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .setAnimationBackgroundColor(demoActivityEmbeddingController.animationBackgroundColor)
.build()
// Use the tag to control the rule how to change split attributes with the current state
var tag = when (id) {
@@ -404,13 +370,6 @@
const val SUFFIX_REVERSED = "_reversed"
const val SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP = "_and_horizontal_layout_in_tabletop"
const val SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE = "_and_fullscreen_in_book_mode"
- val ANIMATION_BACKGROUND_COLORS_TEXT = arrayOf("DEFAULT", "BLUE", "GREEN", "YELLOW")
- val ANIMATION_BACKGROUND_COLORS_VALUE = arrayOf(
- SplitAttributes.BackgroundColor.DEFAULT,
- SplitAttributes.BackgroundColor.color(Color.BLUE),
- SplitAttributes.BackgroundColor.color(Color.GREEN),
- SplitAttributes.BackgroundColor.color(Color.YELLOW)
- )
/**
* The default minimum dimension for large screen devices.
diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
index eb67379..b996f71 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
@@ -151,19 +151,6 @@
android:background="#AAAAAA" />
<!-- Dropdown for animation background color -->
-
- <TextView
- android:id="@+id/animation_background_color_text_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/current_animation_background_color"/>
-
- <Spinner
- android:id="@+id/animation_background_color_dropdown"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:spinnerMode="dropdown" />
-
<View
android:layout_width="match_parent"
android:layout_height="1dp"
diff --git a/window/window-java/api/public_plus_experimental_1.1.0-beta01.txt b/window/window-java/api/public_plus_experimental_1.1.0-beta01.txt
index bcbb1e7..d621966 100644
--- a/window/window-java/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/window/window-java/api/public_plus_experimental_1.1.0-beta01.txt
@@ -1,15 +1,4 @@
// Signature format: 4.0
-package androidx.window.java.area {
-
- @androidx.window.core.ExperimentalWindowApi public final class WindowAreaControllerJavaAdapter implements androidx.window.area.WindowAreaController {
- ctor public WindowAreaControllerJavaAdapter(androidx.window.area.WindowAreaController controller);
- method public void addRearDisplayStatusListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
- method public void removeRearDisplayStatusListener(androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
- method public void startRearDisplayModeSession(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
- }
-
-}
-
package androidx.window.java.embedding {
@androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
diff --git a/window/window/api/1.1.0-beta01.txt b/window/window/api/1.1.0-beta01.txt
index 61229e4..5617dbb 100644
--- a/window/window/api/1.1.0-beta01.txt
+++ b/window/window/api/1.1.0-beta01.txt
@@ -84,29 +84,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 61229e4..5617dbb 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -84,29 +84,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/api/public_plus_experimental_1.1.0-beta01.txt b/window/window/api/public_plus_experimental_1.1.0-beta01.txt
index d7170f2..0ac0175 100644
--- a/window/window/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/window/window/api/public_plus_experimental_1.1.0-beta01.txt
@@ -9,40 +9,6 @@
}
-package androidx.window.area {
-
- @androidx.window.core.ExperimentalWindowApi public interface WindowAreaController {
- method public default static androidx.window.area.WindowAreaController getOrCreate();
- method public void rearDisplayMode(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
- method public kotlinx.coroutines.flow.Flow<androidx.window.area.WindowAreaStatus> rearDisplayStatus();
- field public static final androidx.window.area.WindowAreaController.Companion Companion;
- }
-
- public static final class WindowAreaController.Companion {
- method public androidx.window.area.WindowAreaController getOrCreate();
- }
-
- @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSession {
- method public void close();
- }
-
- @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionCallback {
- method public void onSessionEnded();
- method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
- }
-
- @androidx.window.core.ExperimentalWindowApi public final class WindowAreaStatus {
- field public static final androidx.window.area.WindowAreaStatus AVAILABLE;
- field public static final androidx.window.area.WindowAreaStatus.Companion Companion;
- field public static final androidx.window.area.WindowAreaStatus UNAVAILABLE;
- field public static final androidx.window.area.WindowAreaStatus UNSUPPORTED;
- }
-
- public static final class WindowAreaStatus.Companion {
- }
-
-}
-
package androidx.window.core {
@kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWindowApi {
@@ -125,29 +91,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 650afbe..0ac0175 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -91,29 +91,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/api/restricted_1.1.0-beta01.txt b/window/window/api/restricted_1.1.0-beta01.txt
index 61229e4..5617dbb 100644
--- a/window/window/api/restricted_1.1.0-beta01.txt
+++ b/window/window/api/restricted_1.1.0-beta01.txt
@@ -84,29 +84,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 61229e4..5617dbb 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -84,29 +84,16 @@
}
public final class SplitAttributes {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor getAnimationBackgroundColor();
method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
- property public final androidx.window.embedding.SplitAttributes.BackgroundColor animationBackgroundColor;
property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
}
- public static final class SplitAttributes.BackgroundColor {
- method public static androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor.Companion Companion;
- field public static final androidx.window.embedding.SplitAttributes.BackgroundColor DEFAULT;
- }
-
- public static final class SplitAttributes.BackgroundColor.Companion {
- method public androidx.window.embedding.SplitAttributes.BackgroundColor color(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
- }
-
public static final class SplitAttributes.Builder {
ctor public SplitAttributes.Builder();
method public androidx.window.embedding.SplitAttributes build();
- method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(androidx.window.embedding.SplitAttributes.BackgroundColor color);
method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
}
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index 9e69205..c6a3607 100644
--- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -17,13 +17,12 @@
package androidx.window.samples.embedding
import android.app.Application
-import android.graphics.Color
import androidx.annotation.Sampled
import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.SplitAttributes
+import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
-import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitController
import androidx.window.layout.FoldingFeature
@@ -61,8 +60,6 @@
SplitAttributes.LayoutDirection.LOCALE
}
)
- // Set the color to use when switching between vertical and horizontal
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GRAY))
.build()
}
return@setSplitAttributesCalculator if (
@@ -72,7 +69,6 @@
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GRAY))
.build()
} else {
// Expand containers if the device is in portrait or the width is less than 600 dp.
@@ -96,13 +92,11 @@
builder
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
// Set the color to use when switching between vertical and horizontal
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GRAY))
.build()
} else if (parentConfiguration.screenHeightDp >= 600) {
builder
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
// Set the color to use when switching between vertical and horizontal
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GRAY))
.build()
} else {
// Fallback to expand the secondary container
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
index 631dfc3..c3afe8f 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -20,7 +20,6 @@
import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
import android.app.Activity
-import android.graphics.Color
import androidx.window.WindowTestUtils
import androidx.window.core.ExtensionsUtil
import androidx.window.core.PredicateAdapter
@@ -61,7 +60,6 @@
SplitAttributes.Builder()
.setSplitType(SplitType.SPLIT_TYPE_EQUAL)
.setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
@@ -124,7 +122,6 @@
OEMSplitAttributes.Builder()
.setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
.setLayoutDirection(TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(Color.YELLOW)
.build(),
)
val expectedSplitInfo = SplitInfo(
@@ -133,7 +130,6 @@
SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.YELLOW))
.build()
)
assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
index 582d047..e8fc72a 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
@@ -19,7 +19,6 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import androidx.annotation.RequiresApi
@@ -81,7 +80,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -133,7 +131,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.BLUE))
.build()
assertEquals(TEST_TAG, rule.tag)
assertEquals(NEVER, rule.finishPrimaryWithSecondary)
@@ -154,7 +151,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -180,7 +176,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(LEFT_TO_RIGHT)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GREEN))
.build()
filters.add(
SplitPairFilter(
@@ -382,7 +377,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
assertNull(rule.tag)
assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -437,8 +431,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(BOTTOM_TO_TOP)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(
- application.resources.getColor(R.color.testColor, null)))
.build()
assertEquals(TEST_TAG, rule.tag)
assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
@@ -466,7 +458,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.5f))
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
assertTrue(rule.checkParentBounds(density, minValidWindowBounds()))
@@ -490,7 +481,6 @@
val expectedSplitLayout = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(0.3f))
.setLayoutDirection(LEFT_TO_RIGHT)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GREEN))
.build()
val rule = SplitPlaceholderRule.Builder(filters, intent)
.setMinWidthDp(123)
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index 111a858..6dd98b7 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -100,7 +100,7 @@
when (val splitType = splitAttributes.splitType) {
is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
- is OEMSplitType.RatioSplitType -> ratio(splitType.ratio)
+ is RatioSplitType -> ratio(splitType.ratio)
else -> throw IllegalArgumentException("Unknown split type: $splitType")
}
).setLayoutDirection(
@@ -115,9 +115,6 @@
)
}
)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.buildFromValue(
- splitAttributes.animationBackgroundColor)
- )
.build()
@OptIn(ExperimentalWindowApi::class)
@@ -214,7 +211,6 @@
)
}
)
- .setAnimationBackgroundColor(splitAttributes.animationBackgroundColor.value)
.build()
}
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
index add2f25..e2cb653 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -174,20 +174,12 @@
ALWAYS.value
)
val clearTop = typedArray.getBoolean(R.styleable.SplitPairRule_clearTop, false)
- val animationBackgroundColor = typedArray.getColor(
- R.styleable.SplitPairRule_animationBackgroundColor,
- SplitAttributes.BackgroundColor.DEFAULT.value
- )
- typedArray.recycle()
val defaultAttrs = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.buildSplitTypeFromValue(ratio))
.setLayoutDirection(
SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
)
- .setAnimationBackgroundColor(
- SplitAttributes.BackgroundColor.buildFromValue(animationBackgroundColor)
- )
.build()
SplitPairRule.Builder(emptySet())
@@ -259,20 +251,12 @@
R.styleable.SplitPlaceholderRule_splitLayoutDirection,
LOCALE.value
)
- val animationBackgroundColor = typedArray.getColor(
- R.styleable.SplitPlaceholderRule_animationBackgroundColor,
- SplitAttributes.BackgroundColor.DEFAULT.value
- )
- typedArray.recycle()
val defaultAttrs = SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.buildSplitTypeFromValue(ratio))
.setLayoutDirection(
SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
)
- .setAnimationBackgroundColor(
- SplitAttributes.BackgroundColor.buildFromValue(animationBackgroundColor)
- )
.build()
val packageName = context.applicationContext.packageName
val placeholderActivityClassName = buildClassName(
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index df42279..4991be0 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -17,15 +17,12 @@
package androidx.window.embedding
import android.annotation.SuppressLint
-import android.graphics.Color
-import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.window.core.SpecificationComputer.Companion.startSpecification
import androidx.window.core.VerificationMode
-import androidx.window.embedding.SplitAttributes.BackgroundColor
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
@@ -59,7 +56,6 @@
*
* @see SplitAttributes.SplitType
* @see SplitAttributes.LayoutDirection
- * @see SplitAttributes.BackgroundColor
*/
class SplitAttributes @RestrictTo(LIBRARY_GROUP) constructor(
@@ -74,18 +70,6 @@
* is based on locale.
*/
val layoutDirection: LayoutDirection = LOCALE,
-
- /**
- * The color to use for the background color during the animation of
- * the split involving this `SplitAttributes` object if the animation
- * requires a background.
- *
- * The default is to use the current theme window background color.
- *
- * @see BackgroundColor.color
- * @see BackgroundColor.DEFAULT
- */
- val animationBackgroundColor: BackgroundColor = BackgroundColor.DEFAULT
) {
/**
@@ -364,85 +348,6 @@
}
/**
- * Background color to be used for window transition animations in a split if the animation
- * requires a background.
- *
- * @see SplitAttributes.animationBackgroundColor
- */
- class BackgroundColor private constructor(
-
- /**
- * The description of this `BackgroundColor`.
- */
- private val description: String,
-
- /**
- * [ColorInt] to represent the color to use as the background color.
- */
- @ColorInt
- internal val value: Int,
- ) {
- override fun toString() = "BackgroundColor($description)"
-
- override fun equals(other: Any?): Boolean {
- if (other === this) return true
- if (other !is BackgroundColor) return false
- return value == other.value && description == other.description
- }
-
- override fun hashCode() = description.hashCode() + 31 * value.hashCode()
-
- /**
- * Methods that create various [BackgroundColor].
- */
- companion object {
-
- /**
- * Creates a [BackgroundColor] to represent the given [color].
- *
- * Only opaque color is supported.
- *
- * @param color [ColorInt] of an opaque color.
- * @return the [BackgroundColor] representing the [color].
- *
- * @see [DEFAULT] for the default value, which means to use the
- * current theme window background color.
- */
- @JvmStatic
- fun color(
- @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())
- @ColorInt
- color: Int
- ):
- BackgroundColor {
- require(Color.BLACK <= color && color <= Color.WHITE) {
- "Background color must be opaque"
- }
- return BackgroundColor("color:${Integer.toHexString(color)}", color)
- }
-
- /**
- * The special [BackgroundColor] to represent the default value,
- * which means to use the current theme window background color.
- */
- @JvmField
- val DEFAULT = BackgroundColor("DEFAULT", 0)
-
- /**
- * Returns a [BackgroundColor] with the given [value]
- */
- internal fun buildFromValue(@ColorInt value: Int): BackgroundColor {
- return if (Color.alpha(value) != 255) {
- // Treat any non-opaque color as the default.
- DEFAULT
- } else {
- color(value)
- }
- }
- }
- }
-
- /**
* Non-public properties and methods.
*/
companion object {
@@ -457,7 +362,6 @@
override fun hashCode(): Int {
var result = splitType.hashCode()
result = result * 31 + layoutDirection.hashCode()
- result = result * 31 + animationBackgroundColor.hashCode()
return result
}
@@ -473,8 +377,7 @@
if (this === other) return true
if (other !is SplitAttributes) return false
return splitType == other.splitType &&
- layoutDirection == other.layoutDirection &&
- animationBackgroundColor == other.animationBackgroundColor
+ layoutDirection == other.layoutDirection
}
/**
@@ -484,8 +387,7 @@
*/
override fun toString(): String =
"${SplitAttributes::class.java.simpleName}:" +
- "{splitType=$splitType, layoutDir=$layoutDirection," +
- " animationBackgroundColor=$animationBackgroundColor"
+ "{splitType=$splitType, layoutDir=$layoutDirection }"
/**
* Builder for creating an instance of [SplitAttributes].
@@ -499,7 +401,6 @@
class Builder {
private var splitType = SPLIT_TYPE_EQUAL
private var layoutDirection = LOCALE
- private var animationBackgroundColor = BackgroundColor.DEFAULT
/**
* Sets the split type attribute.
@@ -528,32 +429,11 @@
apply { this.layoutDirection = layoutDirection }
/**
- * Sets the color to use for the background color during animation
- * of the split involving this `SplitAttributes` object if the animation
- * requires a background. Only opaque color is supported.
- *
- * The default is [BackgroundColor.DEFAULT], which means to use the
- * current theme window background color.
- *
- * @param color The animation background color.
- * @return This `Builder`.
- *
- * @see BackgroundColor.color
- * @see BackgroundColor.DEFAULT
- */
- fun setAnimationBackgroundColor(color: BackgroundColor): Builder =
- apply {
- animationBackgroundColor = color
- }
-
- /**
* Builds a `SplitAttributes` instance with the attributes specified by
- * [setSplitType], [setLayoutDirection], and
- * [setAnimationBackgroundColor].
+ * [setSplitType] and [setLayoutDirection].
*
* @return The new `SplitAttributes` instance.
*/
- fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection,
- animationBackgroundColor)
+ fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)
}
}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
index a0ff9a7..7acee3d 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
@@ -16,7 +16,6 @@
package androidx.window.embedding
-import android.graphics.Color
import androidx.window.core.WindowStrictModeException
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
@@ -24,9 +23,9 @@
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
import androidx.window.embedding.SplitAttributes.SplitType
+import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
-import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertThrows
@@ -42,27 +41,14 @@
val attrs1 = SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_EQUAL)
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
val attrs2 = SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(LOCALE)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
.build()
val attrs3 = SplitAttributes.Builder()
.setSplitType(SPLIT_TYPE_HINGE)
.setLayoutDirection(TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.DEFAULT)
- .build()
- val attrs4 = SplitAttributes.Builder()
- .setSplitType(SPLIT_TYPE_HINGE)
- .setLayoutDirection(TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GREEN))
- .build()
- val attrs5 = SplitAttributes.Builder()
- .setSplitType(SPLIT_TYPE_HINGE)
- .setLayoutDirection(TOP_TO_BOTTOM)
- .setAnimationBackgroundColor(SplitAttributes.BackgroundColor.color(Color.GREEN))
.build()
assertNotEquals(attrs1, attrs2)
@@ -73,12 +59,6 @@
assertNotEquals(attrs3, attrs1)
assertNotEquals(attrs3.hashCode(), attrs1.hashCode())
-
- assertNotEquals(attrs3, attrs4)
- assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
-
- assertEquals(attrs4, attrs5)
- assertEquals(attrs4.hashCode(), attrs5.hashCode())
}
@Test
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
index d8483c7..caa1cf6 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcher.java
@@ -30,6 +30,8 @@
import androidx.work.impl.StartStopToken;
import androidx.work.impl.StartStopTokens;
import androidx.work.impl.WorkDatabase;
+import androidx.work.impl.WorkLauncher;
+import androidx.work.impl.WorkLauncherImpl;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkGenerationalId;
import androidx.work.impl.model.WorkSpec;
@@ -59,11 +61,14 @@
// Synthetic access
WorkManagerImpl mWorkManagerImpl;
+ private final WorkLauncher mWorkLauncher;
public WorkManagerGcmDispatcher(
@NonNull WorkManagerImpl workManager, @NonNull WorkTimer timer) {
mWorkManagerImpl = workManager;
mWorkTimer = timer;
+ mWorkLauncher = new WorkLauncherImpl(workManager.getProcessor(),
+ workManager.getWorkTaskExecutor());
}
/**
@@ -106,13 +111,13 @@
mStartStopTokens);
StartStopToken startStopToken = mStartStopTokens.tokenFor(id);
WorkSpecTimeLimitExceededListener timeLimitExceededListener =
- new WorkSpecTimeLimitExceededListener(mWorkManagerImpl, startStopToken);
+ new WorkSpecTimeLimitExceededListener(mWorkLauncher, startStopToken);
Processor processor = mWorkManagerImpl.getProcessor();
processor.addExecutionListener(listener);
String wakeLockTag = "WorkGcm-onRunTask (" + workSpecId + ")";
PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
mWorkManagerImpl.getApplicationContext(), wakeLockTag);
- mWorkManagerImpl.startWork(startStopToken);
+ mWorkLauncher.startWork(startStopToken);
mWorkTimer.startTimer(id, AWAIT_TIME_IN_MILLISECONDS, timeLimitExceededListener);
try {
@@ -180,19 +185,19 @@
static class WorkSpecTimeLimitExceededListener implements WorkTimer.TimeLimitExceededListener {
private static final String TAG = Logger.tagWithPrefix("WrkTimeLimitExceededLstnr");
- private final WorkManagerImpl mWorkManager;
+ private final WorkLauncher mLauncher;
private final StartStopToken mStartStopToken;
WorkSpecTimeLimitExceededListener(
- @NonNull WorkManagerImpl workManager,
+ @NonNull WorkLauncher launcher,
@NonNull StartStopToken startStopToken) {
- mWorkManager = workManager;
+ mLauncher = launcher;
mStartStopToken = startStopToken;
}
@Override
public void onTimeLimitExceeded(@NonNull WorkGenerationalId id) {
Logger.get().debug(TAG, "WorkSpec time limit exceeded " + id);
- mWorkManager.stopWork(mStartStopToken);
+ mLauncher.stopWork(mStartStopToken);
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
index 0cde5ea..72fc7af 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/SchedulersTest.kt
@@ -23,7 +23,9 @@
import androidx.work.impl.StartStopTokens
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkLauncherImpl
import androidx.work.impl.background.greedy.GreedyScheduler
+import androidx.work.impl.constraints.trackers.Trackers
import androidx.work.impl.model.WorkSpec
import androidx.work.impl.testutils.TrackingWorkerFactory
import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
@@ -43,6 +45,9 @@
val taskExecutor = WorkManagerTaskExecutor(configuration.taskExecutor)
val db = WorkDatabase.create(context, taskExecutor.serialTaskExecutor, true)
val processor = Processor(context, configuration, taskExecutor, db)
+ val launcher = WorkLauncherImpl(processor, taskExecutor)
+ val trackers = Trackers(context, taskExecutor)
+ val greedyScheduler = GreedyScheduler(context, configuration, trackers, processor, launcher)
@Test
fun runDependency() {
@@ -55,9 +60,8 @@
override fun hasLimitedSchedulingSlots() = false
}
- val schedulers = mutableListOf<Scheduler>(trackingScheduler)
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db, schedulers, processor)
- schedulers.add(GreedyScheduler(context, configuration, wm.trackers, wm))
+ val wm = WorkManagerImpl(context, configuration, taskExecutor, db,
+ listOf(trackingScheduler, greedyScheduler), processor, trackers)
val workRequest = OneTimeWorkRequest.from(TestWorker::class.java)
val dependency = OneTimeWorkRequest.from(TestWorker::class.java)
@@ -83,9 +87,8 @@
override fun hasLimitedSchedulingSlots() = false
}
- val schedulers = mutableListOf<Scheduler>(trackingScheduler)
- val wm = WorkManagerImpl(context, configuration, taskExecutor, db, schedulers, processor)
- schedulers.add(GreedyScheduler(context, configuration, wm.trackers, wm))
+ val wm = WorkManagerImpl(context, configuration, taskExecutor, db,
+ listOf(trackingScheduler, greedyScheduler), processor, trackers)
val workRequest = OneTimeWorkRequest.from(FailureWorker::class.java)
wm.enqueue(workRequest)
@@ -110,7 +113,7 @@
override fun schedule(vararg workSpecs: WorkSpec) {
scheduledSpecs.addAll(workSpecs)
workSpecs.forEach {
- if (it.runAttemptCount == 0) wm.startWork(tokens.tokenFor(it))
+ if (it.runAttemptCount == 0) launcher.startWork(tokens.tokenFor(it))
}
}
@@ -130,7 +133,7 @@
wm.getWorkInfoByIdLiveData(request.id).observeForever {
when (it.state) {
WorkInfo.State.RUNNING -> {
- wm.stopWork(scheduler.tokens.remove(request.stringId).first())
+ launcher.stopWork(scheduler.tokens.remove(request.stringId).first())
running = true
}
WorkInfo.State.ENQUEUED -> {
@@ -159,7 +162,7 @@
override fun schedule(vararg workSpecs: WorkSpec) {
scheduledSpecs.addAll(workSpecs)
workSpecs.forEach {
- if (it.periodCount == 0) wm.startWork(tokens.tokenFor(it))
+ if (it.periodCount == 0) launcher.startWork(tokens.tokenFor(it))
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index 9906def..daa5863 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -28,9 +28,9 @@
import androidx.work.WorkManager.UpdateResult.APPLIED_IMMEDIATELY
import androidx.work.WorkManager.UpdateResult.NOT_APPLIED
import androidx.work.impl.Processor
-import androidx.work.impl.Scheduler
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkLauncherImpl
import androidx.work.impl.background.greedy.GreedyScheduler
import androidx.work.impl.constraints.trackers.Trackers
import androidx.work.impl.testutils.TestConstraintTracker
@@ -67,17 +67,14 @@
)
val db = WorkDatabase.create(context, executor, true)
- // ugly, ugly hack because of circular dependency:
- // Schedulers need WorkManager, WorkManager needs schedulers
- val schedulers = mutableListOf<Scheduler>()
val processor = Processor(context, configuration, taskExecutor, db)
+ val launcher = WorkLauncherImpl(processor, taskExecutor)
+ val greedyScheduler = GreedyScheduler(context, configuration, trackers, processor, launcher)
val workManager = WorkManagerImpl(
- context, configuration, taskExecutor, db, schedulers, processor, trackers
+ context, configuration, taskExecutor, db, listOf(greedyScheduler), processor, trackers
)
- val greedyScheduler = GreedyScheduler(context, configuration, trackers, workManager)
init {
- schedulers.add(greedyScheduler)
WorkManagerImpl.setDelegate(workManager)
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
index 0ed7dec..da88dcf 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplLargeExecutorTest.java
@@ -190,7 +190,10 @@
Context context,
Configuration configuration,
WorkManagerImpl workManagerImpl) {
- super(context, configuration, workManagerImpl.getTrackers(), workManagerImpl);
+ super(context, configuration, workManagerImpl.getTrackers(),
+ workManagerImpl.getProcessor(),
+ new WorkLauncherImpl(workManagerImpl.getProcessor(),
+ workManagerImpl.getWorkTaskExecutor()));
mScheduledWorkSpecIds = new HashSet<>();
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 206fdc6..700390a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -167,14 +167,17 @@
.setExecutor(Executors.newSingleThreadExecutor())
.setMinimumLoggingLevel(Log.DEBUG)
.build();
+ InstantWorkTaskExecutor workTaskExecutor = new InstantWorkTaskExecutor();
mWorkManagerImpl =
- spy(new WorkManagerImpl(mContext, mConfiguration, new InstantWorkTaskExecutor()));
+ spy(new WorkManagerImpl(mContext, mConfiguration, workTaskExecutor));
+ WorkLauncher workLauncher = new WorkLauncherImpl(mWorkManagerImpl.getProcessor(),
+ workTaskExecutor);
mScheduler =
spy(new GreedyScheduler(
mContext,
mWorkManagerImpl.getConfiguration(),
mWorkManagerImpl.getTrackers(),
- mWorkManagerImpl));
+ mWorkManagerImpl.getProcessor(), workLauncher));
// Don't return any scheduler. We don't need to actually execute work for most of our tests.
when(mWorkManagerImpl.getSchedulers()).thenReturn(Collections.<Scheduler>emptyList());
WorkManagerImpl.setDelegate(mWorkManagerImpl);
@@ -1795,16 +1798,19 @@
return packageManager;
}
};
- mWorkManagerImpl =
- spy(new WorkManagerImpl(mContext, mConfiguration, new InstantWorkTaskExecutor()));
+ InstantWorkTaskExecutor workTaskExecutor = new InstantWorkTaskExecutor();
+ Processor processor = new Processor(mContext, mConfiguration, workTaskExecutor, mDatabase);
+ WorkLauncherImpl launcher = new WorkLauncherImpl(processor, workTaskExecutor);
+
Scheduler scheduler =
new GreedyScheduler(
mContext,
mWorkManagerImpl.getConfiguration(),
mWorkManagerImpl.getTrackers(),
- mWorkManagerImpl);
- // Return GreedyScheduler alone, because real jobs gets scheduled which slow down tests.
- when(mWorkManagerImpl.getSchedulers()).thenReturn(Collections.singletonList(scheduler));
+ processor, launcher);
+ mWorkManagerImpl = new WorkManagerImpl(mContext, mConfiguration, workTaskExecutor,
+ mDatabase, Collections.singletonList(scheduler), processor);
+
WorkManagerImpl.setDelegate(mWorkManagerImpl);
mDatabase = mWorkManagerImpl.getWorkDatabase();
// Initialization of WM enables SystemJobService which needs to be discounted.
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
index f88681c..78e9c98 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/GreedySchedulerTest.java
@@ -25,23 +25,22 @@
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import androidx.work.Configuration;
import androidx.work.Constraints;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManagerTest;
import androidx.work.impl.Processor;
import androidx.work.impl.StartStopToken;
-import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.WorkLauncher;
import androidx.work.impl.constraints.WorkConstraintsTracker;
import androidx.work.impl.model.WorkSpec;
-import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import androidx.work.worker.TestWorker;
import org.junit.Before;
@@ -59,25 +58,23 @@
@RunWith(AndroidJUnit4.class)
public class GreedySchedulerTest extends WorkManagerTest {
private Context mContext;
- private WorkManagerImpl mWorkManagerImpl;
private Processor mMockProcessor;
private WorkConstraintsTracker mMockWorkConstraintsTracker;
private GreedyScheduler mGreedyScheduler;
private DelayedWorkTracker mDelayedWorkTracker;
+ private WorkLauncher mWorkLauncher;
+
@Before
public void setUp() {
mContext = mock(Context.class);
- TaskExecutor taskExecutor = mock(TaskExecutor.class);
- mWorkManagerImpl = mock(WorkManagerImpl.class);
mMockProcessor = mock(Processor.class);
mMockWorkConstraintsTracker = mock(WorkConstraintsTracker.class);
- when(mWorkManagerImpl.getProcessor()).thenReturn(mMockProcessor);
- when(mWorkManagerImpl.getWorkTaskExecutor()).thenReturn(taskExecutor);
+ mWorkLauncher = mock(WorkLauncher.class);
+ Configuration configuration = new Configuration.Builder().build();
mGreedyScheduler = new GreedyScheduler(
- mContext,
- mWorkManagerImpl,
- mMockWorkConstraintsTracker);
+ mContext, configuration,
+ mMockWorkConstraintsTracker, mMockProcessor, mWorkLauncher);
mGreedyScheduler.mInDefaultProcess = true;
mDelayedWorkTracker = mock(DelayedWorkTracker.class);
mGreedyScheduler.setDelayedWorkTracker(mDelayedWorkTracker);
@@ -90,7 +87,7 @@
WorkSpec workSpec = work.getWorkSpec();
mGreedyScheduler.schedule(workSpec);
ArgumentCaptor<StartStopToken> captor = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManagerImpl).startWork(captor.capture());
+ verify(mWorkLauncher).startWork(captor.capture());
assertThat(captor.getValue().getId().getWorkSpecId()).isEqualTo(workSpec.id);
}
@@ -105,7 +102,7 @@
// So the first invocation will always result in startWork(). Subsequent runs will
// use `delayedStartWork()`.
ArgumentCaptor<StartStopToken> captor = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManagerImpl).startWork(captor.capture());
+ verify(mWorkLauncher).startWork(captor.capture());
assertThat(captor.getValue().getId().getWorkSpecId()).isEqualTo(periodicWork.getStringId());
}
@@ -150,7 +147,7 @@
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(TestWorker.class).build();
mGreedyScheduler.onAllConstraintsMet(Collections.singletonList(work.getWorkSpec()));
ArgumentCaptor<StartStopToken> captor = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManagerImpl).startWork(captor.capture());
+ verify(mWorkLauncher).startWork(captor.capture());
assertThat(captor.getValue().getId().getWorkSpecId()).isEqualTo(work.getWorkSpec().id);
}
@@ -162,7 +159,7 @@
mGreedyScheduler.onAllConstraintsMet(Collections.singletonList(work.getWorkSpec()));
mGreedyScheduler.onAllConstraintsNotMet(Collections.singletonList(work.getWorkSpec()));
ArgumentCaptor<StartStopToken> captor = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManagerImpl).stopWork(captor.capture());
+ verify(mWorkLauncher).stopWork(captor.capture());
assertThat(captor.getValue().getId().getWorkSpecId()).isEqualTo(work.getWorkSpec().id);
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
index 6419d8d..459c4ca 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcherTest.java
@@ -59,6 +59,7 @@
import androidx.work.impl.Scheduler;
import androidx.work.impl.Schedulers;
import androidx.work.impl.StartStopToken;
+import androidx.work.impl.WorkLauncher;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.NetworkState;
import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
@@ -123,6 +124,8 @@
}
};
+ private WorkLauncher mLauncher;
+
@Before
@SuppressWarnings("unchecked")
public void setUp() {
@@ -189,9 +192,10 @@
instantTaskExecutor,
mDatabase);
mSpyProcessor = spy(processor);
-
+ mLauncher = mock(WorkLauncher.class);
mDispatcher =
- new CommandInterceptingSystemDispatcher(mContext, mSpyProcessor, mWorkManager);
+ new CommandInterceptingSystemDispatcher(mContext, mSpyProcessor,
+ mWorkManager, mLauncher);
mDispatcher.setCompletedListener(completedListener);
mSpyDispatcher = spy(mDispatcher);
Schedulers.registerRescheduling(Collections.singletonList(scheduler), processor,
@@ -310,7 +314,7 @@
assertThat(captor.getValue().getId()).isEqualTo(workSpecId);
ArgumentCaptor<StartStopToken> captorStop = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManager, times(1)).stopWork(captorStop.capture());
+ verify(mLauncher, times(1)).stopWork(captorStop.capture());
assertThat(captorStop.getValue().getId()).isEqualTo(workSpecId);
}
@@ -340,7 +344,7 @@
assertThat(captor.getValue().getId()).isEqualTo(workSpecId);
ArgumentCaptor<StartStopToken> captorStop = ArgumentCaptor.forClass(StartStopToken.class);
- verify(mWorkManager, times(1)).stopWork(captorStop.capture());
+ verify(mLauncher, times(1)).stopWork(captorStop.capture());
assertThat(captorStop.getValue().getId()).isEqualTo(workSpecId);
}
@@ -755,8 +759,9 @@
CommandInterceptingSystemDispatcher(@NonNull Context context,
@Nullable Processor processor,
- @Nullable WorkManagerImpl workManager) {
- super(context, processor, workManager);
+ @Nullable WorkManagerImpl workManager,
+ @Nullable WorkLauncher launcher) {
+ super(context, processor, workManager, launcher);
mCommands = new ArrayList<>();
mActionCount = new HashMap<>();
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkLauncher.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkLauncher.kt
new file mode 100644
index 0000000..5a249dd
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkLauncher.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.impl
+
+import androidx.work.WorkerParameters
+import androidx.work.WorkerParameters.RuntimeExtras
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.utils.StartWorkRunnable
+import androidx.work.impl.utils.StopWorkRunnable
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+
+interface WorkLauncher {
+
+ fun startWork(workSpecId: StartStopToken) {
+ startWork(workSpecId, null)
+ }
+
+ /**
+ * @param workSpecId The [WorkSpec] id to start
+ * @param runtimeExtras The [WorkerParameters.RuntimeExtras] associated with this work
+ */
+ fun startWork(workSpecId: StartStopToken, runtimeExtras: RuntimeExtras?)
+
+ /**
+ * @param workSpecId The [WorkSpec] id to stop
+ */
+ fun stopWork(workSpecId: StartStopToken)
+}
+
+class WorkLauncherImpl(
+ val processor: Processor,
+ val workTaskExecutor: TaskExecutor,
+) : WorkLauncher {
+ override fun startWork(workSpecId: StartStopToken, runtimeExtras: RuntimeExtras?) {
+ val startWork = StartWorkRunnable(processor, workSpecId, runtimeExtras)
+ workTaskExecutor.executeOnTaskThread(startWork)
+ }
+
+ override fun stopWork(workSpecId: StartStopToken) {
+ workTaskExecutor.executeOnTaskThread(StopWorkRunnable(processor, workSpecId, false))
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
index c114a88..831e93a1c 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -49,7 +49,6 @@
import androidx.work.WorkManager;
import androidx.work.WorkQuery;
import androidx.work.WorkRequest;
-import androidx.work.WorkerParameters;
import androidx.work.impl.background.greedy.GreedyScheduler;
import androidx.work.impl.background.systemalarm.RescheduleReceiver;
import androidx.work.impl.background.systemjob.SystemJobScheduler;
@@ -64,7 +63,6 @@
import androidx.work.impl.utils.PreferenceUtils;
import androidx.work.impl.utils.PruneWorkRunnable;
import androidx.work.impl.utils.RawQueries;
-import androidx.work.impl.utils.StartWorkRunnable;
import androidx.work.impl.utils.StatusRunnable;
import androidx.work.impl.utils.StopWorkRunnable;
import androidx.work.impl.utils.futures.SettableFuture;
@@ -105,6 +103,7 @@
private BroadcastReceiver.PendingResult mRescheduleReceiverResult;
private volatile RemoteWorkManager mRemoteWorkManager;
private final Trackers mTrackers;
+ private final WorkLauncher mWorkLauncher;
private static WorkManagerImpl sDelegatedInstance = null;
private static WorkManagerImpl sDefaultInstance = null;
private static final Object sLock = new Object();
@@ -283,14 +282,17 @@
Context applicationContext = context.getApplicationContext();
Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
mTrackers = new Trackers(applicationContext, workTaskExecutor);
- List<Scheduler> schedulers =
- createSchedulers(applicationContext, configuration, mTrackers);
- Processor processor = new Processor(
+ mProcessor = new Processor(
context,
configuration,
workTaskExecutor,
database);
- internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
+ mWorkLauncher = new WorkLauncherImpl(mProcessor, workTaskExecutor);
+ mWorkTaskExecutor = workTaskExecutor;
+ mWorkDatabase = database;
+ List<Scheduler> schedulers =
+ createSchedulers(applicationContext, configuration, mTrackers);
+ internalInit(context, configuration, workTaskExecutor, schedulers);
}
/**
@@ -338,7 +340,11 @@
@NonNull Processor processor,
@NonNull Trackers trackers) {
mTrackers = trackers;
- internalInit(context, configuration, workTaskExecutor, workDatabase, schedulers, processor);
+ mWorkLauncher = new WorkLauncherImpl(processor, workTaskExecutor);
+ mProcessor = processor;
+ mWorkTaskExecutor = workTaskExecutor;
+ mWorkDatabase = workDatabase;
+ internalInit(context, configuration, workTaskExecutor, schedulers);
}
/**
@@ -698,38 +704,6 @@
}
/**
- * @param workSpecId The {@link WorkSpec} id to start
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void startWork(@NonNull StartStopToken workSpecId) {
- startWork(workSpecId, null);
- }
-
- /**
- * @param workSpecId The {@link WorkSpec} id to start
- * @param runtimeExtras The {@link WorkerParameters.RuntimeExtras} associated with this work
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void startWork(
- @NonNull StartStopToken workSpecId,
- @Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
- mWorkTaskExecutor
- .executeOnTaskThread(
- new StartWorkRunnable(mProcessor, workSpecId, runtimeExtras));
- }
-
- /**
- * @param workSpecId The {@link WorkSpec} id to stop
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void stopWork(@NonNull StartStopToken workSpecId) {
- mWorkTaskExecutor.executeOnTaskThread(new StopWorkRunnable(mProcessor, workSpecId, false));
- }
-
- /**
* @param id The {@link WorkSpec} id to stop when running in the context of a
* foreground service.
* @hide
@@ -802,28 +776,21 @@
*
* @param context The application {@link Context}
* @param configuration The {@link Configuration} configuration
- * @param workDatabase The {@link WorkDatabase} instance
* @param schedulers The {@link List} of {@link Scheduler}s to use
- * @param processor The {@link Processor} instance
*/
private void internalInit(@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
- @NonNull WorkDatabase workDatabase,
- @NonNull List<Scheduler> schedulers,
- @NonNull Processor processor) {
+ @NonNull List<Scheduler> schedulers) {
context = context.getApplicationContext();
mContext = context;
mConfiguration = configuration;
- mWorkTaskExecutor = workTaskExecutor;
- mWorkDatabase = workDatabase;
mSchedulers = schedulers;
- mProcessor = processor;
- mPreferenceUtils = new PreferenceUtils(workDatabase);
+ mPreferenceUtils = new PreferenceUtils(mWorkDatabase);
mForceStopRunnableCompleted = false;
- Schedulers.registerRescheduling(schedulers, processor,
- workTaskExecutor.getSerialTaskExecutor(), workDatabase, configuration);
+ Schedulers.registerRescheduling(schedulers, mProcessor,
+ workTaskExecutor.getSerialTaskExecutor(), mWorkDatabase, configuration);
// Check for direct boot mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Api24Impl.isDeviceProtectedStorage(
context)) {
@@ -849,7 +816,7 @@
Schedulers.createBestAvailableBackgroundScheduler(context, this),
// Specify the task executor directly here as this happens before internalInit.
// GreedyScheduler creates ConstraintTrackers and controllers eagerly.
- new GreedyScheduler(context, configuration, trackers, this));
+ new GreedyScheduler(context, configuration, trackers, mProcessor, mWorkLauncher));
}
/**
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 656f129..d223a1d 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
@@ -30,10 +30,11 @@
import androidx.work.Logger;
import androidx.work.WorkInfo;
import androidx.work.impl.ExecutionListener;
+import androidx.work.impl.Processor;
import androidx.work.impl.Scheduler;
import androidx.work.impl.StartStopToken;
import androidx.work.impl.StartStopTokens;
-import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.WorkLauncher;
import androidx.work.impl.constraints.WorkConstraintsCallback;
import androidx.work.impl.constraints.WorkConstraintsTracker;
import androidx.work.impl.constraints.WorkConstraintsTrackerImpl;
@@ -59,14 +60,16 @@
private static final String TAG = Logger.tagWithPrefix("GreedyScheduler");
private final Context mContext;
- private final WorkManagerImpl mWorkManagerImpl;
private final WorkConstraintsTracker mWorkConstraintsTracker;
private final Set<WorkSpec> mConstrainedWorkSpecs = new HashSet<>();
private DelayedWorkTracker mDelayedWorkTracker;
private boolean mRegisteredExecutionListener;
- private final Object mLock;
+ private final Object mLock = new Object();
private final StartStopTokens mStartStopTokens = new StartStopTokens();
+ private final Processor mProcessor;
+ private final WorkLauncher mWorkLauncher;
+ private final Configuration mConfiguration;
// Internal State
Boolean mInDefaultProcess;
@@ -74,23 +77,30 @@
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull Trackers trackers,
- @NonNull WorkManagerImpl workManagerImpl) {
+ @NonNull Processor processor,
+ @NonNull WorkLauncher workLauncher
+ ) {
mContext = context;
- mWorkManagerImpl = workManagerImpl;
mWorkConstraintsTracker = new WorkConstraintsTrackerImpl(trackers, this);
mDelayedWorkTracker = new DelayedWorkTracker(this, configuration.getRunnableScheduler());
- mLock = new Object();
+ mConfiguration = configuration;
+ mProcessor = processor;
+ mWorkLauncher = workLauncher;
}
@VisibleForTesting
public GreedyScheduler(
@NonNull Context context,
- @NonNull WorkManagerImpl workManagerImpl,
- @NonNull WorkConstraintsTracker workConstraintsTracker) {
+ @NonNull Configuration configuration,
+ @NonNull WorkConstraintsTracker workConstraintsTracker,
+ @NonNull Processor processor,
+ @NonNull WorkLauncher workLauncher
+ ) {
mContext = context;
- mWorkManagerImpl = workManagerImpl;
+ mProcessor = processor;
+ mWorkLauncher = workLauncher;
mWorkConstraintsTracker = workConstraintsTracker;
- mLock = new Object();
+ mConfiguration = configuration;
}
@VisibleForTesting
@@ -154,7 +164,7 @@
// it doesn't help against races, but reduces useless load in the system
if (!mStartStopTokens.contains(generationalId(workSpec))) {
Logger.get().debug(TAG, "Starting work for " + workSpec.id);
- mWorkManagerImpl.startWork(mStartStopTokens.tokenFor(workSpec));
+ mWorkLauncher.startWork(mStartStopTokens.tokenFor(workSpec));
}
}
}
@@ -173,8 +183,7 @@
}
private void checkDefaultProcess() {
- Configuration configuration = mWorkManagerImpl.getConfiguration();
- mInDefaultProcess = ProcessUtils.isDefaultProcess(mContext, configuration);
+ mInDefaultProcess = ProcessUtils.isDefaultProcess(mContext, mConfiguration);
}
@Override
@@ -195,7 +204,7 @@
}
// onExecutionCompleted does the cleanup.
for (StartStopToken id: mStartStopTokens.remove(workSpecId)) {
- mWorkManagerImpl.stopWork(id);
+ mWorkLauncher.stopWork(id);
}
}
@@ -206,7 +215,7 @@
// it doesn't help against races, but reduces useless load in the system
if (!mStartStopTokens.contains(id)) {
Logger.get().debug(TAG, "Constraints met: Scheduling work ID " + id);
- mWorkManagerImpl.startWork(mStartStopTokens.tokenFor(id));
+ mWorkLauncher.startWork(mStartStopTokens.tokenFor(id));
}
}
}
@@ -218,7 +227,7 @@
Logger.get().debug(TAG, "Constraints not met: Cancelling work ID " + id);
StartStopToken runId = mStartStopTokens.remove(id);
if (runId != null) {
- mWorkManagerImpl.stopWork(runId);
+ mWorkLauncher.stopWork(runId);
}
}
}
@@ -251,7 +260,7 @@
// This method needs to be called *after* Processor is created, since Processor needs
// Schedulers and is created after this class.
if (!mRegisteredExecutionListener) {
- mWorkManagerImpl.getProcessor().addExecutionListener(this);
+ mProcessor.addExecutionListener(this);
mRegisteredExecutionListener = true;
}
}
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 211df7c..e96c4df 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
@@ -316,7 +316,7 @@
}
for (StartStopToken token: tokens) {
Logger.get().debug(TAG, "Handing stopWork work for " + workSpecId);
- dispatcher.getWorkManager().stopWork(token);
+ dispatcher.getWorkerLauncher().stopWork(token);
Alarms.cancelAlarm(mContext,
dispatcher.getWorkManager().getWorkDatabase(), token.getId());
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 cda9784..bd76dbf 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
@@ -31,6 +31,8 @@
import androidx.work.impl.ExecutionListener;
import androidx.work.impl.Processor;
import androidx.work.impl.StartStopTokens;
+import androidx.work.impl.WorkLauncher;
+import androidx.work.impl.WorkLauncherImpl;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkGenerationalId;
import androidx.work.impl.utils.WakeLocks;
@@ -74,16 +76,19 @@
private CommandsCompletedListener mCompletedListener;
private StartStopTokens mStartStopTokens;
+ private final WorkLauncher mWorkLauncher;
SystemAlarmDispatcher(@NonNull Context context) {
- this(context, null, null);
+ this(context, null, null, null);
}
@VisibleForTesting
SystemAlarmDispatcher(
@NonNull Context context,
@Nullable Processor processor,
- @Nullable WorkManagerImpl workManager) {
+ @Nullable WorkManagerImpl workManager,
+ @Nullable WorkLauncher launcher
+ ) {
mContext = context.getApplicationContext();
mStartStopTokens = new StartStopTokens();
mCommandHandler = new CommandHandler(mContext, mStartStopTokens);
@@ -91,6 +96,8 @@
mWorkTimer = new WorkTimer(mWorkManager.getConfiguration().getRunnableScheduler());
mProcessor = processor != null ? processor : mWorkManager.getProcessor();
mTaskExecutor = mWorkManager.getWorkTaskExecutor();
+ mWorkLauncher = launcher != null ? launcher :
+ new WorkLauncherImpl(mProcessor, mTaskExecutor);
mProcessor.addExecutionListener(this);
// a list of pending intents which need to be processed
mIntents = new ArrayList<>();
@@ -190,6 +197,10 @@
return mTaskExecutor;
}
+ WorkLauncher getWorkerLauncher() {
+ return mWorkLauncher;
+ }
+
@MainThread
@SuppressWarnings("WeakerAccess") /* synthetic access */
void dequeueAndCheckForCompletion() {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
index 03f6342..fd39016 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobService.java
@@ -36,8 +36,11 @@
import androidx.work.Logger;
import androidx.work.WorkerParameters;
import androidx.work.impl.ExecutionListener;
+import androidx.work.impl.Processor;
import androidx.work.impl.StartStopToken;
import androidx.work.impl.StartStopTokens;
+import androidx.work.impl.WorkLauncher;
+import androidx.work.impl.WorkLauncherImpl;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkGenerationalId;
@@ -57,13 +60,17 @@
private WorkManagerImpl mWorkManagerImpl;
private final Map<WorkGenerationalId, JobParameters> mJobParameters = new HashMap<>();
private final StartStopTokens mStartStopTokens = new StartStopTokens();
+ private WorkLauncher mWorkLauncher;
@Override
public void onCreate() {
super.onCreate();
try {
mWorkManagerImpl = WorkManagerImpl.getInstance(getApplicationContext());
- mWorkManagerImpl.getProcessor().addExecutionListener(this);
+ Processor processor = mWorkManagerImpl.getProcessor();
+ mWorkLauncher = new WorkLauncherImpl(processor,
+ mWorkManagerImpl.getWorkTaskExecutor());
+ processor.addExecutionListener(this);
} catch (IllegalStateException e) {
// This can occur if...
// 1. The app is performing an auto-backup. Prior to O, JobScheduler could erroneously
@@ -150,7 +157,7 @@
// In such cases, we rely on SystemJobService to ask for a reschedule by calling
// jobFinished(params, true) in onExecuted(...);
// For more information look at b/123211993
- mWorkManagerImpl.startWork(mStartStopTokens.tokenFor(workGenerationalId), runtimeExtras);
+ mWorkLauncher.startWork(mStartStopTokens.tokenFor(workGenerationalId), runtimeExtras);
return true;
}
@@ -174,7 +181,7 @@
}
StartStopToken runId = mStartStopTokens.remove(workGenerationalId);
if (runId != null) {
- mWorkManagerImpl.stopWork(runId);
+ mWorkLauncher.stopWork(runId);
}
return !mWorkManagerImpl.getProcessor().isCancelled(workGenerationalId.getWorkSpecId());
}
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt b/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
index 84d9918..b01ee03 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestScheduler.kt
@@ -22,7 +22,7 @@
import androidx.work.impl.Scheduler
import androidx.work.impl.StartStopTokens
import androidx.work.impl.WorkDatabase
-import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkLauncher
import androidx.work.impl.model.WorkSpec
import androidx.work.impl.model.WorkSpecDao
import androidx.work.impl.model.generationalId
@@ -36,7 +36,10 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class TestScheduler(private val workManagerImpl: WorkManagerImpl) : Scheduler {
+class TestScheduler(
+ private val workDatabase: WorkDatabase,
+ private val launcher: WorkLauncher
+) : Scheduler {
@GuardedBy("lock")
private val pendingWorkStates = mutableMapOf<String, InternalWorkState>()
private val lock = Any()
@@ -73,7 +76,7 @@
// Schedulers.registerRescheduling
override fun cancel(workSpecId: String) {
val tokens = startStopTokens.remove(workSpecId)
- tokens.forEach { workManagerImpl.stopWork(it) }
+ tokens.forEach { launcher.stopWork(it) }
}
/**
@@ -142,13 +145,13 @@
pendingWorkStates.remove(generationalId.workSpecId)
startStopTokens.tokenFor(generationalId)
}
- workManagerImpl.rewindLastEnqueueTime(spec.id)
- workManagerImpl.startWork(token)
+ workDatabase.rewindLastEnqueueTime(spec.id)
+ launcher.startWork(token)
}
}
private fun loadSpec(id: String): WorkSpec {
- val workSpec = workManagerImpl.workDatabase.workSpecDao().getWorkSpec(id)
+ val workSpec = workDatabase.workSpecDao().getWorkSpec(id)
?: throw IllegalArgumentException("Work with id $id is not enqueued!")
return workSpec
}
@@ -171,15 +174,14 @@
private val WorkSpec.isFirstPeriodicRun get() = periodCount == 0 && runAttemptCount == 0
-private fun WorkManagerImpl.rewindLastEnqueueTime(id: String): WorkSpec {
+private fun WorkDatabase.rewindLastEnqueueTime(id: String): WorkSpec {
// We need to pass check that mWorkSpec.calculateNextRunTime() < now
// so we reset "rewind" enqueue time to pass the check
// we don't reuse available internalWorkState.mWorkSpec, because it
// is not update with period_count and last_enqueue_time
// More proper solution would be to abstract away time instead of just using
// System.currentTimeMillis() in WM
- val workDatabase: WorkDatabase = workDatabase
- val dao: WorkSpecDao = workDatabase.workSpecDao()
+ val dao: WorkSpecDao = workSpecDao()
val workSpec: WorkSpec = dao.getWorkSpec(id)
?: throw IllegalStateException("WorkSpec is already deleted from WM's db")
val now = System.currentTimeMillis()
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index f24a8da..37f70ec 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -23,6 +23,7 @@
import androidx.work.Configuration;
import androidx.work.WorkManager;
import androidx.work.impl.Scheduler;
+import androidx.work.impl.WorkLauncherImpl;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.utils.taskexecutor.SerialExecutor;
@@ -94,7 +95,8 @@
@NonNull
public List<Scheduler> createSchedulers(@NonNull Context context,
@NonNull Configuration configuration, @NonNull Trackers trackers) {
- mScheduler = new TestScheduler(this);
+ WorkLauncherImpl launcher = new WorkLauncherImpl(getProcessor(), getWorkTaskExecutor());
+ mScheduler = new TestScheduler(getWorkDatabase(), launcher);
return Collections.singletonList((Scheduler) mScheduler);
}