Merge "Remove sandbox support from SdkSandboxManagerCompat for Api 33 devices." into androidx-main
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
index 77b733f..a5cc9aa 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalDetector.kt
@@ -75,6 +75,7 @@
import org.jetbrains.uast.getContainingUMethod
import org.jetbrains.uast.java.JavaUAnnotation
import org.jetbrains.uast.toUElement
+import org.jetbrains.uast.toUElementOfType
import org.jetbrains.uast.tryResolve
class ExperimentalDetector : Detector(), SourceCodeScanner {
@@ -98,13 +99,11 @@
// Infer the overridden method by taking the first (and only) abstract method from the
// functional interface being implemented.
val superClass = (lambda.functionalInterfaceType as? PsiClassReferenceType)?.resolve()
- val superMethod = superClass?.allMethods
+ superClass?.toUElementOfType<UClass>()?.methods
?.first { method -> method.isAbstract() }
- ?.toUElement()
-
- if (superMethod is UMethod) {
- checkMethodOverride(context, lambda, superMethod)
- }
+ ?.let { superMethod ->
+ checkMethodOverride(context, lambda, superMethod)
+ }
}
override fun visitClass(
@@ -920,4 +919,5 @@
} == true
private fun PsiModifierListOwner.isAbstract(): Boolean =
- modifierList?.hasModifierProperty(PsiModifier.ABSTRACT) == true
+ modifierList?.hasModifierProperty(PsiModifier.ABSTRACT) == true ||
+ hasModifierProperty(PsiModifier.ABSTRACT)
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
index d1f5856..3c4ede5 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
@@ -344,6 +344,33 @@
check(*input).expect(expected)
}
+ @Test
+ fun resolveSamWithValueClass() {
+ val input = arrayOf(
+ kotlin(
+ """
+ @JvmInline
+ value class MyValue(val p: Int)
+
+ fun interface FunInterface {
+ fun sam(): MyValue
+ }
+
+ fun itfConsumer(itf: FunInterface) {
+ itf.sam().p
+ }
+
+ fun test() {
+ itfConsumer {
+ MyValue(42)
+ }
+ }
+ """.trimIndent()
+ )
+ )
+ check(*input).expectClean()
+ }
+
/* ktlint-disable max-line-length */
companion object {
/**
diff --git a/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/properties/ExceptDateSamples.kt b/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/properties/ExceptDateSamples.kt
index a265e12..56c43e7 100644
--- a/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/properties/ExceptDateSamples.kt
+++ b/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/properties/ExceptDateSamples.kt
@@ -26,14 +26,13 @@
public fun exceptDateMapWhenUsage(exceptDate: ExceptDate) =
exceptDate.mapWhen(
object : ExceptDate.Mapper<String> {
- public override fun date(instance: LocalDate): String = """Got LocalDate: $instance"""
+ override fun date(instance: LocalDate): String = """Got LocalDate: $instance"""
- public override fun localDateTime(instance: LocalDateTime): String =
+ override fun localDateTime(instance: LocalDateTime): String =
"""Got a local DateTime: $instance"""
- public override fun instant(instance: Instant): String =
- """Got an absolute DateTime: $instance"""
+ override fun instant(instance: Instant): String = """Got an absolute DateTime: $instance"""
- public override fun orElse(): String = """Got some unrecognized variant: $exceptDate"""
+ override fun orElse(): String = """Got some unrecognized variant: $exceptDate"""
}
)
diff --git a/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/types/DayOfWeekSamples.kt b/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/types/DayOfWeekSamples.kt
index 395abf4..1bdc8b2 100644
--- a/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/types/DayOfWeekSamples.kt
+++ b/appactions/builtintypes/builtintypes/samples/src/main/java/androidx/appactions/builtintypes/samples/types/DayOfWeekSamples.kt
@@ -23,22 +23,22 @@
public fun dayOfWeekMapWhenUsage(dayOfWeek: DayOfWeek) =
dayOfWeek.mapWhen(
object : DayOfWeek.Mapper<String> {
- public override fun friday(): String = "Got Friday"
+ override fun friday(): String = "Got Friday"
- public override fun monday(): String = "Got Monday"
+ override fun monday(): String = "Got Monday"
- public override fun publicHolidays(): String = "Got PublicHolidays"
+ override fun publicHolidays(): String = "Got PublicHolidays"
- public override fun saturday(): String = "Got Saturday"
+ override fun saturday(): String = "Got Saturday"
- public override fun sunday(): String = "Got Sunday"
+ override fun sunday(): String = "Got Sunday"
- public override fun thursday(): String = "Got Thursday"
+ override fun thursday(): String = "Got Thursday"
- public override fun tuesday(): String = "Got Tuesday"
+ override fun tuesday(): String = "Got Tuesday"
- public override fun wednesday(): String = "Got Wednesday"
+ override fun wednesday(): String = "Got Wednesday"
- public override fun orElse(): String = """Got some unrecognized DayOfWeek: $dayOfWeek"""
+ override fun orElse(): String = """Got some unrecognized DayOfWeek: $dayOfWeek"""
}
)
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ByDay.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ByDay.kt
index 1f536336..1f085e1 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ByDay.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ByDay.kt
@@ -51,7 +51,7 @@
/** Constructor for the [DayOfWeek] variant. */
public constructor(dayOfWeek: DayOfWeek) : this(asDayOfWeek = dayOfWeek)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -64,12 +64,12 @@
else -> error("No variant present in ByDay")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ByDay) return false
if (asDayOfWeek != other.asDayOfWeek) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asDayOfWeek)
+ override fun hashCode(): Int = Objects.hash(asDayOfWeek)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt
index 2c5db36..18964fb 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndDate.kt
@@ -51,7 +51,7 @@
/** Constructor for the [LocalDate] variant. */
public constructor(date: LocalDate) : this(asDate = date)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -64,12 +64,12 @@
else -> error("No variant present in EndDate")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EndDate) return false
if (asDate != other.asDate) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asDate)
+ override fun hashCode(): Int = Objects.hash(asDate)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndTime.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndTime.kt
index 7b66224..58d1b00 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndTime.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/EndTime.kt
@@ -56,7 +56,7 @@
/** Constructor for the [LocalTime] variant. */
public constructor(time: LocalTime) : this(asTime = time)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -69,12 +69,12 @@
else -> error("No variant present in EndTime")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EndTime) return false
if (asTime != other.asTime) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asTime)
+ override fun hashCode(): Int = Objects.hash(asTime)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ExceptDate.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ExceptDate.kt
index a2e81d0..4e15b62 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ExceptDate.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/ExceptDate.kt
@@ -92,7 +92,7 @@
else -> error("No variant present in ExceptDate")
}
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -117,7 +117,7 @@
else -> error("No variant present in ExceptDate")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ExceptDate) return false
if (asDate != other.asDate) return false
@@ -126,7 +126,7 @@
return true
}
- public override fun hashCode(): Int = Objects.hash(asDate, asLocalDateTime, asInstant)
+ override fun hashCode(): Int = Objects.hash(asDate, asLocalDateTime, asInstant)
/** Maps each of the possible variants of [ExceptDate] to some [R]. */
public interface Mapper<R> {
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/Name.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/Name.kt
index 5348a83..a2d3f08 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/Name.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/Name.kt
@@ -47,7 +47,7 @@
/** Constructor for the [String] variant. */
public constructor(text: String) : this(asText = text)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -60,12 +60,12 @@
else -> error("No variant present in Name")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Name) return false
if (asText != other.asText) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asText)
+ override fun hashCode(): Int = Objects.hash(asText)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/RepeatFrequency.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/RepeatFrequency.kt
index c66b581..861e71d 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/RepeatFrequency.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/RepeatFrequency.kt
@@ -52,7 +52,7 @@
/** Constructor for the [Duration] variant. */
public constructor(duration: Duration) : this(asDuration = duration)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -65,12 +65,12 @@
else -> error("No variant present in RepeatFrequency")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RepeatFrequency) return false
if (asDuration != other.asDuration) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asDuration)
+ override fun hashCode(): Int = Objects.hash(asDuration)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt
index f6c2b67..431b420 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartDate.kt
@@ -51,7 +51,7 @@
/** Constructor for the [LocalDate] variant. */
public constructor(date: LocalDate) : this(asDate = date)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -64,12 +64,12 @@
else -> error("No variant present in StartDate")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StartDate) return false
if (asDate != other.asDate) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asDate)
+ override fun hashCode(): Int = Objects.hash(asDate)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartTime.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartTime.kt
index f6d9144..ea72193 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartTime.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/properties/StartTime.kt
@@ -56,7 +56,7 @@
/** Constructor for the [LocalTime] variant. */
public constructor(time: LocalTime) : this(asTime = time)
- public override fun toString(): String = toString(includeWrapperName = true)
+ override fun toString(): String = toString(includeWrapperName = true)
internal fun toString(includeWrapperName: Boolean): String =
when {
@@ -69,12 +69,12 @@
else -> error("No variant present in StartTime")
}
- public override fun equals(other: Any?): Boolean {
+ override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StartTime) return false
if (asTime != other.asTime) return false
return true
}
- public override fun hashCode(): Int = Objects.hash(asTime)
+ override fun hashCode(): Int = Objects.hash(asTime)
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/serializers/DayOfWeekAsCanonicalUrlSerializer.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/serializers/DayOfWeekAsCanonicalUrlSerializer.kt
index 5dfdb1e..0579bf4 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/serializers/DayOfWeekAsCanonicalUrlSerializer.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/serializers/DayOfWeekAsCanonicalUrlSerializer.kt
@@ -27,8 +27,8 @@
* @see DayOfWeek.canonicalUrl
*/
public class DayOfWeekAsCanonicalUrlSerializer : StringSerializer<DayOfWeek> {
- public override fun serialize(instance: DayOfWeek): String = instance.canonicalUrl
+ override fun serialize(instance: DayOfWeek): String = instance.canonicalUrl
- public override fun deserialize(`value`: String): DayOfWeek? =
+ override fun deserialize(`value`: String): DayOfWeek? =
DayOfWeek.values().firstOrNull { it.canonicalUrl == value }
}
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt
index a1ff043..4ed329e 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Alarm.kt
@@ -67,7 +67,7 @@
get() = null
/** Converts this [Alarm] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -82,7 +82,7 @@
*/
public interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
/** Returns a built [Alarm]. */
- public override fun build(): Alarm
+ override fun build(): Alarm
/** Sets the `alarmSchedule`. */
@Suppress("DocumentExceptions")
@@ -106,8 +106,8 @@
* )
* class MyAlarm internal constructor(
* alarm: Alarm,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractAlarm<
* MyAlarm,
* MyAlarm.Builder
@@ -127,6 +127,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractAlarm.Builder<
* Builder,
@@ -142,11 +143,11 @@
Builder : AbstractAlarm.Builder<Builder, Self>
>
internal constructor(
- public final override val namespace: String,
- public final override val alarmSchedule: Schedule?,
- @get:Suppress("AutoBoxing") public final override val isAlarmEnabled: Boolean?,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val alarmSchedule: Schedule?,
+ @get:Suppress("AutoBoxing") final override val isAlarmEnabled: Boolean?,
+ final override val identifier: String,
+ final override val name: Name?,
) : Alarm {
/**
* Human readable name for the concrete [Self] class.
@@ -170,7 +171,7 @@
/** Returns a concrete [Builder] with the additional, non-[Alarm] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setAlarmSchedule(alarmSchedule)
@@ -178,7 +179,7 @@
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -191,10 +192,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, alarmSchedule, isAlarmEnabled, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -221,11 +222,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyAlarm :
* : AbstractAlarm<
* MyAlarm,
* MyAlarm.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractAlarm.Builder<
* Builder,
@@ -307,36 +310,36 @@
*/
@Suppress("BuilderSetStyle") protected abstract fun buildFromAlarm(alarm: Alarm): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromAlarm(AlarmImpl(namespace, alarmSchedule, isAlarmEnabled, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setAlarmSchedule(schedule: Schedule?): Self {
+ final override fun setAlarmSchedule(schedule: Schedule?): Self {
this.alarmSchedule = schedule
return this as Self
}
- public final override fun setAlarmEnabled(@Suppress("AutoBoxing") boolean: Boolean?): Self {
+ final override fun setAlarmEnabled(@Suppress("AutoBoxing") boolean: Boolean?): Self {
this.isAlarmEnabled = boolean
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -350,11 +353,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, alarmSchedule, isAlarmEnabled, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/CommonExecutionStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/CommonExecutionStatus.kt
index 067ccb1..9bf51a9 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/CommonExecutionStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/CommonExecutionStatus.kt
@@ -49,7 +49,7 @@
)
public interface CommonExecutionStatus : ExecutionStatus {
/** Converts this [CommonExecutionStatus] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -66,7 +66,7 @@
*/
public interface Builder<Self : Builder<Self>> : ExecutionStatus.Builder<Self> {
/** Returns a built [CommonExecutionStatus]. */
- public override fun build(): CommonExecutionStatus
+ override fun build(): CommonExecutionStatus
}
}
@@ -81,8 +81,8 @@
* )
* class MyCommonExecutionStatus internal constructor(
* commonExecutionStatus: CommonExecutionStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractCommonExecutionStatus<
* MyCommonExecutionStatus,
* MyCommonExecutionStatus.Builder
@@ -102,6 +102,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractCommonExecutionStatus.Builder<
* Builder,
@@ -113,13 +114,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractCommonExecutionStatus<
- Self : AbstractCommonExecutionStatus<Self, Builder>,
- Builder : AbstractCommonExecutionStatus.Builder<Builder, Self>
- >
+ Self : AbstractCommonExecutionStatus<Self, Builder>,
+ Builder : AbstractCommonExecutionStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : CommonExecutionStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -152,13 +153,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -169,10 +170,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -193,11 +194,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyCommonExecutionStatus :
* : AbstractCommonExecutionStatus<
* MyCommonExecutionStatus,
* MyCommonExecutionStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractCommonExecutionStatus.Builder<
* Builder,
@@ -244,9 +247,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractCommonExecutionStatus<Built, Self>
- > : CommonExecutionStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractCommonExecutionStatus<Built, Self>
+ > : CommonExecutionStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -281,26 +284,26 @@
commonExecutionStatus: CommonExecutionStatus
): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromCommonExecutionStatus(CommonExecutionStatusImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -312,11 +315,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/DayOfWeek.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/DayOfWeek.kt
index 5eb1d15..450fdc3 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/DayOfWeek.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/DayOfWeek.kt
@@ -54,7 +54,7 @@
else -> mapper.orElse()
}
- public override fun toString(): String = """DayOfWeek($canonicalUrl)"""
+ override fun toString(): String = """DayOfWeek($canonicalUrl)"""
public companion object {
/** The day of the week between Thursday and Saturday. */
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ExecutionStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ExecutionStatus.kt
index 83fec04..52faa64 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ExecutionStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ExecutionStatus.kt
@@ -49,7 +49,7 @@
)
public interface ExecutionStatus : Intangible {
/** Converts this [ExecutionStatus] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -66,7 +66,7 @@
*/
public interface Builder<Self : Builder<Self>> : Intangible.Builder<Self> {
/** Returns a built [ExecutionStatus]. */
- public override fun build(): ExecutionStatus
+ override fun build(): ExecutionStatus
}
}
@@ -81,8 +81,8 @@
* )
* class MyExecutionStatus internal constructor(
* executionStatus: ExecutionStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractExecutionStatus<
* MyExecutionStatus,
* MyExecutionStatus.Builder
@@ -102,6 +102,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractExecutionStatus.Builder<
* Builder,
@@ -113,13 +114,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractExecutionStatus<
- Self : AbstractExecutionStatus<Self, Builder>,
- Builder : AbstractExecutionStatus.Builder<Builder, Self>
- >
+ Self : AbstractExecutionStatus<Self, Builder>,
+ Builder : AbstractExecutionStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : ExecutionStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -145,13 +146,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -162,10 +163,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -186,11 +187,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyExecutionStatus :
* : AbstractExecutionStatus<
* MyExecutionStatus,
* MyExecutionStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractExecutionStatus.Builder<
* Builder,
@@ -237,9 +240,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractExecutionStatus<Built, Self>
- > : ExecutionStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractExecutionStatus<Built, Self>
+ > : ExecutionStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -271,26 +274,26 @@
@Suppress("BuilderSetStyle")
protected abstract fun buildFromExecutionStatus(executionStatus: ExecutionStatus): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromExecutionStatus(ExecutionStatusImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -302,11 +305,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/GenericErrorStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/GenericErrorStatus.kt
index 6fd1add..9b05a4a 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/GenericErrorStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/GenericErrorStatus.kt
@@ -45,7 +45,7 @@
)
public interface GenericErrorStatus : CommonExecutionStatus {
/** Converts this [GenericErrorStatus] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -62,7 +62,7 @@
*/
public interface Builder<Self : Builder<Self>> : CommonExecutionStatus.Builder<Self> {
/** Returns a built [GenericErrorStatus]. */
- public override fun build(): GenericErrorStatus
+ override fun build(): GenericErrorStatus
}
}
@@ -77,8 +77,8 @@
* )
* class MyGenericErrorStatus internal constructor(
* genericErrorStatus: GenericErrorStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractGenericErrorStatus<
* MyGenericErrorStatus,
* MyGenericErrorStatus.Builder
@@ -98,6 +98,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractGenericErrorStatus.Builder<
* Builder,
@@ -109,13 +110,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractGenericErrorStatus<
- Self : AbstractGenericErrorStatus<Self, Builder>,
- Builder : AbstractGenericErrorStatus.Builder<Builder, Self>
- >
+ Self : AbstractGenericErrorStatus<Self, Builder>,
+ Builder : AbstractGenericErrorStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : GenericErrorStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -142,13 +143,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -159,10 +160,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -183,11 +184,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyGenericErrorStatus :
* : AbstractGenericErrorStatus<
* MyGenericErrorStatus,
* MyGenericErrorStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractGenericErrorStatus.Builder<
* Builder,
@@ -234,9 +237,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractGenericErrorStatus<Built, Self>
- > : GenericErrorStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractGenericErrorStatus<Built, Self>
+ > : GenericErrorStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -271,26 +274,26 @@
genericErrorStatus: GenericErrorStatus
): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromGenericErrorStatus(GenericErrorStatusImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -302,11 +305,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Intangible.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Intangible.kt
index 7f3e192..09aaac6 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Intangible.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Intangible.kt
@@ -46,7 +46,7 @@
)
public interface Intangible : Thing {
/** Converts this [Intangible] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -61,7 +61,7 @@
*/
public interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
/** Returns a built [Intangible]. */
- public override fun build(): Intangible
+ override fun build(): Intangible
}
}
@@ -76,8 +76,8 @@
* )
* class MyIntangible internal constructor(
* intangible: Intangible,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractIntangible<
* MyIntangible,
* MyIntangible.Builder
@@ -97,6 +97,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractIntangible.Builder<
* Builder,
@@ -108,13 +109,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractIntangible<
- Self : AbstractIntangible<Self, Builder>,
- Builder : AbstractIntangible.Builder<Builder, Self>
- >
+ Self : AbstractIntangible<Self, Builder>,
+ Builder : AbstractIntangible.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : Intangible {
/**
* Human readable name for the concrete [Self] class.
@@ -138,13 +139,13 @@
/** Returns a concrete [Builder] with the additional, non-[Intangible] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -155,10 +156,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -179,11 +180,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyIntangible :
* : AbstractIntangible<
* MyIntangible,
* MyIntangible.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractIntangible.Builder<
* Builder,
@@ -230,9 +233,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractIntangible<Built, Self>
- > : Intangible.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractIntangible<Built, Self>
+ > : Intangible.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -264,26 +267,26 @@
@Suppress("BuilderSetStyle")
protected abstract fun buildFromIntangible(intangible: Intangible): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromIntangible(IntangibleImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -295,11 +298,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ObjectCreationLimitReachedStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ObjectCreationLimitReachedStatus.kt
index e5cc7ed..bb7888c 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ObjectCreationLimitReachedStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/ObjectCreationLimitReachedStatus.kt
@@ -50,7 +50,7 @@
* Converts this [ObjectCreationLimitReachedStatus] to its builder with all the properties copied
* over.
*/
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -67,7 +67,7 @@
*/
public interface Builder<Self : Builder<Self>> : ExecutionStatus.Builder<Self> {
/** Returns a built [ObjectCreationLimitReachedStatus]. */
- public override fun build(): ObjectCreationLimitReachedStatus
+ override fun build(): ObjectCreationLimitReachedStatus
}
}
@@ -82,8 +82,8 @@
* )
* class MyObjectCreationLimitReachedStatus internal constructor(
* objectCreationLimitReachedStatus: ObjectCreationLimitReachedStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractObjectCreationLimitReachedStatus<
* MyObjectCreationLimitReachedStatus,
* MyObjectCreationLimitReachedStatus.Builder
@@ -103,6 +103,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractObjectCreationLimitReachedStatus.Builder<
* Builder,
@@ -114,13 +115,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractObjectCreationLimitReachedStatus<
- Self : AbstractObjectCreationLimitReachedStatus<Self, Builder>,
- Builder : AbstractObjectCreationLimitReachedStatus.Builder<Builder, Self>
- >
+ Self : AbstractObjectCreationLimitReachedStatus<Self, Builder>,
+ Builder : AbstractObjectCreationLimitReachedStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : ObjectCreationLimitReachedStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -154,13 +155,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -171,10 +172,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -195,11 +196,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyObjectCreationLimitReachedStatus :
* : AbstractObjectCreationLimitReachedStatus<
* MyObjectCreationLimitReachedStatus,
* MyObjectCreationLimitReachedStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractObjectCreationLimitReachedStatus.Builder<
* Builder,
@@ -246,9 +249,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractObjectCreationLimitReachedStatus<Built, Self>
- > : ObjectCreationLimitReachedStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractObjectCreationLimitReachedStatus<Built, Self>
+ > : ObjectCreationLimitReachedStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -283,28 +286,28 @@
objectCreationLimitReachedStatus: ObjectCreationLimitReachedStatus
): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromObjectCreationLimitReachedStatus(
ObjectCreationLimitReachedStatusImpl(namespace, identifier, name)
)
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -316,11 +319,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -341,8 +344,8 @@
private class ObjectCreationLimitReachedStatusImpl :
AbstractObjectCreationLimitReachedStatus<
- ObjectCreationLimitReachedStatusImpl, ObjectCreationLimitReachedStatusImpl.Builder
- > {
+ ObjectCreationLimitReachedStatusImpl, ObjectCreationLimitReachedStatusImpl.Builder
+ > {
protected override val selfTypeName: String
get() = "ObjectCreationLimitReachedStatus"
@@ -363,8 +366,8 @@
public class Builder :
AbstractObjectCreationLimitReachedStatus.Builder<
- Builder, ObjectCreationLimitReachedStatusImpl
- >() {
+ Builder, ObjectCreationLimitReachedStatusImpl
+ >() {
protected override val selfTypeName: String
get() = "ObjectCreationLimitReachedStatus.Builder"
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Person.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Person.kt
index 35da28c..51aca82 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Person.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Person.kt
@@ -64,7 +64,7 @@
get() = null
/** Converts this [Person] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -79,7 +79,7 @@
*/
public interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
/** Returns a built [Person]. */
- public override fun build(): Person
+ override fun build(): Person
/** Sets the `email`. */
@Suppress("DocumentExceptions")
@@ -102,8 +102,8 @@
* )
* class MyPerson internal constructor(
* person: Person,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractPerson<
* MyPerson,
* MyPerson.Builder
@@ -123,6 +123,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractPerson.Builder<
* Builder,
@@ -138,11 +139,11 @@
Builder : AbstractPerson.Builder<Builder, Self>
>
internal constructor(
- public final override val namespace: String,
- public final override val email: String?,
- public final override val telephoneNumber: String?,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val email: String?,
+ final override val telephoneNumber: String?,
+ final override val identifier: String,
+ final override val name: Name?,
) : Person {
/**
* Human readable name for the concrete [Self] class.
@@ -166,7 +167,7 @@
/** Returns a concrete [Builder] with the additional, non-[Person] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setEmail(email)
@@ -174,7 +175,7 @@
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -187,10 +188,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, email, telephoneNumber, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -217,11 +218,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyPerson :
* : AbstractPerson<
* MyPerson,
* MyPerson.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractPerson.Builder<
* Builder,
@@ -303,36 +306,36 @@
*/
@Suppress("BuilderSetStyle") protected abstract fun buildFromPerson(person: Person): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromPerson(PersonImpl(namespace, email, telephoneNumber, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setEmail(text: String?): Self {
+ final override fun setEmail(text: String?): Self {
this.email = text
return this as Self
}
- public final override fun setTelephoneNumber(text: String?): Self {
+ final override fun setTelephoneNumber(text: String?): Self {
this.telephoneNumber = text
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -346,11 +349,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, email, telephoneNumber, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Schedule.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Schedule.kt
index 0f2ec37..8a83a84 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Schedule.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Schedule.kt
@@ -201,7 +201,7 @@
get() = null
/** Converts this [Schedule] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -216,7 +216,7 @@
*/
public interface Builder<Self : Builder<Self>> : Intangible.Builder<Self> {
/** Returns a built [Schedule]. */
- public override fun build(): Schedule
+ override fun build(): Schedule
/** Appends [DayOfWeek] as a value to `byDays`. */
public fun addByDay(dayOfWeek: DayOfWeek): Self = addByDay(ByDay(dayOfWeek))
@@ -340,8 +340,8 @@
* )
* class MySchedule internal constructor(
* schedule: Schedule,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractSchedule<
* MySchedule,
* MySchedule.Builder
@@ -361,6 +361,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractSchedule.Builder<
* Builder,
@@ -376,21 +377,21 @@
Builder : AbstractSchedule.Builder<Builder, Self>
>
internal constructor(
- public final override val namespace: String,
- public final override val byDays: List<ByDay>,
- public final override val byMonths: List<Long>,
- public final override val byMonthDays: List<Long>,
- public final override val byMonthWeeks: List<Long>,
- public final override val endDate: EndDate?,
- public final override val endTime: EndTime?,
- public final override val exceptDate: ExceptDate?,
- @get:Suppress("AutoBoxing") public final override val repeatCount: Long?,
- public final override val repeatFrequency: RepeatFrequency?,
- public final override val scheduleTimezone: String?,
- public final override val startDate: StartDate?,
- public final override val startTime: StartTime?,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val byDays: List<ByDay>,
+ final override val byMonths: List<Long>,
+ final override val byMonthDays: List<Long>,
+ final override val byMonthWeeks: List<Long>,
+ final override val endDate: EndDate?,
+ final override val endTime: EndTime?,
+ final override val exceptDate: ExceptDate?,
+ @get:Suppress("AutoBoxing") final override val repeatCount: Long?,
+ final override val repeatFrequency: RepeatFrequency?,
+ final override val scheduleTimezone: String?,
+ final override val startDate: StartDate?,
+ final override val startTime: StartTime?,
+ final override val identifier: String,
+ final override val name: Name?,
) : Schedule {
/**
* Human readable name for the concrete [Self] class.
@@ -430,7 +431,7 @@
/** Returns a concrete [Builder] with the additional, non-[Schedule] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.addByDays(byDays)
@@ -448,7 +449,7 @@
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -471,7 +472,7 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(
namespace,
byDays,
@@ -491,7 +492,7 @@
additionalProperties
)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -548,11 +549,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MySchedule :
* : AbstractSchedule<
* MySchedule,
* MySchedule.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractSchedule.Builder<
* Builder,
@@ -656,7 +659,7 @@
*/
@Suppress("BuilderSetStyle") protected abstract fun buildFromSchedule(schedule: Schedule): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromSchedule(
ScheduleImpl(
namespace,
@@ -677,123 +680,123 @@
)
)
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun addByDay(byDay: ByDay): Self {
+ final override fun addByDay(byDay: ByDay): Self {
byDays += byDay
return this as Self
}
- public final override fun addByDays(values: Iterable<ByDay>): Self {
+ final override fun addByDays(values: Iterable<ByDay>): Self {
byDays += values
return this as Self
}
- public final override fun clearByDays(): Self {
+ final override fun clearByDays(): Self {
byDays.clear()
return this as Self
}
- public final override fun addByMonth(integer: Long): Self {
+ final override fun addByMonth(integer: Long): Self {
byMonths += integer
return this as Self
}
- public final override fun addByMonths(values: Iterable<Long>): Self {
+ final override fun addByMonths(values: Iterable<Long>): Self {
byMonths += values
return this as Self
}
- public final override fun clearByMonths(): Self {
+ final override fun clearByMonths(): Self {
byMonths.clear()
return this as Self
}
- public final override fun addByMonthDay(integer: Long): Self {
+ final override fun addByMonthDay(integer: Long): Self {
byMonthDays += integer
return this as Self
}
- public final override fun addByMonthDays(values: Iterable<Long>): Self {
+ final override fun addByMonthDays(values: Iterable<Long>): Self {
byMonthDays += values
return this as Self
}
- public final override fun clearByMonthDays(): Self {
+ final override fun clearByMonthDays(): Self {
byMonthDays.clear()
return this as Self
}
- public final override fun addByMonthWeek(integer: Long): Self {
+ final override fun addByMonthWeek(integer: Long): Self {
byMonthWeeks += integer
return this as Self
}
- public final override fun addByMonthWeeks(values: Iterable<Long>): Self {
+ final override fun addByMonthWeeks(values: Iterable<Long>): Self {
byMonthWeeks += values
return this as Self
}
- public final override fun clearByMonthWeeks(): Self {
+ final override fun clearByMonthWeeks(): Self {
byMonthWeeks.clear()
return this as Self
}
- public final override fun setEndDate(endDate: EndDate?): Self {
+ final override fun setEndDate(endDate: EndDate?): Self {
this.endDate = endDate
return this as Self
}
- public final override fun setEndTime(endTime: EndTime?): Self {
+ final override fun setEndTime(endTime: EndTime?): Self {
this.endTime = endTime
return this as Self
}
- public final override fun setExceptDate(exceptDate: ExceptDate?): Self {
+ final override fun setExceptDate(exceptDate: ExceptDate?): Self {
this.exceptDate = exceptDate
return this as Self
}
- public final override fun setRepeatCount(@Suppress("AutoBoxing") integer: Long?): Self {
+ final override fun setRepeatCount(@Suppress("AutoBoxing") integer: Long?): Self {
this.repeatCount = integer
return this as Self
}
- public final override fun setRepeatFrequency(repeatFrequency: RepeatFrequency?): Self {
+ final override fun setRepeatFrequency(repeatFrequency: RepeatFrequency?): Self {
this.repeatFrequency = repeatFrequency
return this as Self
}
- public final override fun setScheduleTimezone(text: String?): Self {
+ final override fun setScheduleTimezone(text: String?): Self {
this.scheduleTimezone = text
return this as Self
}
- public final override fun setStartDate(startDate: StartDate?): Self {
+ final override fun setStartDate(startDate: StartDate?): Self {
this.startDate = startDate
return this as Self
}
- public final override fun setStartTime(startTime: StartTime?): Self {
+ final override fun setStartTime(startTime: StartTime?): Self {
this.startTime = startTime
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -817,7 +820,7 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(
namespace,
byDays,
@@ -838,7 +841,7 @@
)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/SuccessStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/SuccessStatus.kt
index 48d1590..65ac499 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/SuccessStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/SuccessStatus.kt
@@ -45,7 +45,7 @@
)
public interface SuccessStatus : CommonExecutionStatus {
/** Converts this [SuccessStatus] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -62,7 +62,7 @@
*/
public interface Builder<Self : Builder<Self>> : CommonExecutionStatus.Builder<Self> {
/** Returns a built [SuccessStatus]. */
- public override fun build(): SuccessStatus
+ override fun build(): SuccessStatus
}
}
@@ -77,8 +77,8 @@
* )
* class MySuccessStatus internal constructor(
* successStatus: SuccessStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractSuccessStatus<
* MySuccessStatus,
* MySuccessStatus.Builder
@@ -98,6 +98,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractSuccessStatus.Builder<
* Builder,
@@ -109,13 +110,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractSuccessStatus<
- Self : AbstractSuccessStatus<Self, Builder>,
- Builder : AbstractSuccessStatus.Builder<Builder, Self>
- >
+ Self : AbstractSuccessStatus<Self, Builder>,
+ Builder : AbstractSuccessStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : SuccessStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -141,13 +142,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -158,10 +159,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -182,11 +183,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MySuccessStatus :
* : AbstractSuccessStatus<
* MySuccessStatus,
* MySuccessStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractSuccessStatus.Builder<
* Builder,
@@ -233,9 +236,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractSuccessStatus<Built, Self>
- > : SuccessStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractSuccessStatus<Built, Self>
+ > : SuccessStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -267,26 +270,26 @@
@Suppress("BuilderSetStyle")
protected abstract fun buildFromSuccessStatus(successStatus: SuccessStatus): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromSuccessStatus(SuccessStatusImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -298,11 +301,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Thing.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
index 585d0c9..f86de3b 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Thing.kt
@@ -113,8 +113,8 @@
* )
* class MyThing internal constructor(
* thing: Thing,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractThing<
* MyThing,
* MyThing.Builder
@@ -134,6 +134,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractThing.Builder<
* Builder,
@@ -149,9 +150,9 @@
Builder : AbstractThing.Builder<Builder, Self>
>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : Thing {
/**
* Human readable name for the concrete [Self] class.
@@ -173,13 +174,13 @@
/** Returns a concrete [Builder] with the additional, non-[Thing] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -190,10 +191,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -214,11 +215,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyThing :
* : AbstractThing<
* MyThing,
* MyThing.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractThing.Builder<
* Builder,
@@ -296,26 +299,25 @@
*/
@Suppress("BuilderSetStyle") protected abstract fun buildFromThing(thing: Thing): Built
- public final override fun build(): Built =
- buildFromThing(ThingImpl(namespace, identifier, name))
+ final override fun build(): Built = buildFromThing(ThingImpl(namespace, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -327,11 +329,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Timer.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Timer.kt
index a40ebf1..c34b587 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Timer.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/Timer.kt
@@ -57,7 +57,7 @@
get() = null
/** Converts this [Timer] to its builder with all the properties copied over. */
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -72,7 +72,7 @@
*/
public interface Builder<Self : Builder<Self>> : Thing.Builder<Self> {
/** Returns a built [Timer]. */
- public override fun build(): Timer
+ override fun build(): Timer
/** Sets the `duration`. */
@Suppress("DocumentExceptions")
@@ -91,8 +91,8 @@
* )
* class MyTimer internal constructor(
* timer: Timer,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractTimer<
* MyTimer,
* MyTimer.Builder
@@ -112,6 +112,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractTimer.Builder<
* Builder,
@@ -127,10 +128,10 @@
Builder : AbstractTimer.Builder<Builder, Self>
>
internal constructor(
- public final override val namespace: String,
- public final override val duration: Duration?,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val duration: Duration?,
+ final override val identifier: String,
+ final override val name: Name?,
) : Timer {
/**
* Human readable name for the concrete [Self] class.
@@ -154,14 +155,14 @@
/** Returns a concrete [Builder] with the additional, non-[Timer] properties copied over. */
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setDuration(duration)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -173,10 +174,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, duration, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -200,11 +201,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyTimer :
* : AbstractTimer<
* MyTimer,
* MyTimer.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractTimer.Builder<
* Builder,
@@ -284,31 +287,31 @@
*/
@Suppress("BuilderSetStyle") protected abstract fun buildFromTimer(timer: Timer): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromTimer(TimerImpl(namespace, duration, identifier, name))
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setDuration(duration: Duration?): Self {
+ final override fun setDuration(duration: Duration?): Self {
this.duration = duration
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -321,11 +324,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, duration, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
diff --git a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/UnsupportedOperationStatus.kt b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/UnsupportedOperationStatus.kt
index f4c63a4..7743764 100644
--- a/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/UnsupportedOperationStatus.kt
+++ b/appactions/builtintypes/builtintypes/src/main/java/androidx/appactions/builtintypes/types/UnsupportedOperationStatus.kt
@@ -48,7 +48,7 @@
/**
* Converts this [UnsupportedOperationStatus] to its builder with all the properties copied over.
*/
- public override fun toBuilder(): Builder<*>
+ override fun toBuilder(): Builder<*>
public companion object {
/** Returns a default implementation of [Builder]. */
@@ -65,7 +65,7 @@
*/
public interface Builder<Self : Builder<Self>> : ExecutionStatus.Builder<Self> {
/** Returns a built [UnsupportedOperationStatus]. */
- public override fun build(): UnsupportedOperationStatus
+ override fun build(): UnsupportedOperationStatus
}
}
@@ -80,8 +80,8 @@
* )
* class MyUnsupportedOperationStatus internal constructor(
* unsupportedOperationStatus: UnsupportedOperationStatus,
- * val foo: String,
- * val bars: List<Int>,
+ * @Document.StringProperty val foo: String,
+ * @Document.LongProperty val bars: List<Int>,
* ) : AbstractUnsupportedOperationStatus<
* MyUnsupportedOperationStatus,
* MyUnsupportedOperationStatus.Builder
@@ -101,6 +101,7 @@
* .addBars(bars)
* }
*
+ * @Document.BuilderProducer
* class Builder :
* AbstractUnsupportedOperationStatus.Builder<
* Builder,
@@ -112,13 +113,13 @@
*/
@Suppress("UNCHECKED_CAST")
public abstract class AbstractUnsupportedOperationStatus<
- Self : AbstractUnsupportedOperationStatus<Self, Builder>,
- Builder : AbstractUnsupportedOperationStatus.Builder<Builder, Self>
- >
+ Self : AbstractUnsupportedOperationStatus<Self, Builder>,
+ Builder : AbstractUnsupportedOperationStatus.Builder<Builder, Self>
+>
internal constructor(
- public final override val namespace: String,
- public final override val identifier: String,
- public final override val name: Name?,
+ final override val namespace: String,
+ final override val identifier: String,
+ final override val name: Name?,
) : UnsupportedOperationStatus {
/**
* Human readable name for the concrete [Self] class.
@@ -152,13 +153,13 @@
*/
protected abstract fun toBuilderWithAdditionalPropertiesOnly(): Builder
- public final override fun toBuilder(): Builder =
+ final override fun toBuilder(): Builder =
toBuilderWithAdditionalPropertiesOnly()
.setNamespace(namespace)
.setIdentifier(identifier)
.setName(name)
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -169,10 +170,10 @@
return true
}
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -193,11 +194,13 @@
*
* Allows for extension like:
* ```kt
+ * @Document(...)
* class MyUnsupportedOperationStatus :
* : AbstractUnsupportedOperationStatus<
* MyUnsupportedOperationStatus,
* MyUnsupportedOperationStatus.Builder>(...) {
*
+ * @Document.BuilderProducer
* class Builder
* : AbstractUnsupportedOperationStatus.Builder<
* Builder,
@@ -244,9 +247,9 @@
*/
@Suppress("StaticFinalBuilder")
public abstract class Builder<
- Self : Builder<Self, Built>,
- Built : AbstractUnsupportedOperationStatus<Built, Self>
- > : UnsupportedOperationStatus.Builder<Self> {
+ Self : Builder<Self, Built>,
+ Built : AbstractUnsupportedOperationStatus<Built, Self>
+ > : UnsupportedOperationStatus.Builder<Self> {
/**
* Human readable name for the concrete [Self] class.
*
@@ -281,28 +284,28 @@
unsupportedOperationStatus: UnsupportedOperationStatus
): Built
- public final override fun build(): Built =
+ final override fun build(): Built =
buildFromUnsupportedOperationStatus(
UnsupportedOperationStatusImpl(namespace, identifier, name)
)
- public final override fun setNamespace(namespace: String): Self {
+ final override fun setNamespace(namespace: String): Self {
this.namespace = namespace
return this as Self
}
- public final override fun setIdentifier(text: String): Self {
+ final override fun setIdentifier(text: String): Self {
this.identifier = text
return this as Self
}
- public final override fun setName(name: Name?): Self {
+ final override fun setName(name: Name?): Self {
this.name = name
return this as Self
}
@Suppress("BuilderSetStyle")
- public final override fun equals(other: Any?): Boolean {
+ final override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.java != other::class.java) return false
other as Self
@@ -314,11 +317,11 @@
}
@Suppress("BuilderSetStyle")
- public final override fun hashCode(): Int =
+ final override fun hashCode(): Int =
Objects.hash(namespace, identifier, name, additionalProperties)
@Suppress("BuilderSetStyle")
- public final override fun toString(): String {
+ final override fun toString(): String {
val attributes = mutableMapOf<String, String>()
if (namespace.isNotEmpty()) {
attributes["namespace"] = namespace
@@ -339,8 +342,8 @@
private class UnsupportedOperationStatusImpl :
AbstractUnsupportedOperationStatus<
- UnsupportedOperationStatusImpl, UnsupportedOperationStatusImpl.Builder
- > {
+ UnsupportedOperationStatusImpl, UnsupportedOperationStatusImpl.Builder
+ > {
protected override val selfTypeName: String
get() = "UnsupportedOperationStatus"
diff --git a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
index 2285232..04e9541 100644
--- a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
+++ b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
@@ -62,13 +62,9 @@
insets.right,
insets.bottom
);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
return Api18Impl.getOpticalInsets(DrawableCompat.unwrap(drawable));
}
-
- // If we reach here, either we're running on a device pre-v18, the Drawable didn't have
- // any optical insets, or a reflection issue, so we'll just return an empty rect.
- return INSETS_NONE;
}
/**
@@ -140,8 +136,7 @@
}
}
- // Only accessible on SDK_INT >= 18 and < 29.
- @RequiresApi(18)
+ // Only accessible on SDK_INT < 29.
static class Api18Impl {
private static final boolean sReflectionSuccessful;
private static final Method sGetOpticalInsets;
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
index 446b6d8..684696a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
@@ -34,7 +34,6 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.View;
import android.widget.TextView;
import androidx.annotation.DoNotInline;
@@ -47,7 +46,6 @@
import androidx.core.view.ViewCompat;
import androidx.core.widget.TextViewCompat;
-import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -75,11 +73,6 @@
@SuppressLint("BanConcurrentHashMap")
private static java.util.concurrent.ConcurrentHashMap<String, Method>
sTextViewMethodByNameCache = new java.util.concurrent.ConcurrentHashMap<>();
- // Cache of TextView fields used via reflection; the key is the field name and the value is
- // the field itself or null if it can not be found.
- @SuppressLint("BanConcurrentHashMap")
- private static java.util.concurrent.ConcurrentHashMap<String, Field> sTextViewFieldByNameCache =
- new java.util.concurrent.ConcurrentHashMap<>();
// Use this to specify that any of the auto-size configuration int values have not been set.
static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
// Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when
@@ -647,14 +640,12 @@
setRawTextSize(TypedValue.applyDimension(unit, size, res.getDisplayMetrics()));
}
+ @SuppressLint("BanUncheckedReflection")
private void setRawTextSize(float size) {
if (size != mTextView.getPaint().getTextSize()) {
mTextView.getPaint().setTextSize(size);
- boolean isInLayout = false;
- if (Build.VERSION.SDK_INT >= 18) {
- isInLayout = Api18Impl.isInLayout(mTextView);
- }
+ boolean isInLayout = mTextView.isInLayout();
if (mTextView.getLayout() != null) {
// Do not auto-size right after setting the text size.
@@ -731,11 +722,18 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Api23Impl.createStaticLayoutForMeasuring(
text, alignment, availableWidth, maxLines, mTextView, mTempTextPaint, mImpl);
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- return Api16Impl.createStaticLayoutForMeasuring(
- text, alignment, availableWidth, mTextView, mTempTextPaint);
} else {
- return createStaticLayoutForMeasuringPre16(text, alignment, availableWidth);
+ final float lineSpacingMultiplier = mTextView.getLineSpacingMultiplier();
+ final float lineSpacingAdd = mTextView.getLineSpacingExtra();
+ final boolean includePad = mTextView.getIncludeFontPadding();
+
+ // The layout could not be constructed using the builder so fall back to the
+ // most broad constructor.
+ return new StaticLayout(text, mTempTextPaint, availableWidth,
+ alignment,
+ lineSpacingMultiplier,
+ lineSpacingAdd,
+ includePad);
}
}
@@ -749,7 +747,7 @@
}
}
- final int maxLines = Build.VERSION.SDK_INT >= 16 ? Api16Impl.getMaxLines(mTextView) : -1;
+ final int maxLines = mTextView.getMaxLines();
initTempTextPaint(suggestedSizeInPx);
// Needs reflection call due to being private.
@@ -771,25 +769,7 @@
return true;
}
-
- private StaticLayout createStaticLayoutForMeasuringPre16(CharSequence text,
- Layout.Alignment alignment, int availableWidth) {
- // The default values have been inlined with the StaticLayout defaults.
-
- final float lineSpacingMultiplier = accessAndReturnWithDefault(mTextView,
- "mSpacingMult", 1.0f);
- final float lineSpacingAdd = accessAndReturnWithDefault(mTextView,
- "mSpacingAdd", 0.0f);
- final boolean includePad = accessAndReturnWithDefault(mTextView,
- "mIncludePad", true);
-
- return new StaticLayout(text, mTempTextPaint, availableWidth,
- alignment,
- lineSpacingMultiplier,
- lineSpacingAdd,
- includePad);
- }
-
+ @SuppressLint("BanUncheckedReflection")
@SuppressWarnings("unchecked")
// This is marked package-protected so that it doesn't require a synthetic accessor
// when being used from the Impl inner classes
@@ -814,22 +794,6 @@
return result;
}
- @SuppressWarnings("unchecked")
- private static <T> T accessAndReturnWithDefault(@NonNull Object object,
- @NonNull final String fieldName, @NonNull final T defaultValue) {
- try {
- final Field field = getTextViewField(fieldName);
- if (field == null) {
- return defaultValue;
- }
-
- return (T) field.get(object);
- } catch (IllegalAccessException e) {
- Log.w(TAG, "Failed to access TextView#" + fieldName + " member", e);
- return defaultValue;
- }
- }
-
@Nullable
private static Method getTextViewMethod(@NonNull final String methodName) {
try {
@@ -850,25 +814,6 @@
}
}
- @Nullable
- private static Field getTextViewField(@NonNull final String fieldName) {
- try {
- Field field = sTextViewFieldByNameCache.get(fieldName);
- if (field == null) {
- field = TextView.class.getDeclaredField(fieldName);
- if (field != null) {
- field.setAccessible(true);
- sTextViewFieldByNameCache.put(fieldName, field);
- }
- }
-
- return field;
- } catch (NoSuchFieldException e) {
- Log.w(TAG, "Failed to access TextView#" + fieldName + " member", e);
- return null;
- }
- }
-
/**
* @return {@code true} if this widget supports auto-sizing text and has been configured to
* auto-size.
@@ -928,50 +873,4 @@
return layoutBuilder.build();
}
}
-
- @RequiresApi(18)
- private static final class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static boolean isInLayout(@NonNull View view) {
- return view.isInLayout();
- }
- }
-
- @RequiresApi(16)
- private static final class Api16Impl {
- private Api16Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getMaxLines(@NonNull TextView textView) {
- return textView.getMaxLines();
- }
-
- @DoNotInline
- @NonNull
- static StaticLayout createStaticLayoutForMeasuring(
- @NonNull CharSequence text,
- @NonNull Layout.Alignment alignment,
- int availableWidth,
- @NonNull TextView textView,
- @NonNull TextPaint tempTextPaint
- ) {
- final float lineSpacingMultiplier = textView.getLineSpacingMultiplier();
- final float lineSpacingAdd = textView.getLineSpacingExtra();
- final boolean includePad = textView.getIncludeFontPadding();
-
- // The layout could not be constructed using the builder so fall back to the
- // most broad constructor.
- return new StaticLayout(text, tempTextPaint, availableWidth,
- alignment,
- lineSpacingMultiplier,
- lineSpacingAdd,
- includePad);
- }
- }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
index c764cb6..24ca26b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
@@ -48,11 +48,9 @@
import android.widget.Switch;
import android.widget.TextView;
-import androidx.annotation.DoNotInline;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.appcompat.R;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.text.AllCapsTransformationMethod;
@@ -1139,9 +1137,7 @@
final float targetPosition = newCheckedState ? 1 : 0;
mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.setAutoCancel(mPositionAnimator, true);
- }
+ mPositionAnimator.setAutoCancel(true);
mPositionAnimator.start();
}
@@ -1692,16 +1688,4 @@
}
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setAutoCancel(ObjectAnimator objectAnimator, boolean cancel) {
- objectAnimator.setAutoCancel(cancel);
- }
- }
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentClassCreationInfo.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentClassCreationInfo.java
index 65c7b554..f882f0f 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentClassCreationInfo.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentClassCreationInfo.java
@@ -450,8 +450,8 @@
}
/**
- * Makes sure the type is a {@link DeclaredType} with a non-private & non-static method
- * of the form {@code DocumentClass build()}.
+ * Makes sure the builder type is a {@link DeclaredType} with a non-private & non-static
+ * method of the form {@code DocumentClass build()}.
*
* @param annotatedElement The method/class annotated with
* {@code @Document.BuilderProducer}.
@@ -469,12 +469,12 @@
if (builderType.getKind() != TypeKind.DECLARED) {
throw exception;
}
- TypeElement builderClass = (TypeElement) ((DeclaredType) builderType).asElement();
- boolean hasBuildMethod = helper.getAllMethods(builderClass).stream()
- .anyMatch(method -> !method.getModifiers().contains(Modifier.STATIC)
- && !method.getModifiers().contains(Modifier.PRIVATE)
- && helper.isReturnTypeMatching(method, documentClass.asType())
- && method.getParameters().isEmpty());
+ boolean hasBuildMethod = helper.getAllMethods((DeclaredType) builderType)
+ .anyMatch(method -> method.getElement().getSimpleName().contentEquals("build")
+ && !method.getElement().getModifiers().contains(Modifier.STATIC)
+ && !method.getElement().getModifiers().contains(Modifier.PRIVATE)
+ && helper.isReturnTypeMatching(method.getType(), documentClass.asType())
+ && method.getType().getParameterTypes().isEmpty());
if (!hasBuildMethod) {
throw exception;
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 7884791..8ad865d 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -41,6 +41,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
+import java.util.stream.Stream;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
@@ -121,6 +122,7 @@
final TypeMirror mBytePrimitiveType;
private final ProcessingEnvironment mEnv;
private final Types mTypeUtils;
+ private final Elements mElementUtils;
private final WeakHashMap<TypeElement, LinkedHashSet<ExecutableElement>> mAllMethodsCache =
new WeakHashMap<>();
@@ -128,27 +130,27 @@
IntrospectionHelper(ProcessingEnvironment env) {
mEnv = env;
- Elements elementUtil = env.getElementUtils();
+ mElementUtils = env.getElementUtils();
mTypeUtils = env.getTypeUtils();
- mCollectionType = elementUtil.getTypeElement(Collection.class.getName()).asType();
- mListType = elementUtil.getTypeElement(List.class.getName()).asType();
- mStringType = elementUtil.getTypeElement(String.class.getName()).asType();
- mIntegerBoxType = elementUtil.getTypeElement(Integer.class.getName()).asType();
+ mCollectionType = mElementUtils.getTypeElement(Collection.class.getName()).asType();
+ mListType = mElementUtils.getTypeElement(List.class.getName()).asType();
+ mStringType = mElementUtils.getTypeElement(String.class.getName()).asType();
+ mIntegerBoxType = mElementUtils.getTypeElement(Integer.class.getName()).asType();
mIntPrimitiveType = mTypeUtils.unboxedType(mIntegerBoxType);
- mLongBoxType = elementUtil.getTypeElement(Long.class.getName()).asType();
+ mLongBoxType = mElementUtils.getTypeElement(Long.class.getName()).asType();
mLongPrimitiveType = mTypeUtils.unboxedType(mLongBoxType);
- mFloatBoxType = elementUtil.getTypeElement(Float.class.getName()).asType();
+ mFloatBoxType = mElementUtils.getTypeElement(Float.class.getName()).asType();
mFloatPrimitiveType = mTypeUtils.unboxedType(mFloatBoxType);
- mDoubleBoxType = elementUtil.getTypeElement(Double.class.getName()).asType();
+ mDoubleBoxType = mElementUtils.getTypeElement(Double.class.getName()).asType();
mDoublePrimitiveType = mTypeUtils.unboxedType(mDoubleBoxType);
- mBooleanBoxType = elementUtil.getTypeElement(Boolean.class.getName()).asType();
+ mBooleanBoxType = mElementUtils.getTypeElement(Boolean.class.getName()).asType();
mBooleanPrimitiveType = mTypeUtils.unboxedType(mBooleanBoxType);
- mByteBoxType = elementUtil.getTypeElement(Byte.class.getName()).asType();
+ mByteBoxType = mElementUtils.getTypeElement(Byte.class.getName()).asType();
mByteBoxArrayType = mTypeUtils.getArrayType(mByteBoxType);
mBytePrimitiveType = mTypeUtils.unboxedType(mByteBoxType);
mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
mGenericDocumentType =
- elementUtil.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();
+ mElementUtils.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();
}
/**
@@ -402,13 +404,62 @@
}
/**
- * Whether the method returns the specified type.
+ * A method's type and element (i.e. declaration).
+ *
+ * <p>Note: The parameter and return types may differ between the type and the element.
+ * For example,
+ *
+ * <pre>
+ * {@code
+ * public class StringSet implements Set<String> {...}
+ * }
+ * </pre>
+ *
+ * <p>Here, the type of {@code StringSet.add()} is {@code (String) -> boolean} and the element
+ * points to the generic declaration within {@code Set<T>} with a return type of
+ * {@code boolean} and a single parameter of type {@code T}.
+ */
+ public static class MethodTypeAndElement {
+ private final ExecutableType mType;
+ private final ExecutableElement mElement;
+
+ public MethodTypeAndElement(
+ @NonNull ExecutableType type, @NonNull ExecutableElement element) {
+ mType = type;
+ mElement = element;
+ }
+
+ @NonNull
+ public ExecutableType getType() {
+ return mType;
+ }
+
+ @NonNull
+ public ExecutableElement getElement() {
+ return mElement;
+ }
+ }
+
+ /**
+ * Returns a stream of all the methods (including inherited) within a {@link DeclaredType}.
+ *
+ * <p>Does not include constructors.
+ */
+ @NonNull
+ public Stream<MethodTypeAndElement> getAllMethods(@NonNull DeclaredType type) {
+ return mElementUtils.getAllMembers((TypeElement) type.asElement()).stream()
+ .filter(el -> el.getKind() == ElementKind.METHOD)
+ .map(el -> new MethodTypeAndElement(
+ (ExecutableType) mTypeUtils.asMemberOf(type, el),
+ (ExecutableElement) el));
+ }
+
+ /**
+ * Whether the method returns the specified type (or subtype).
*/
public boolean isReturnTypeMatching(
- @NonNull ExecutableElement method, @NonNull TypeMirror type) {
- TypeMirror target = method.getKind() == ElementKind.CONSTRUCTOR
- ? method.getEnclosingElement().asType() : method.getReturnType();
- return mTypeUtils.isSameType(type, target);
+ @NonNull ExecutableType method, @NonNull TypeMirror type) {
+ return mTypeUtils.isAssignable(method.getReturnType(), type);
}
/**
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 0b5e501..2c9e09d 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -2678,6 +2678,48 @@
}
@Test
+ public void testBuilderThatUsesGenerics() throws Exception {
+ Compilation compilation = compile(
+ "@Document\n"
+ + "public class Gift {\n"
+ + " @Document.Namespace private final String mNamespace;\n"
+ + " @Document.Id private final String mId;\n"
+ + " private Gift(String namespace, String id) {\n"
+ + " mNamespace = namespace;\n"
+ + " mId = id;\n"
+ + " }\n"
+ + " public String getNamespace() { return mNamespace; }\n"
+ + " public String getId() { return mId; }\n"
+ + " public static abstract class BaseBuilder<T> {\n"
+ + " public final T build() { return buildInternal(false); }\n"
+ + " // Give this a param to have zero methods with the signature\n"
+ + " // () -> DocumentClass\n"
+ + " protected abstract T buildInternal(boolean ignore);\n"
+ + " }\n"
+ + " @Document.BuilderProducer\n"
+ + " public static class Builder extends BaseBuilder<Gift> {\n"
+ + " private String mNamespace = \"\";\n"
+ + " private String mId = \"\";\n"
+ + " @Override\n"
+ + " protected Gift buildInternal(boolean ignore) {\n"
+ + " return new Gift(mNamespace, mId);\n"
+ + " }\n"
+ + " public Builder setNamespace(String namespace) {\n"
+ + " mNamespace = namespace;\n"
+ + " return this;\n"
+ + " }\n"
+ + " public Builder setId(String id) {\n"
+ + " mId = id;\n"
+ + " return this;\n"
+ + " }\n"
+ + " }\n"
+ + "}");
+ assertThat(compilation).succeededWithoutWarnings();
+ checkResultContains("Gift.java", "Gift.Builder builder = new Gift.Builder()");
+ checkResultContains("Gift.java", "return builder.build()");
+ }
+
+ @Test
public void testCreationByBuilderWithParameter() throws Exception {
Compilation compilation = compile(
"@Document\n"
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
index 93b9fb4..798062a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AndroidxTracingTraceTest.kt
@@ -65,8 +65,6 @@
val traceFilePath = linkRule.createReportedTracePath(Packages.TEST)
val perfettoCapture = PerfettoCapture()
- verifyTraceEnable(false)
-
perfettoCapture.start(
PerfettoConfig.Benchmark(
appTagPackages = listOf(Packages.TEST),
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
index 41d9868..5b04350c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureSweepTest.kt
@@ -28,11 +28,9 @@
import androidx.benchmark.perfetto.PerfettoTraceProcessor
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
-import androidx.testutils.verifyWithPolling
import androidx.tracing.Trace
import androidx.tracing.trace
import kotlin.test.assertEquals
-import kotlin.test.fail
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
@@ -94,8 +92,6 @@
val traceFilePath = linkRule.createReportedTracePath(Packages.TEST)
val perfettoCapture = PerfettoCapture(unbundled)
- verifyTraceEnable(false)
-
perfettoCapture.start(
PerfettoConfig.Benchmark(
appTagPackages = listOf(Packages.TEST),
@@ -103,14 +99,10 @@
)
)
- if (!Trace.isEnabled()) {
- // Should be available immediately, but let's wait a while to see if it works slowly.
- val delayMs = verifyTraceEnable(true)
- fail(
- "In-process tracing should be enabled immediately after trace " +
- "capture is started. Had to poll for approx $delayMs ms"
- )
- }
+ assertTrue(
+ "In-process tracing should be enabled immediately after trace capture is started",
+ Trace.isEnabled()
+ )
/**
* Trace section labels, in order
@@ -155,27 +147,3 @@
}
}
}
-
-fun verifyTraceEnable(enabled: Boolean): Long {
- // We poll here, since we may need to wait for enable flags to propagate to apps
- return verifyWithPolling(
- "Timeout waiting for Trace.isEnabled == $enabled, tags=${getTags()}",
- periodMs = 50,
- timeoutMs = 5000
- ) {
- Trace.isEnabled() == enabled
- }
-}
-
-private fun getTags(): String {
- val method = android.os.Trace::class.java.getMethod(
- "isTagEnabled",
- Long::class.javaPrimitiveType
- )
- val never = method.invoke(null, /*TRACE_TAG_NEVER*/ 0)
- val always = method.invoke(null, /*TRACE_TAG_ALWAYS*/ 1L shl 0)
- val view = method.invoke(null, /*TRACE_TAG_VIEW*/ 1L shl 3)
- val app = method.invoke(null, /*TRACE_TAG_APP*/ 1L shl 12)
-
- return "n $never, a $always, v $view, app $app"
-}
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
index 9236324..df5d8b1 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
@@ -17,11 +17,13 @@
package androidx.bluetooth
import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanResult as FwkScanResult
import android.content.Context
import android.os.Build
import android.os.ParcelUuid
+import androidx.bluetooth.utils.addressType
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
@@ -39,6 +41,7 @@
*/
@RunWith(JUnit4::class)
class ScanResultTest {
+
@Rule
@JvmField
val permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= 31) {
@@ -53,6 +56,7 @@
private val bluetoothManager: BluetoothManager? =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager?.adapter
+ private val bluetoothLe = BluetoothLe(context)
@Before
fun setUp() {
@@ -90,8 +94,13 @@
assertThat(BluetoothDevice(fwkBluetoothDevice).bondState)
.isEqualTo(scanResult.device.bondState)
assertThat(address).isEqualTo(scanResult.deviceAddress.address)
- assertThat(BluetoothAddress.ADDRESS_TYPE_UNKNOWN)
- .isEqualTo(scanResult.deviceAddress.addressType)
+ val expectedAddressType = if (Build.VERSION.SDK_INT >= 34) {
+ BluetoothAddress.ADDRESS_TYPE_PUBLIC
+ } else {
+ BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+ assertThat(scanResult.deviceAddress.addressType)
+ .isEqualTo(expectedAddressType)
assertThat(true).isEqualTo(scanResult.isConnectable())
assertThat(timeStampNanos).isEqualTo(scanResult.timestampNanos)
assertThat(scanResult.getManufacturerSpecificData(1)).isNull()
@@ -129,4 +138,100 @@
assertThat(scanResult.device).isEqualTo(scanResult.device)
assertThat(scanResult.deviceAddress).isEqualTo(scanResult.deviceAddress)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomStatic() {
+ val address = "F0:43:A8:23:10:11"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomResolvable() {
+ val address = "40:01:02:03:04:05"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomNonResolvable() {
+ val address = "00:01:02:03:04:05"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE)
+ }
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
index 7846be5..c104d69 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
@@ -44,15 +44,6 @@
/** Address type is unknown. */
const val ADDRESS_TYPE_UNKNOWN: Int = 0xFFFF
-
- /** Address type random static bits value */
- internal const val TYPE_RANDOM_STATIC_BITS_VALUE: Int = 3
-
- /** Address type random resolvable bits value */
- internal const val TYPE_RANDOM_RESOLVABLE_BITS_VALUE: Int = 1
-
- /** Address type random non resolvable bits value */
- internal const val TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE: Int = 0
}
@Target(
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
index b9af5f7..2e3cf987 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
@@ -22,6 +22,7 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.bluetooth.utils.addressType
import java.util.UUID
/**
@@ -74,12 +75,10 @@
/** Remote Bluetooth device found. */
val device: BluetoothDevice = BluetoothDevice(fwkScanResult.device)
- // TODO(kihongs) Find a way to get address type from framework scan result
/** Bluetooth address for the remote device found. */
-
val deviceAddress: BluetoothAddress = BluetoothAddress(
fwkScanResult.device.address,
- BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ fwkScanResult.device.addressType()
)
/** Device timestamp when the advertisement was last seen. */
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt
new file mode 100644
index 0000000..3e39c67
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.bluetooth.utils
+
+import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
+import android.os.Build
+import android.os.Parcel
+import androidx.annotation.RequiresApi
+import androidx.bluetooth.BluetoothAddress
+
+/** Address type random static bits value */
+private const val ADDRESS_TYPE_RANDOM_STATIC_BITS_VALUE: Int = 3
+
+/** Address type random resolvable bits value */
+private const val ADDRESS_TYPE_RANDOM_RESOLVABLE_BITS_VALUE: Int = 1
+
+/** Address type random non resolvable bits value */
+private const val ADDRESS_TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE: Int = 0
+
+// mAddressType is added to the parcel in API 34
+internal fun FwkBluetoothDevice.addressType(): @BluetoothAddress.AddressType Int {
+ return if (Build.VERSION.SDK_INT >= 34) {
+ return addressType34()
+ } else {
+ BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+}
+
+@RequiresApi(34)
+private fun FwkBluetoothDevice.addressType34(): @BluetoothAddress.AddressType Int {
+ val parcel = Parcel.obtain()
+ writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ parcel.readString() // Skip address
+ val mAddressType = parcel.readInt()
+ parcel.recycle()
+
+ return when (mAddressType) {
+ FwkBluetoothDevice.ADDRESS_TYPE_PUBLIC -> BluetoothAddress.ADDRESS_TYPE_PUBLIC
+ FwkBluetoothDevice.ADDRESS_TYPE_RANDOM ->
+ when (address.substring(0, 1).toInt(16).shr(2)) {
+ ADDRESS_TYPE_RANDOM_STATIC_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC
+
+ ADDRESS_TYPE_RANDOM_RESOLVABLE_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE
+
+ ADDRESS_TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE
+
+ else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+
+ FwkBluetoothDevice.ADDRESS_TYPE_UNKNOWN -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
index 6dac023..ed4f1dd 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
@@ -17,11 +17,7 @@
package androidx.bluetooth.utils
import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
-import android.os.Build
-import android.os.Parcel
-import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
-import androidx.bluetooth.BluetoothAddress
import java.security.MessageDigest
import java.util.UUID
import kotlin.experimental.and
@@ -32,19 +28,7 @@
packageName: String,
fwkDevice: FwkBluetoothDevice
): UUID {
- return if (Build.VERSION.SDK_INT >= 34) {
- deviceId(packageName, fwkDevice.address, fwkDevice.addressType())
- } else {
- deviceId(packageName, fwkDevice.address, BluetoothAddress.ADDRESS_TYPE_UNKNOWN)
- }
-}
-
-private fun deviceId(
- packageName: String,
- address: String,
- @BluetoothAddress.AddressType addressType: Int
-): UUID {
- val name = packageName + address + addressType
+ val name = packageName + fwkDevice.address + fwkDevice.addressType()
val md = MessageDigest.getInstance("SHA-1")
md.update(name.toByteArray())
val hash = md.digest()
@@ -68,30 +52,3 @@
return UUID(msb, lsb)
}
-
-// mAddressType is added to the parcel in API 34
-@RequiresApi(34)
-private fun FwkBluetoothDevice.addressType(): @BluetoothAddress.AddressType Int {
- val parcel = Parcel.obtain()
- writeToParcel(parcel, 0)
- parcel.setDataPosition(0)
- parcel.readString() // Skip address
- val mAddressType = parcel.readInt()
- parcel.recycle()
-
- return when (mAddressType) {
- FwkBluetoothDevice.ADDRESS_TYPE_PUBLIC -> BluetoothAddress.ADDRESS_TYPE_PUBLIC
- FwkBluetoothDevice.ADDRESS_TYPE_RANDOM ->
- when (address.substring(0, 0).toInt(16).shr(2)) {
- BluetoothAddress.TYPE_RANDOM_STATIC_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC
- BluetoothAddress.TYPE_RANDOM_RESOLVABLE_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE
- BluetoothAddress.TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE
- else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- }
- FwkBluetoothDevice.ADDRESS_TYPE_UNKNOWN -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- }
-}
diff --git a/buildSrc-tests/src/test/java/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt b/buildSrc-tests/src/test/java/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
index b1b65e6..88bc085 100644
--- a/buildSrc-tests/src/test/java/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
@@ -18,11 +18,11 @@
import java.io.File
import java.util.function.BiFunction
-import java.util.function.Predicate
import org.gradle.api.Project
import org.gradle.api.Transformer
import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.provider.Provider
+import org.gradle.api.specs.Spec
import org.gradle.testfixtures.ProjectBuilder
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
@@ -171,26 +171,24 @@
class TestProvider(private val list: List<String>) : Provider<List<String>> {
override fun get(): List<String> = list
override fun getOrNull(): List<String> = list
- override fun isPresent(): Boolean = TODO("used")
- override fun forUseAtConfigurationTime(): Provider<List<String>> = TODO("used")
+ override fun isPresent(): Boolean = TODO("unused")
+ override fun forUseAtConfigurationTime(): Provider<List<String>> = TODO("unused")
override fun <U : Any?, R : Any?> zip(
right: Provider<U>,
combiner: BiFunction<in List<String>, in U, out R?>
- ): Provider<R> = TODO("used")
+ ): Provider<R> = TODO("unused")
override fun orElse(provider: Provider<out List<String>>): Provider<List<String>> {
- TODO("used")
+ TODO("unused")
}
- override fun orElse(value: List<String>): Provider<List<String>> = TODO("used")
+ override fun orElse(value: List<String>): Provider<List<String>> = TODO("unused")
override fun <S : Any?> flatMap(
transformer: Transformer<out Provider<out S>?, in List<String>>
- ): Provider<S> = TODO("used")
- override fun filter(predicate: Predicate<in List<String>>): Provider<List<String>> {
- TODO("used")
- }
+ ): Provider<S> = TODO("unused")
+ override fun filter(spec: Spec<in List<String>>): Provider<List<String>> = TODO("unused")
override fun <S : Any?> map(
transformer: Transformer<out S?, in List<String>>
- ): Provider<S> = TODO("used")
- override fun getOrElse(defaultValue: List<String>): List<String> = TODO("used")
+ ): Provider<S> = TODO("unused")
+ override fun getOrElse(defaultValue: List<String>): List<String> = TODO("unused")
}
@Test
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 57ccca4..05fed81 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -24,13 +24,13 @@
import android.hardware.camera2.CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.params.DynamicRangeProfiles
-import android.os.Build
import android.util.Range
import android.util.Size
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.DynamicRangeProfilesCompat
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
@@ -57,6 +57,7 @@
import androidx.camera.core.ZoomState
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.DynamicRanges
import androidx.camera.core.impl.EncoderProfilesProvider
import androidx.camera.core.impl.Quirks
import androidx.camera.core.impl.Timebase
@@ -199,17 +200,16 @@
return false
}
- @SuppressLint("ClassVerificationFailure")
override fun getSupportedDynamicRanges(): Set<DynamicRange> {
- // TODO: use DynamicRangesCompat instead after it is migrates from camera-camera2.
- if (Build.VERSION.SDK_INT >= 33) {
- val availableProfiles = cameraProperties.metadata[
- CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]
- if (availableProfiles != null) {
- return profileSetToDynamicRangeSet(availableProfiles.supportedProfiles)
- }
- }
- return setOf(SDR)
+ return DynamicRangeProfilesCompat
+ .fromCameraMetaData(cameraProperties.metadata)
+ .supportedDynamicRanges
+ }
+
+ override fun querySupportedDynamicRanges(
+ candidateDynamicRanges: Set<DynamicRange>
+ ): Set<DynamicRange> {
+ return DynamicRanges.findAllPossibleMatches(candidateDynamicRanges, supportedDynamicRanges)
}
override fun isPreviewStabilizationSupported(): Boolean {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index e845092..d352ea7 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -24,6 +24,8 @@
import android.util.Range
import android.util.Size
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
@@ -31,6 +33,12 @@
import androidx.camera.camera2.pipe.integration.testing.FakeZoomCompat
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.CameraInfo
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.DOLBY_VISION_10_BIT
+import androidx.camera.core.DynamicRange.DOLBY_VISION_8_BIT
+import androidx.camera.core.DynamicRange.HDR10_10_BIT
+import androidx.camera.core.DynamicRange.HDR10_PLUS_10_BIT
+import androidx.camera.core.DynamicRange.HLG_10_BIT
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.ZoomState
@@ -256,4 +264,131 @@
assertThat(cameraInfo.isVideoStabilizationSupported).isFalse()
}
+
+ // Analog to Camera2CameraInfoImplTest#apiVersionMet_canReturnSupportedHdrDynamicRanges()
+ @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun cameraInfo_hdrDynamicRangeSupported() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter(
+ cameraProperties = FakeCameraProperties(
+ FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to
+ HLG10_UNCONSTRAINED
+ )
+ )
+ )
+ )
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(
+ HLG_10_BIT, HDR10_10_BIT, HDR10_PLUS_10_BIT, DOLBY_VISION_10_BIT, DOLBY_VISION_8_BIT
+ )
+ )).containsExactly(HLG_10_BIT)
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(DynamicRange.HDR_UNSPECIFIED_10_BIT)
+ )).containsExactly(HLG_10_BIT)
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun cameraInfo_tenBitHdrDynamicRangeSupported_whenAlsoQuerying8Bit() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter(
+ cameraProperties = FakeCameraProperties(
+ FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to
+ DOLBY_VISION_10B_UNCONSTRAINED
+ )
+ )
+ )
+ )
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(DOLBY_VISION_10_BIT, DOLBY_VISION_8_BIT)
+ )).containsExactly(DOLBY_VISION_10_BIT)
+ }
+
+ // Analog to Camera2CameraInfoImplTest#apiVersionMet_canReturnSupportedDynamicRanges()
+ @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun cameraInfo_returnsAllSupportedDynamicRanges_whenQueryingWithUnspecified() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter(
+ cameraProperties = FakeCameraProperties(
+ FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to
+ HLG10_UNCONSTRAINED
+ )
+ )
+ )
+ )
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(DynamicRange.UNSPECIFIED)
+ )).containsExactly(DynamicRange.SDR, HLG_10_BIT)
+ }
+
+ // Analog to
+ // Camera2CameraInfoImplTest#apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
+ @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+ @Test
+ fun cameraInfo_hdrAndSdrDynamicRangesSupported_whenQueryingWithFullySpecified() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter(
+ cameraProperties = FakeCameraProperties(
+ FakeCameraMetadata(
+ characteristics = mapOf(
+ CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to
+ HLG10_UNCONSTRAINED
+ )
+ )
+ )
+ )
+
+ assertThat(
+ cameraInfo.querySupportedDynamicRanges(
+ setOf(
+ DynamicRange.SDR,
+ HLG_10_BIT
+ )
+ )
+ ).containsExactly(DynamicRange.SDR, HLG_10_BIT)
+ }
+
+ // Analog to Camera2CameraInfoImplTest#apiVersionNotMet_canReturnSupportedDynamicRanges()
+ @Test
+ fun cameraInfo_queryUnspecifiedDynamicRangeSupported() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(DynamicRange.UNSPECIFIED))).containsExactly(DynamicRange.SDR)
+ }
+
+ // Analog to Camera2CameraInfoImplTest#apiVersionNotMet_queryHdrDynamicRangeNotSupported()
+ @Test
+ fun cameraInfo_queryForHdrWhenUnsupported_returnsEmptySet() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(
+ setOf(DynamicRange.HDR_UNSPECIFIED_10_BIT))).isEmpty()
+ }
+
+ // Analog to Camera2CameraInfoImplTest#querySdrDynamicRange_alwaysSupported()
+ @Test
+ fun cameraInfo_querySdrSupported() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(setOf(DynamicRange.SDR))).containsExactly(
+ DynamicRange.SDR
+ )
+ }
+
+ // Analog to Camera2CameraInfoImplTest#queryDynamicRangeWithEmptySet_returnsEmptySet()
+ @Test
+ fun cameraInfo_queryWithEmptySet_returnsEmptySet() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(emptySet())).isEmpty()
+ }
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
index bcaae5a..9f2b153 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
@@ -27,7 +27,7 @@
import androidx.annotation.RequiresApi
val HLG10_UNCONSTRAINED by lazy {
- DynamicRangeProfiles(longArrayOf(HLG10, 0, 0))
+ DynamicRangeProfiles(longArrayOf(HLG10, CONSTRAINTS_NONE, LATENCY_NONE))
}
val HLG10_CONSTRAINED by lazy {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index ffffd60..5dd2cf2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -59,6 +59,7 @@
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.DynamicRanges;
import androidx.camera.core.impl.EncoderProfilesProvider;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.Quirks;
@@ -451,6 +452,14 @@
return dynamicRangesCompat.getSupportedDynamicRanges();
}
+ @NonNull
+ @Override
+ public Set<DynamicRange> querySupportedDynamicRanges(
+ @NonNull Set<DynamicRange> candidateDynamicRanges) {
+ return DynamicRanges.findAllPossibleMatches(candidateDynamicRanges,
+ getSupportedDynamicRanges());
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 8a876ca..96f94bd 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -20,9 +20,18 @@
import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF;
import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON;
import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION;
+
+import static androidx.camera.core.DynamicRange.DOLBY_VISION_10_BIT;
+import static androidx.camera.core.DynamicRange.DOLBY_VISION_8_BIT;
+import static androidx.camera.core.DynamicRange.HDR10_10_BIT;
+import static androidx.camera.core.DynamicRange.HDR10_PLUS_10_BIT;
+import static androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT;
import static androidx.camera.core.DynamicRange.HLG_10_BIT;
import static androidx.camera.core.DynamicRange.SDR;
+import static androidx.camera.core.DynamicRange.UNSPECIFIED;
+
import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -77,6 +86,7 @@
import org.robolectric.util.ReflectionHelpers;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -728,13 +738,44 @@
@Config(minSdk = 33)
@Test
- public void apiVersionMet_canReturnSupportedDynamicRanges() throws CameraAccessExceptionCompat {
+ public void apiVersionMet_canReturnOnlySupportedHdrDynamicRanges()
+ throws CameraAccessExceptionCompat {
init(/* hasAvailableCapabilities = */ true);
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
- CAMERA0_ID, mCameraManagerCompat);
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
- Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ new HashSet<>(Arrays.asList(HLG_10_BIT, HDR10_10_BIT, HDR10_PLUS_10_BIT,
+ DOLBY_VISION_10_BIT, DOLBY_VISION_8_BIT)));
+ assertThat(supportedDynamicRanges).containsExactly(HLG_10_BIT);
+ supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ Collections.singleton(HDR_UNSPECIFIED_10_BIT));
+ assertThat(supportedDynamicRanges).containsExactly(HLG_10_BIT);
+ }
+
+ @Config(minSdk = 33)
+ @Test
+ public void apiVersionMet_canReturnSupportedDynamicRanges()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ Collections.singleton(UNSPECIFIED));
+ assertThat(supportedDynamicRanges).containsExactly(SDR, HLG_10_BIT);
+ }
+
+ @Config(minSdk = 33)
+ @Test
+ public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ new HashSet<>(Arrays.asList(SDR, HLG_10_BIT)));
assertThat(supportedDynamicRanges).containsExactly(SDR, HLG_10_BIT);
}
@@ -744,13 +785,44 @@
throws CameraAccessExceptionCompat {
init(/* hasAvailableCapabilities = */ true);
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
- CAMERA0_ID, mCameraManagerCompat);
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
- Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ Collections.singleton(UNSPECIFIED));
assertThat(supportedDynamicRanges).containsExactly(SDR);
}
+ @Config(maxSdk = 32)
+ @Test
+ public void apiVersionNotMet_queryHdrDynamicRangeNotSupported()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ Set<DynamicRange> supportedDynamicRanges = cameraInfo.querySupportedDynamicRanges(
+ Collections.singleton(HDR_UNSPECIFIED_10_BIT));
+ assertThat(supportedDynamicRanges).isEmpty();
+ }
+
+ @Test
+ public void querySdrDynamicRange_alwaysSupported() throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(Collections.singleton(SDR))).isNotEmpty();
+ }
+
+ @Test
+ public void queryDynamicRangeWithEmptySet_returnsEmptySet() throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.querySupportedDynamicRanges(Collections.emptySet())).isEmpty();
+ }
+
private CameraManagerCompat initCameraManagerWithPhysicalIds(
List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 57fd948..e712179 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -27,6 +27,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.StringDef;
+import androidx.camera.core.impl.DynamicRanges;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.internal.compat.MediaActionSoundCompat;
import androidx.lifecycle.LifecycleOwner;
@@ -340,6 +341,71 @@
return false;
}
+ /**
+ * Returns the supported dynamic ranges of this camera from a set of candidate dynamic ranges.
+ *
+ * <p>Dynamic range specifies how the range of colors, highlights and shadows captured by
+ * the frame producer are represented on a display. Some dynamic ranges allow the preview
+ * surface to make full use of the extended range of brightness of the display.
+ *
+ * <p>The returned dynamic ranges are those which the camera can produce. However, because
+ * care usually needs to be taken to ensure the frames produced can be displayed correctly,
+ * the returned dynamic ranges will be limited to those passed in to {@code
+ * candidateDynamicRanges}. For example, if the device display supports HLG, HDR10 and
+ * HDR10+, and you're attempting to use a UI component to receive frames from those dynamic
+ * ranges that you know will be display correctly, you would use a {@code
+ * candidateDynamicRanges} set consisting of {@code {DynamicRange.HLG_10_BIT,
+ * DynamicRange.HDR10_10_BIT, DynamicRange.HDR10_PLUS_10_BIT}}. If the only 10-bit/HDR {@code
+ * DynamicRange} the camera can produce is {@code HLG_10_BIT}, then that will be the only
+ * dynamic range returned by this method given the above candidate list.
+ *
+ * <p>Consult the documentation of each use case to determine whether using the dynamic ranges
+ * published here are appropriate. Some use cases may have complex requirements that prohibit
+ * them from publishing a candidate list for use with this method, such as
+ * {@link androidx.camera.video.Recorder Recorder}. For those cases, alternative APIs may be
+ * present for querying the supported dynamic ranges that can be set on the use case.
+ *
+ * <p>The dynamic ranges published as return values by this method are fully-defined. That is,
+ * the resulting set will not contain dynamic ranges such as {@link DynamicRange#UNSPECIFIED} or
+ * {@link DynamicRange#HDR_UNSPECIFIED_10_BIT}. However, non-fully-defined dynamic ranges can
+ * be used in {@code candidateDynamicRanges}, and will resolve to fully-defined dynamic ranges
+ * in the resulting set. To query all dynamic ranges the camera can produce, {@code
+ * Collections.singleton(DynamicRange.UNSPECIFIED}} can be used as the candidate set.
+ *
+ * <p>Because SDR is always supported, including {@link DynamicRange#SDR} in {@code
+ * candidateDynamicRanges} will always result in {@code SDR} being present in the result set.
+ * If an empty candidate set is provided, it is treated as a no-op, and an empty set will be
+ * returned.
+ *
+ * @param candidateDynamicRanges a set of dynamic ranges representing the dynamic ranges the
+ * consumer of frames can support. Note that each use case may
+ * have its own requirements on which dynamic ranges it can
+ * consume based on how it is configured, and those dynamic
+ * ranges may not be published as a set of candidate dynamic
+ * ranges. In that case, this API may not be appropriate. An
+ * example of this is
+ * {@link androidx.camera.video.VideoCapture VideoCapture}'s
+ * {@link androidx.camera.video.Recorder Recorder} class, which
+ * must also take into account the dynamic ranges supported by
+ * the media codecs on the device, and the quality of the video
+ * being recorded. For that class, it is recommended to use
+ * {@link androidx.camera.video.RecorderVideoCapabilities#getSupportedDynamicRanges()
+ * RecorderVideoCapabilities.getSupportedDynamicRanges()}
+ * instead.
+ * @return a set of dynamic ranges supported by the camera based on the candidate dynamic ranges
+ *
+ * @see Preview.Builder#setDynamicRange(DynamicRange)
+ * @see androidx.camera.video.RecorderVideoCapabilities#getSupportedDynamicRanges()
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ default Set<DynamicRange> querySupportedDynamicRanges(
+ @NonNull Set<DynamicRange> candidateDynamicRanges) {
+ // For the default implementation, only assume SDR is supported.
+ return DynamicRanges.findAllPossibleMatches(candidateDynamicRanges,
+ Collections.singleton(DynamicRange.SDR));
+ }
+
@StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
IMPLEMENTATION_TYPE_CAMERA2_LEGACY, IMPLEMENTATION_TYPE_CAMERA2,
IMPLEMENTATION_TYPE_FAKE})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 8bc0b73..d804dab 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
@@ -19,6 +19,7 @@
import static androidx.camera.core.CameraEffect.PREVIEW;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
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;
@@ -62,7 +63,6 @@
import android.view.SurfaceView;
import android.view.TextureView;
-import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -77,6 +77,7 @@
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageInputConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
@@ -288,20 +289,6 @@
}
}
- @RestrictTo(Scope.LIBRARY_GROUP)
- @Override
- @IntRange(from = 0, to = 359)
- protected int getRelativeRotation(@NonNull CameraInternal cameraInternal,
- boolean requireMirroring) {
- if (cameraInternal.getHasTransform()) {
- return super.getRelativeRotation(cameraInternal, requireMirroring);
- } else {
- // If there is a virtual parent camera, the buffer is already rotated because
- // SurfaceView cannot handle additional rotation.
- return 0;
- }
- }
-
private boolean shouldMirror(@NonNull CameraInternal camera) {
// 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,
@@ -673,6 +660,37 @@
}
/**
+ * Returns the dynamic range.
+ *
+ * <p>The dynamic range is set by {@link Preview.Builder#setDynamicRange(DynamicRange)}.
+ * If the dynamic range set is not a fully defined dynamic range, such as
+ * {@link DynamicRange#HDR_UNSPECIFIED_10_BIT}, then it will be returned just as provided,
+ * and will not be returned as a fully defined dynamic range. The fully defined dynamic
+ * range, which is determined by resolving the combination of requested dynamic ranges from
+ * other use cases according to the device capabilities, will be
+ * communicated to the {@link Preview.SurfaceProvider} via
+ * {@link SurfaceRequest#getDynamicRange()}}.
+ *
+ * <p>If the dynamic range was not provided to
+ * {@link Preview.Builder#setDynamicRange(DynamicRange)}, this will return the default of
+ * {@link DynamicRange#UNSPECIFIED}
+ *
+ * @return the dynamic range set for this {@code Preview} use case.
+ *
+ * @see Preview.Builder#setDynamicRange(DynamicRange)
+ */
+ // Internal implementation note: this method should not be used to retrieve the dynamic range
+ // that will be sent to the SurfaceProvider. That should always be retrieved from the StreamSpec
+ // since that will be the final DynamicRange chosen by the camera based on other use case
+ // combinations.
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicRange getDynamicRange() {
+ return getCurrentConfig().hasDynamicRange() ? getCurrentConfig().getDynamicRange() :
+ Defaults.DEFAULT_DYNAMIC_RANGE;
+ }
+
+ /**
* Returns {@link PreviewCapabilities} to query preview stream related device capability.
*
* @return {@link PreviewCapabilities}
@@ -776,11 +794,19 @@
private static final PreviewConfig DEFAULT_CONFIG;
+ /**
+ * Preview uses an UNSPECIFIED dynamic range by default. This means the dynamic range can be
+ * inherited from other use cases during dynamic range resolution when the use case is
+ * bound.
+ */
+ private static final DynamicRange DEFAULT_DYNAMIC_RANGE = DynamicRange.UNSPECIFIED;
+
static {
Builder builder = new Builder()
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
.setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
- .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
+ .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR)
+ .setDynamicRange(DEFAULT_DYNAMIC_RANGE);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -796,6 +822,7 @@
public static final class Builder
implements UseCaseConfig.Builder<Preview, PreviewConfig, Builder>,
ImageOutputConfig.Builder<Builder>,
+ ImageInputConfig.Builder<Builder>,
ThreadConfig.Builder<Builder> {
private final MutableOptionsBundle mMutableConfig;
@@ -1125,6 +1152,88 @@
return this;
}
+ // Implementations of ImageInputConfig.Builder default methods
+
+ /**
+ * Sets the {@link DynamicRange}.
+ *
+ * <p>Dynamic range specifies how the range of colors, highlights and shadows captured by
+ * the frame producer are represented on a display. Some dynamic ranges allow the preview
+ * surface to make full use of the extended range of brightness of the display.
+ *
+ * <p>The supported dynamic ranges for preview depend on the capabilities of the
+ * camera and the ability of the {@link Surface} provided by the
+ * {@link Preview.SurfaceProvider} to consume the dynamic range. The supported dynamic
+ * ranges of the camera can be queried using
+ * {@link CameraInfo#querySupportedDynamicRanges(Set)}.
+ *
+ * <p>As an example, if the {@link Surface} provided by {@link Preview.SurfaceProvider}
+ * comes from a {@link SurfaceView}, such as with
+ * {@link androidx.camera.viewfinder.CameraViewfinder CameraViewfinder} set to
+ * implementation mode
+ * {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#PERFORMANCE
+ * PERFORMANCE}, you may want to query the dynamic ranges supported by the display:
+ * <pre>
+ * <code>
+ *
+ * // Get supported HDR dynamic ranges from the display
+ * Display display = requireContext().getDisplay();
+ * List<Integer> displayHdrTypes =
+ * display.getHdrCapabilities().getSupportedHdrTypes();
+ * Set<DynamicRange> displayHighDynamicRanges =
+ * // Simple map of Display.HdrCapabilities enums to CameraX DynamicRange
+ * convertToDynamicRangeSet(displayHdrTypes);
+ *
+ * // Query dynamic ranges supported by the camera from our
+ * // dynamic ranges supported by the display.
+ * mSupportedHighDynamicRanges =
+ * mCameraInfo.querySupportedDynamicRanges(
+ * displayHighDynamicRanges);
+ *
+ * // Update our UI picker for dynamic range.
+ * ...
+ *
+ *
+ * // Create the Preview use case from the dynamic range
+ * // selected by the UI picker.
+ * mPreview = new Preview.Builder()
+ * .setDynamicRange(mSelectedDynamicRange)
+ * .build();
+ * </code>
+ * </pre>
+ *
+ * <p>If the dynamic range is not provided, the returned {@code Preview} use case will use
+ * a default of {@link DynamicRange#UNSPECIFIED}. When a {@code Preview} is bound with
+ * other use cases that specify a dynamic range, such as
+ * {@link androidx.camera.video.VideoCapture}, and the preview dynamic range is {@code
+ * UNSPECIFIED}, the resulting dynamic range of the preview will usually match the other
+ * use case's dynamic range. If no other use cases are bound with the preview, an
+ * {@code UNSPECIFIED} dynamic range will resolve to {@link DynamicRange#SDR}. When
+ * using a {@code Preview} with another use case, it is recommended to leave the dynamic
+ * range of the {@code Preview} as {@link DynamicRange#UNSPECIFIED}, so the camera can
+ * choose the best supported dynamic range that satisfies the requirements of both use
+ * cases.
+ *
+ * <p>If an unspecified dynamic range is used, the resolved fully-defined dynamic range of
+ * frames sent from the camera will be communicated to the
+ * {@link Preview.SurfaceProvider} via {@link SurfaceRequest#getDynamicRange()}, and the
+ * provided {@link Surface} should be configured to use that dynamic range.
+ *
+ * <p>It is possible to choose a high dynamic range (HDR) with unspecified encoding by
+ * providing {@link DynamicRange#HDR_UNSPECIFIED_10_BIT}.
+ *
+ * @return The current Builder.
+ * @see DynamicRange
+ * @see CameraInfo#querySupportedDynamicRanges(Set)
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder setDynamicRange(@NonNull DynamicRange dynamicRange) {
+ getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+ return this;
+ }
+
// Implementations of ThreadConfig.Builder default methods
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/DynamicRanges.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/DynamicRanges.kt
new file mode 100644
index 0000000..3137051
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/DynamicRanges.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.impl
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.DynamicRange
+import androidx.core.util.Preconditions
+
+/**
+ * Utility methods for handling dynamic range.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+object DynamicRanges {
+
+ /**
+ * Returns `true` if the test dynamic range can resolve to the fully specified dynamic
+ * range set.
+ *
+ * A range can resolve if test fields are unspecified and appropriately match the fields
+ * of the fully specified dynamic range, or the test fields exactly match the fields of
+ * the fully specified dynamic range.
+ */
+ @JvmStatic
+ fun canResolve(
+ dynamicRangeToTest: DynamicRange,
+ fullySpecifiedDynamicRanges: Set<DynamicRange>,
+
+ ): Boolean {
+ return if (dynamicRangeToTest.isFullySpecified) {
+ fullySpecifiedDynamicRanges.contains(dynamicRangeToTest)
+ } else {
+ fullySpecifiedDynamicRanges.firstOrNull { fullySpecifiedDynamicRange ->
+ canResolveUnderSpecifiedTo(
+ dynamicRangeToTest,
+ fullySpecifiedDynamicRange
+ )
+ } != null
+ }
+ }
+ /**
+ * Returns a set of all possible matches from a set of dynamic ranges that may contain
+ * under-specified dynamic ranges to a set that contains only fully-specified dynamic ranges.
+ *
+ * A dynamic range can resolve if test fields are unspecified and appropriately match the fields
+ * of the fully specified dynamic range, or the test fields exactly match the fields of
+ * the fully specified dynamic range.
+ */
+ @JvmStatic
+ fun findAllPossibleMatches(
+ dynamicRangesToTest: Set<DynamicRange>,
+ fullySpecifiedDynamicRanges: Set<DynamicRange>
+ ): Set<DynamicRange> {
+ return buildSet {
+ dynamicRangesToTest.forEach {
+ if (it.isFullySpecified) {
+ // Add matching fully-specified dynamic ranges directly
+ if (fullySpecifiedDynamicRanges.contains(it)) {
+ add(it)
+ }
+ } else {
+ // Iterate through fully-specified dynamic ranges to find which could be used
+ // by the corresponding under-specified dynamic ranges
+ fullySpecifiedDynamicRanges.forEach { fullySpecifiedDynamicRange ->
+ if (canResolveUnderSpecifiedTo(it, fullySpecifiedDynamicRange)) {
+ add(fullySpecifiedDynamicRange)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun canResolveUnderSpecifiedTo(
+ underSpecifiedDynamicRange: DynamicRange,
+ fullySpecifiedDynamicRange: DynamicRange
+ ): Boolean {
+ return canMatchBitDepth(underSpecifiedDynamicRange, fullySpecifiedDynamicRange) &&
+ canMatchEncoding(underSpecifiedDynamicRange, fullySpecifiedDynamicRange)
+ }
+
+ private fun canMatchBitDepth(
+ dynamicRangeToTest: DynamicRange,
+ fullySpecifiedDynamicRange: DynamicRange
+ ): Boolean {
+ Preconditions.checkState(
+ fullySpecifiedDynamicRange.isFullySpecified, "Fully specified " +
+ "range is not actually fully specified."
+ )
+ return if (dynamicRangeToTest.bitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED) {
+ true
+ } else {
+ dynamicRangeToTest.bitDepth == fullySpecifiedDynamicRange.bitDepth
+ }
+ }
+
+ private fun canMatchEncoding(
+ dynamicRangeToTest: DynamicRange,
+ fullySpecifiedDynamicRange: DynamicRange
+ ): Boolean {
+ Preconditions.checkState(
+ fullySpecifiedDynamicRange.isFullySpecified, "Fully specified " +
+ "range is not actually fully specified."
+ )
+ val encodingToTest = dynamicRangeToTest.encoding
+ if (encodingToTest == DynamicRange.ENCODING_UNSPECIFIED) {
+ return true
+ }
+ val fullySpecifiedEncoding = fullySpecifiedDynamicRange.encoding
+ return if (encodingToTest == DynamicRange.ENCODING_HDR_UNSPECIFIED &&
+ fullySpecifiedEncoding != DynamicRange.ENCODING_SDR) {
+ true
+ } else {
+ encodingToTest == fullySpecifiedEncoding
+ }
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 5ce2595..fc397ff3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -183,6 +183,13 @@
@NonNull
@Override
+ public Set<DynamicRange> querySupportedDynamicRanges(
+ @NonNull Set<DynamicRange> candidateDynamicRanges) {
+ return mCameraInfoInternal.querySupportedDynamicRanges(candidateDynamicRanges);
+ }
+
+ @NonNull
+ @Override
public CameraInfoInternal getImplementation() {
return mCameraInfoInternal.getImplementation();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
index 94583c4..5477b9c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
@@ -26,7 +26,6 @@
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
-import android.opengl.Matrix;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
@@ -314,17 +313,13 @@
private Bitmap getBitmap(@NonNull Size size,
@NonNull float[] textureTransform,
int rotationDegrees) {
- float[] snapshotTransform = new float[16];
- Matrix.setIdentityM(snapshotTransform, 0);
-
- // Flip the snapshot. This is for reverting the GL transform added in SurfaceOutputImpl.
- MatrixExt.preVerticalFlip(snapshotTransform, 0.5f);
+ float[] snapshotTransform = textureTransform.clone();
// Rotate the output if requested.
MatrixExt.preRotate(snapshotTransform, rotationDegrees, 0.5f, 0.5f);
- // Apply the texture transform.
- Matrix.multiplyMM(snapshotTransform, 0, snapshotTransform, 0, textureTransform, 0);
+ // Flip the snapshot. This is for reverting the GL transform added in SurfaceOutputImpl.
+ MatrixExt.preVerticalFlip(snapshotTransform, 0.5f);
// Update the size based on the rotation degrees.
size = rotateSize(size, rotationDegrees);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index f614de1..0f2c246 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -80,7 +80,7 @@
private final StreamSharingConfig mDefaultConfig;
@NonNull
- private final VirtualCamera mVirtualCamera;
+ private final VirtualCameraAdapter mVirtualCameraAdapter;
// Node that applies effect to the input.
@Nullable
private SurfaceProcessorNode mEffectNode;
@@ -114,7 +114,6 @@
return new StreamSharingConfig(OptionsBundle.from(mutableConfig));
}
-
/**
* Constructs a {@link StreamSharing} with a parent {@link CameraInternal}, children
* {@link UseCase}s, and a {@link UseCaseConfigFactory} for getting default {@link UseCase}
@@ -125,17 +124,17 @@
@NonNull UseCaseConfigFactory useCaseConfigFactory) {
super(getDefaultConfig(children));
mDefaultConfig = getDefaultConfig(children);
- mVirtualCamera = new VirtualCamera(parentCamera, children, useCaseConfigFactory,
- (jpegQuality, rotationDegrees) -> {
- SurfaceProcessorNode sharingNode = mSharingNode;
- if (sharingNode != null) {
- return sharingNode.getSurfaceProcessor().snapshot(
- jpegQuality, rotationDegrees);
- } else {
- return Futures.immediateFailedFuture(new Exception(
- "Failed to take picture: pipeline is not ready."));
- }
- });
+ mVirtualCameraAdapter = new VirtualCameraAdapter(
+ parentCamera, children, useCaseConfigFactory, (jpegQuality, rotationDegrees) -> {
+ SurfaceProcessorNode sharingNode = mSharingNode;
+ if (sharingNode != null) {
+ return sharingNode.getSurfaceProcessor().snapshot(
+ jpegQuality, rotationDegrees);
+ } else {
+ return Futures.immediateFailedFuture(new Exception(
+ "Failed to take picture: pipeline is not ready."));
+ }
+ });
}
@Nullable
@@ -164,7 +163,7 @@
@Override
protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
- mVirtualCamera.mergeChildrenConfigs(builder.getMutableConfig());
+ mVirtualCameraAdapter.mergeChildrenConfigs(builder.getMutableConfig());
return builder.getUseCaseConfig();
}
@@ -192,31 +191,31 @@
@Override
public void onBind() {
super.onBind();
- mVirtualCamera.bindChildren();
+ mVirtualCameraAdapter.bindChildren();
}
@Override
public void onUnbind() {
super.onUnbind();
clearPipeline();
- mVirtualCamera.unbindChildren();
+ mVirtualCameraAdapter.unbindChildren();
}
@Override
public void onStateAttached() {
super.onStateAttached();
- mVirtualCamera.notifyStateAttached();
+ mVirtualCameraAdapter.notifyStateAttached();
}
@Override
public void onStateDetached() {
super.onStateDetached();
- mVirtualCamera.notifyStateDetached();
+ mVirtualCameraAdapter.notifyStateDetached();
}
@NonNull
public Set<UseCase> getChildren() {
- return mVirtualCamera.getChildren();
+ return mVirtualCameraAdapter.getChildren();
}
/**
@@ -257,7 +256,8 @@
// Transform the input based on virtual camera configuration.
Map<UseCase, SurfaceProcessorNode.OutConfig> outConfigMap =
- mVirtualCamera.getChildrenOutConfigs(mSharingInputEdge);
+ mVirtualCameraAdapter.getChildrenOutConfigs(mSharingInputEdge,
+ getTargetRotationInternal());
SurfaceProcessorNode.Out out = mSharingNode.transform(
SurfaceProcessorNode.In.of(mSharingInputEdge,
new ArrayList<>(outConfigMap.values())));
@@ -267,7 +267,7 @@
for (Map.Entry<UseCase, SurfaceProcessorNode.OutConfig> entry : outConfigMap.entrySet()) {
outputEdges.put(entry.getKey(), out.get(entry.getValue()));
}
- mVirtualCamera.setChildrenEdges(outputEdges);
+ mVirtualCameraAdapter.setChildrenEdges(outputEdges);
// Send the camera edge Surface to the camera2.
SessionConfig.Builder builder = SessionConfig.Builder.createFrom(config,
@@ -276,7 +276,8 @@
propagateChildrenCamera2Interop(streamSpec.getResolution(), builder);
builder.addSurface(mCameraEdge.getDeferrableSurface());
- builder.addRepeatingCameraCaptureCallback(mVirtualCamera.getParentMetadataCallback());
+ builder.addRepeatingCameraCaptureCallback(
+ mVirtualCameraAdapter.getParentMetadataCallback());
if (streamSpec.getImplementationOptions() != null) {
builder.addImplementationOptions(streamSpec.getImplementationOptions());
}
@@ -349,7 +350,7 @@
// children UseCase does not have additional logic in SessionConfig error listener
// so this is OK. If they do, we need to invoke the children's SessionConfig
// error listeners instead.
- mVirtualCamera.resetChildren();
+ mVirtualCameraAdapter.resetChildren();
}
});
}
@@ -409,8 +410,8 @@
@VisibleForTesting
@NonNull
- VirtualCamera getVirtualCamera() {
- return mVirtualCamera;
+ VirtualCameraAdapter getVirtualCameraAdapter() {
+ return mVirtualCameraAdapter;
}
/**
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 2d939b7..4d3ad88 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
@@ -15,63 +15,22 @@
*/
package androidx.camera.core.streamsharing;
-import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
-import static androidx.camera.core.CameraEffect.PREVIEW;
-import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
-import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
-import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
-import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
-import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
-import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
-import static androidx.camera.core.streamsharing.DynamicRangeUtils.resolveDynamicRange;
-import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
-import static androidx.core.util.Preconditions.checkState;
-import static java.util.Objects.requireNonNull;
-
-import android.graphics.ImageFormat;
import android.os.Build;
-import android.util.Size;
-import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.CameraEffect;
-import androidx.camera.core.DynamicRange;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.Observable;
-import androidx.camera.core.impl.SessionConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.UseCaseConfigFactory;
-import androidx.camera.core.impl.stabilization.StabilizationMode;
-import androidx.camera.core.processing.SurfaceEdge;
-import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig;
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
/**
* A virtual implementation of {@link CameraInternal}.
@@ -82,249 +41,65 @@
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class VirtualCamera implements CameraInternal {
private static final String UNSUPPORTED_MESSAGE = "Operation not supported by VirtualCamera.";
- // Children UseCases associated with this virtual camera.
- @NonNull
- final Set<UseCase> mChildren;
- // Specs for children UseCase, calculated and set by StreamSharing.
- @NonNull
- final Map<UseCase, SurfaceEdge> mChildrenEdges = new HashMap<>();
- // Whether a children is in the active state. See: UseCase.State.ACTIVE
- @NonNull
- final Map<UseCase, Boolean> mChildrenActiveState = new HashMap<>();
- // Config factory for getting children's config.
- @NonNull
- private final UseCaseConfigFactory mUseCaseConfigFactory;
// The parent camera instance.
@NonNull
private final CameraInternal mParentCamera;
- // The callback that receives the parent camera's metadata.
- @NonNull
- private final CameraCaptureCallback mParentMetadataCallback = createCameraCaptureCallback();
@NonNull
private final VirtualCameraControl mVirtualCameraControl;
@NonNull
private final VirtualCameraInfo mVirtualCameraInfo;
+ private final UseCase.StateChangeCallback mStateChangeCallback;
+
/**
- * @param parentCamera the parent {@link CameraInternal} instance. For example, the
- * real camera.
- * @param children the children {@link UseCase}.
- * @param useCaseConfigFactory the factory for configuring children {@link UseCase}.
+ * @param parentCamera the parent {@link CameraInternal} instance. For example, the
+ * real camera.
*/
VirtualCamera(@NonNull CameraInternal parentCamera,
- @NonNull Set<UseCase> children,
- @NonNull UseCaseConfigFactory useCaseConfigFactory,
+ @NonNull UseCase.StateChangeCallback useCaseStateCallback,
@NonNull StreamSharing.Control streamSharingControl) {
mParentCamera = parentCamera;
- mUseCaseConfigFactory = useCaseConfigFactory;
- mChildren = children;
+ mStateChangeCallback = useCaseStateCallback;
mVirtualCameraControl = new VirtualCameraControl(parentCamera.getCameraControlInternal(),
streamSharingControl);
mVirtualCameraInfo = new VirtualCameraInfo(parentCamera.getCameraInfoInternal());
- // Set children state to inactive by default.
- for (UseCase child : children) {
- mChildrenActiveState.put(child, false);
- }
- }
-
- // --- API for StreamSharing ---
- void mergeChildrenConfigs(@NonNull MutableConfig mutableConfig) {
- Set<UseCaseConfig<?>> childrenConfigs = new HashSet<>();
- for (UseCase useCase : mChildren) {
- childrenConfigs.add(useCase.mergeConfigs(mParentCamera.getCameraInfoInternal(),
- null,
- useCase.getDefaultConfig(true, mUseCaseConfigFactory)));
- }
-
- // Merge resolution configs.
- List<Size> cameraSupportedResolutions =
- new ArrayList<>(mParentCamera.getCameraInfoInternal().getSupportedResolutions(
- INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
- Size sensorSize = rectToSize(mParentCamera.getCameraControlInternal().getSensorRect());
- List<Size> mergedResolutions = getMergedResolutions(cameraSupportedResolutions, sensorSize,
- mutableConfig, childrenConfigs);
- mutableConfig.insertOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS, mergedResolutions);
-
- // Merge Surface occupancy priority.
- mutableConfig.insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY,
- getHighestSurfacePriority(childrenConfigs));
-
- // Merge dynamic range configs. Try to find a dynamic range that can match all child
- // requirements, or throw an exception if no matching dynamic range.
- // TODO: This approach works for the current code base, where only VideoCapture can be
- // configured (Preview follows the settings, ImageCapture is fixed as SDR). When
- // dynamic range APIs opened on other use cases, we might want a more advanced approach
- // that allows conflicts, e.g. converting HDR stream to SDR stream.
- DynamicRange dynamicRange = resolveDynamicRange(childrenConfigs);
- if (dynamicRange == null) {
- throw new IllegalArgumentException("Failed to merge child dynamic ranges, can not find"
- + " a dynamic range that satisfies all children.");
- }
- mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
-
- // Merge Preview stabilization and video stabilization configs.
- for (UseCase useCase : mChildren) {
- if (useCase.getCurrentConfig().getVideoStabilizationMode()
- != StabilizationMode.UNSPECIFIED) {
- mutableConfig.insertOption(OPTION_VIDEO_STABILIZATION_MODE,
- useCase.getCurrentConfig().getVideoStabilizationMode());
- }
-
- if (useCase.getCurrentConfig().getPreviewStabilizationMode()
- != StabilizationMode.UNSPECIFIED) {
- mutableConfig.insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
- useCase.getCurrentConfig().getPreviewStabilizationMode());
- }
- }
- }
-
- void bindChildren() {
- for (UseCase useCase : mChildren) {
- useCase.bindToCamera(this, null,
- useCase.getDefaultConfig(true, mUseCaseConfigFactory));
- }
- }
-
- void unbindChildren() {
- for (UseCase useCase : mChildren) {
- useCase.unbindFromCamera(this);
- }
- }
-
- void notifyStateAttached() {
- for (UseCase useCase : mChildren) {
- useCase.onStateAttached();
- }
- }
-
- void notifyStateDetached() {
- for (UseCase useCase : mChildren) {
- useCase.onStateDetached();
- }
- }
-
- @NonNull
- Set<UseCase> getChildren() {
- return mChildren;
}
/**
- * Gets {@link OutConfig} for children {@link UseCase} based on the input edge.
+ * Sets the rotation applied by this virtual camera.
*/
- @NonNull
- Map<UseCase, OutConfig> getChildrenOutConfigs(@NonNull SurfaceEdge cameraEdge) {
- Map<UseCase, OutConfig> outConfigs = new HashMap<>();
- for (UseCase useCase : mChildren) {
- // TODO(b/264936115): This is a temporary solution where children use the parent
- // stream without changing it. Later we will update it to allow
- // cropping/down-sampling to better match children UseCase config.
- int rotationDegrees = getChildRotationDegrees(useCase);
- outConfigs.put(useCase, OutConfig.of(
- getChildTargetType(useCase),
- getChildFormat(useCase),
- cameraEdge.getCropRect(),
- getRotatedSize(cameraEdge.getCropRect(), rotationDegrees),
- rotationDegrees,
- useCase.isMirroringRequired(this)));
- }
- return outConfigs;
+ void setRotationDegrees(int sensorRotationDegrees) {
+ mVirtualCameraInfo.setVirtualCameraRotationDegrees(sensorRotationDegrees);
}
- /**
- * Update children {@link SurfaceEdge} calculated by {@link StreamSharing}.
- */
- void setChildrenEdges(@NonNull Map<UseCase, SurfaceEdge> childrenEdges) {
- mChildrenEdges.clear();
- mChildrenEdges.putAll(childrenEdges);
- for (Map.Entry<UseCase, SurfaceEdge> entry : mChildrenEdges.entrySet()) {
- UseCase useCase = entry.getKey();
- SurfaceEdge surfaceEdge = entry.getValue();
- useCase.setViewPortCropRect(surfaceEdge.getCropRect());
- useCase.setSensorToBufferTransformMatrix(surfaceEdge.getSensorToBufferTransform());
- useCase.updateSuggestedStreamSpec(surfaceEdge.getStreamSpec());
- useCase.notifyState();
- }
- }
+ // --- Forward UseCase state change to VirtualCameraAdapter ---
- /**
- * Invokes {@link UseCase.StateChangeCallback#onUseCaseReset} for all children.
- */
- void resetChildren() {
- checkMainThread();
- for (UseCase useCase : mChildren) {
- onUseCaseReset(useCase);
- }
- }
-
- /**
- * Gets the callback for receiving parent camera's metadata.
- */
- @NonNull
- CameraCaptureCallback getParentMetadataCallback() {
- return mParentMetadataCallback;
- }
-
- // --- Handle children state change ---
@MainThread
@Override
public void onUseCaseActive(@NonNull UseCase useCase) {
checkMainThread();
- if (isUseCaseActive(useCase)) {
- return;
- }
- mChildrenActiveState.put(useCase, true);
- DeferrableSurface childSurface = getChildSurface(useCase);
- if (childSurface != null) {
- forceSetProvider(getUseCaseEdge(useCase), childSurface, useCase.getSessionConfig());
- }
+ mStateChangeCallback.onUseCaseActive(useCase);
}
@MainThread
@Override
public void onUseCaseInactive(@NonNull UseCase useCase) {
checkMainThread();
- if (!isUseCaseActive(useCase)) {
- return;
- }
- mChildrenActiveState.put(useCase, false);
- getUseCaseEdge(useCase).disconnect();
+ mStateChangeCallback.onUseCaseInactive(useCase);
}
@MainThread
@Override
public void onUseCaseUpdated(@NonNull UseCase useCase) {
checkMainThread();
- if (!isUseCaseActive(useCase)) {
- // No-op if the child is inactive. It will connect when it becomes active.
- return;
- }
- SurfaceEdge edge = getUseCaseEdge(useCase);
- DeferrableSurface childSurface = getChildSurface(useCase);
- if (childSurface != null) {
- // If the child has a Surface, connect. VideoCapture uses this mechanism to
- // resume/start recording.
- forceSetProvider(edge, childSurface, useCase.getSessionConfig());
- } else {
- // If the child has no Surface, disconnect. VideoCapture uses this mechanism to
- // pause/stop recording.
- edge.disconnect();
- }
+ mStateChangeCallback.onUseCaseUpdated(useCase);
}
@MainThread
@Override
public void onUseCaseReset(@NonNull UseCase useCase) {
checkMainThread();
- SurfaceEdge edge = getUseCaseEdge(useCase);
- edge.invalidate();
- if (!isUseCaseActive(useCase)) {
- // No-op if the child is inactive. It will connect when it becomes active.
- return;
- }
- DeferrableSurface childSurface = getChildSurface(useCase);
- if (childSurface != null) {
- forceSetProvider(edge, childSurface, useCase.getSessionConfig());
- }
+ mStateChangeCallback.onUseCaseReset(useCase);
}
// --- Forward parent camera properties and events ---
@@ -352,112 +127,8 @@
return mParentCamera.getCameraState();
}
- // --- private methods ---
-
- @IntRange(from = 0, to = 359)
- private int getChildRotationDegrees(@NonNull UseCase child) {
- if (child instanceof Preview) {
- // Rotate the buffer for Preview because SurfaceView cannot handle rotation.
- return mParentCamera.getCameraInfo().getSensorRotationDegrees(
- ((Preview) child).getTargetRotation());
- }
- // By default, sharing node does not rotate
- return 0;
- }
-
- private static int getChildFormat(@NonNull UseCase useCase) {
- return useCase instanceof ImageCapture ? ImageFormat.JPEG
- : INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
- }
-
- @CameraEffect.Targets
- private static int getChildTargetType(@NonNull UseCase useCase) {
- if (useCase instanceof Preview) {
- return PREVIEW;
- } else if (useCase instanceof ImageCapture) {
- return IMAGE_CAPTURE;
- } else {
- return VIDEO_CAPTURE;
- }
- }
-
- private static int getHighestSurfacePriority(Set<UseCaseConfig<?>> childrenConfigs) {
- int highestPriority = 0;
- for (UseCaseConfig<?> childConfig : childrenConfigs) {
- highestPriority = Math.max(highestPriority,
- childConfig.getSurfaceOccupancyPriority(0));
- }
- return highestPriority;
- }
-
- @NonNull
- private SurfaceEdge getUseCaseEdge(@NonNull UseCase useCase) {
- return requireNonNull(mChildrenEdges.get(useCase));
- }
-
- private boolean isUseCaseActive(@NonNull UseCase useCase) {
- return requireNonNull(mChildrenActiveState.get(useCase));
- }
-
- private void forceSetProvider(@NonNull SurfaceEdge edge,
- @NonNull DeferrableSurface childSurface,
- @NonNull SessionConfig childSessionConfig) {
- edge.invalidate();
- try {
- edge.setProvider(childSurface);
- } catch (DeferrableSurface.SurfaceClosedException e) {
- // The Surface is closed by the child. This will happen when e.g. the child is Preview
- // with SurfaceView implementation.
- // Invoke the error listener so it will recreate the pipeline.
- for (SessionConfig.ErrorListener listener : childSessionConfig.getErrorListeners()) {
- listener.onError(childSessionConfig,
- SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET);
- }
- }
- }
-
- /**
- * Gets the {@link DeferrableSurface} associated with the child.
- */
- @VisibleForTesting
- @Nullable
- static DeferrableSurface getChildSurface(@NonNull UseCase child) {
- // Get repeating Surface for preview & video, regular Surface for image capture.
- List<DeferrableSurface> surfaces = child instanceof ImageCapture
- ? child.getSessionConfig().getSurfaces() :
- child.getSessionConfig().getRepeatingCaptureConfig().getSurfaces();
- checkState(surfaces.size() <= 1);
- if (surfaces.size() == 1) {
- return surfaces.get(0);
- }
- return null;
- }
-
- CameraCaptureCallback createCameraCaptureCallback() {
- return new CameraCaptureCallback() {
- @Override
- public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
- super.onCaptureCompleted(cameraCaptureResult);
- for (UseCase child : mChildren) {
- sendCameraCaptureResultToChild(cameraCaptureResult,
- child.getSessionConfig());
- }
- }
- };
- }
-
- static void sendCameraCaptureResultToChild(
- @NonNull CameraCaptureResult cameraCaptureResult,
- @NonNull SessionConfig sessionConfig) {
- for (CameraCaptureCallback callback :
- sessionConfig.getRepeatingCameraCaptureCallbacks()) {
- callback.onCaptureCompleted(new VirtualCameraCaptureResult(
- sessionConfig.getRepeatingCaptureConfig().getTagBundle(),
- cameraCaptureResult));
- }
- }
-
// --- Unused overrides ---
+
@Override
public void open() {
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
new file mode 100644
index 0000000..6323d6c
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
@@ -0,0 +1,437 @@
+/*
+ * 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.streamsharing;
+
+import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
+import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_PREVIEW_STABILIZATION_MODE;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_VIDEO_STABILIZATION_MODE;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.impl.utils.TransformUtils.within360;
+import static androidx.camera.core.streamsharing.DynamicRangeUtils.resolveDynamicRange;
+import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.graphics.ImageFormat;
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraEffect;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.Preview;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraCaptureCallback;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
+import androidx.camera.core.processing.SurfaceEdge;
+import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A virtual implementation of {@link CameraInternal}.
+ *
+ * <p> This class manages children {@link UseCase} and connects/disconnects them to the
+ * parent {@link StreamSharing}. It also forwards parent camera properties/events to the children.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class VirtualCameraAdapter implements UseCase.StateChangeCallback {
+
+ // Children UseCases associated with this virtual camera.
+ @NonNull
+ final Set<UseCase> mChildren;
+ // Specs for children UseCase, calculated and set by StreamSharing.
+ @NonNull
+ final Map<UseCase, SurfaceEdge> mChildrenEdges = new HashMap<>();
+ @NonNull
+ private final Map<UseCase, VirtualCamera> mChildrenVirtualCameras = new HashMap<>();
+ // Whether a children is in the active state. See: UseCase.State.ACTIVE
+ @NonNull
+ final Map<UseCase, Boolean> mChildrenActiveState = new HashMap<>();
+ // Config factory for getting children's config.
+ @NonNull
+ private final UseCaseConfigFactory mUseCaseConfigFactory;
+ // The parent camera instance.
+ @NonNull
+ private final CameraInternal mParentCamera;
+ // The callback that receives the parent camera's metadata.
+ @NonNull
+ private final CameraCaptureCallback mParentMetadataCallback = createCameraCaptureCallback();
+
+
+ /**
+ * @param parentCamera the parent {@link CameraInternal} instance. For example, the
+ * real camera.
+ * @param children the children {@link UseCase}.
+ * @param useCaseConfigFactory the factory for configuring children {@link UseCase}.
+ */
+ VirtualCameraAdapter(@NonNull CameraInternal parentCamera,
+ @NonNull Set<UseCase> children,
+ @NonNull UseCaseConfigFactory useCaseConfigFactory,
+ @NonNull StreamSharing.Control streamSharingControl) {
+ mParentCamera = parentCamera;
+ mUseCaseConfigFactory = useCaseConfigFactory;
+ mChildren = children;
+ // Set children state to inactive by default.
+ for (UseCase child : children) {
+ mChildrenActiveState.put(child, false);
+ mChildrenVirtualCameras.put(child, new VirtualCamera(
+ parentCamera,
+ this,
+ streamSharingControl));
+ }
+ }
+
+ // --- API for StreamSharing ---
+ void mergeChildrenConfigs(@NonNull MutableConfig mutableConfig) {
+ Set<UseCaseConfig<?>> childrenConfigs = new HashSet<>();
+ for (UseCase useCase : mChildren) {
+ childrenConfigs.add(useCase.mergeConfigs(mParentCamera.getCameraInfoInternal(),
+ null,
+ useCase.getDefaultConfig(true, mUseCaseConfigFactory)));
+ }
+
+ // Merge resolution configs.
+ List<Size> cameraSupportedResolutions =
+ new ArrayList<>(mParentCamera.getCameraInfoInternal().getSupportedResolutions(
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+ Size sensorSize = rectToSize(mParentCamera.getCameraControlInternal().getSensorRect());
+ List<Size> mergedResolutions = getMergedResolutions(cameraSupportedResolutions, sensorSize,
+ mutableConfig, childrenConfigs);
+ mutableConfig.insertOption(OPTION_CUSTOM_ORDERED_RESOLUTIONS, mergedResolutions);
+
+ // Merge Surface occupancy priority.
+ mutableConfig.insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY,
+ getHighestSurfacePriority(childrenConfigs));
+
+ // Merge dynamic range configs. Try to find a dynamic range that can match all child
+ // requirements, or throw an exception if no matching dynamic range.
+ // TODO: This approach works for the current code base, where only VideoCapture can be
+ // configured (Preview follows the settings, ImageCapture is fixed as SDR). When
+ // dynamic range APIs opened on other use cases, we might want a more advanced approach
+ // that allows conflicts, e.g. converting HDR stream to SDR stream.
+ DynamicRange dynamicRange = resolveDynamicRange(childrenConfigs);
+ if (dynamicRange == null) {
+ throw new IllegalArgumentException("Failed to merge child dynamic ranges, can not find"
+ + " a dynamic range that satisfies all children.");
+ }
+ mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+
+ // Merge Preview stabilization and video stabilization configs.
+ for (UseCase useCase : mChildren) {
+ if (useCase.getCurrentConfig().getVideoStabilizationMode()
+ != StabilizationMode.UNSPECIFIED) {
+ mutableConfig.insertOption(OPTION_VIDEO_STABILIZATION_MODE,
+ useCase.getCurrentConfig().getVideoStabilizationMode());
+ }
+
+ if (useCase.getCurrentConfig().getPreviewStabilizationMode()
+ != StabilizationMode.UNSPECIFIED) {
+ mutableConfig.insertOption(OPTION_PREVIEW_STABILIZATION_MODE,
+ useCase.getCurrentConfig().getPreviewStabilizationMode());
+ }
+ }
+ }
+
+ void bindChildren() {
+ for (UseCase useCase : mChildren) {
+ useCase.bindToCamera(
+ requireNonNull(mChildrenVirtualCameras.get(useCase)),
+ null,
+ useCase.getDefaultConfig(true, mUseCaseConfigFactory));
+ }
+ }
+
+ void unbindChildren() {
+ for (UseCase useCase : mChildren) {
+ useCase.unbindFromCamera(requireNonNull(mChildrenVirtualCameras.get(useCase)));
+ }
+ }
+
+ void notifyStateAttached() {
+ for (UseCase useCase : mChildren) {
+ useCase.onStateAttached();
+ }
+ }
+
+ void notifyStateDetached() {
+ for (UseCase useCase : mChildren) {
+ useCase.onStateDetached();
+ }
+ }
+
+ @NonNull
+ Set<UseCase> getChildren() {
+ return mChildren;
+ }
+
+ /**
+ * Gets {@link OutConfig} for children {@link UseCase} based on the input edge.
+ */
+ @NonNull
+ Map<UseCase, OutConfig> getChildrenOutConfigs(@NonNull SurfaceEdge cameraEdge,
+ @ImageOutputConfig.RotationValue int parentTargetRotation) {
+ Map<UseCase, OutConfig> outConfigs = new HashMap<>();
+ int parentRotationDegrees = mParentCamera.getCameraInfo().getSensorRotationDegrees(
+ parentTargetRotation);
+ for (UseCase useCase : mChildren) {
+ // TODO(b/264936115): This is a temporary solution where children use the parent
+ // stream without changing it. Later we will update it to allow
+ // cropping/down-sampling to better match children UseCase config.
+ int childRotationDegrees = getChildRotationDegrees(useCase);
+ requireNonNull(mChildrenVirtualCameras.get(useCase))
+ .setRotationDegrees(childRotationDegrees);
+ int childParentDelta = within360(
+ cameraEdge.getRotationDegrees() + childRotationDegrees - parentRotationDegrees);
+ outConfigs.put(useCase, OutConfig.of(
+ getChildTargetType(useCase),
+ getChildFormat(useCase),
+ cameraEdge.getCropRect(),
+ getRotatedSize(cameraEdge.getCropRect(), childParentDelta),
+ childParentDelta,
+ useCase.isMirroringRequired(mParentCamera)));
+ }
+ return outConfigs;
+ }
+
+ /**
+ * Update children {@link SurfaceEdge} calculated by {@link StreamSharing}.
+ */
+ void setChildrenEdges(@NonNull Map<UseCase, SurfaceEdge> childrenEdges) {
+ mChildrenEdges.clear();
+ mChildrenEdges.putAll(childrenEdges);
+ for (Map.Entry<UseCase, SurfaceEdge> entry : mChildrenEdges.entrySet()) {
+ UseCase useCase = entry.getKey();
+ SurfaceEdge surfaceEdge = entry.getValue();
+ useCase.setViewPortCropRect(surfaceEdge.getCropRect());
+ useCase.setSensorToBufferTransformMatrix(surfaceEdge.getSensorToBufferTransform());
+ useCase.updateSuggestedStreamSpec(surfaceEdge.getStreamSpec());
+ useCase.notifyState();
+ }
+ }
+
+ /**
+ * Invokes {@link UseCase.StateChangeCallback#onUseCaseReset} for all children.
+ */
+ void resetChildren() {
+ checkMainThread();
+ for (UseCase useCase : mChildren) {
+ onUseCaseReset(useCase);
+ }
+ }
+
+ /**
+ * Gets the callback for receiving parent camera's metadata.
+ */
+ @NonNull
+ CameraCaptureCallback getParentMetadataCallback() {
+ return mParentMetadataCallback;
+ }
+
+ // --- Handle children state change ---
+ @MainThread
+ @Override
+ public void onUseCaseActive(@NonNull UseCase useCase) {
+ checkMainThread();
+ if (isUseCaseActive(useCase)) {
+ return;
+ }
+ mChildrenActiveState.put(useCase, true);
+ DeferrableSurface childSurface = getChildSurface(useCase);
+ if (childSurface != null) {
+ forceSetProvider(getUseCaseEdge(useCase), childSurface, useCase.getSessionConfig());
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onUseCaseInactive(@NonNull UseCase useCase) {
+ checkMainThread();
+ if (!isUseCaseActive(useCase)) {
+ return;
+ }
+ mChildrenActiveState.put(useCase, false);
+ getUseCaseEdge(useCase).disconnect();
+ }
+
+ @MainThread
+ @Override
+ public void onUseCaseUpdated(@NonNull UseCase useCase) {
+ checkMainThread();
+ if (!isUseCaseActive(useCase)) {
+ // No-op if the child is inactive. It will connect when it becomes active.
+ return;
+ }
+ SurfaceEdge edge = getUseCaseEdge(useCase);
+ DeferrableSurface childSurface = getChildSurface(useCase);
+ if (childSurface != null) {
+ // If the child has a Surface, connect. VideoCapture uses this mechanism to
+ // resume/start recording.
+ forceSetProvider(edge, childSurface, useCase.getSessionConfig());
+ } else {
+ // If the child has no Surface, disconnect. VideoCapture uses this mechanism to
+ // pause/stop recording.
+ edge.disconnect();
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onUseCaseReset(@NonNull UseCase useCase) {
+ checkMainThread();
+ SurfaceEdge edge = getUseCaseEdge(useCase);
+ edge.invalidate();
+ if (!isUseCaseActive(useCase)) {
+ // No-op if the child is inactive. It will connect when it becomes active.
+ return;
+ }
+ DeferrableSurface childSurface = getChildSurface(useCase);
+ if (childSurface != null) {
+ forceSetProvider(edge, childSurface, useCase.getSessionConfig());
+ }
+ }
+
+ // --- private methods ---
+
+ @IntRange(from = 0, to = 359)
+ private int getChildRotationDegrees(@NonNull UseCase child) {
+ int childTargetRotation = ((ImageOutputConfig) child.getCurrentConfig())
+ .getTargetRotation(Surface.ROTATION_0);
+ return mParentCamera.getCameraInfo().getSensorRotationDegrees(
+ childTargetRotation);
+ }
+
+ private static int getChildFormat(@NonNull UseCase useCase) {
+ return useCase instanceof ImageCapture ? ImageFormat.JPEG
+ : INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+ }
+
+ @CameraEffect.Targets
+ private static int getChildTargetType(@NonNull UseCase useCase) {
+ if (useCase instanceof Preview) {
+ return PREVIEW;
+ } else if (useCase instanceof ImageCapture) {
+ return IMAGE_CAPTURE;
+ } else {
+ return VIDEO_CAPTURE;
+ }
+ }
+
+ private static int getHighestSurfacePriority(Set<UseCaseConfig<?>> childrenConfigs) {
+ int highestPriority = 0;
+ for (UseCaseConfig<?> childConfig : childrenConfigs) {
+ highestPriority = Math.max(highestPriority,
+ childConfig.getSurfaceOccupancyPriority(0));
+ }
+ return highestPriority;
+ }
+
+ @NonNull
+ private SurfaceEdge getUseCaseEdge(@NonNull UseCase useCase) {
+ return requireNonNull(mChildrenEdges.get(useCase));
+ }
+
+ private boolean isUseCaseActive(@NonNull UseCase useCase) {
+ return requireNonNull(mChildrenActiveState.get(useCase));
+ }
+
+ private static void forceSetProvider(@NonNull SurfaceEdge edge,
+ @NonNull DeferrableSurface childSurface,
+ @NonNull SessionConfig childSessionConfig) {
+ edge.invalidate();
+ try {
+ edge.setProvider(childSurface);
+ } catch (DeferrableSurface.SurfaceClosedException e) {
+ // The Surface is closed by the child. This will happen when e.g. the child is Preview
+ // with SurfaceView implementation.
+ // Invoke the error listener so it will recreate the pipeline.
+ for (SessionConfig.ErrorListener listener : childSessionConfig.getErrorListeners()) {
+ listener.onError(childSessionConfig,
+ SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET);
+ }
+ }
+ }
+
+ /**
+ * Gets the {@link DeferrableSurface} associated with the child.
+ */
+ @VisibleForTesting
+ @Nullable
+ static DeferrableSurface getChildSurface(@NonNull UseCase child) {
+ // Get repeating Surface for preview & video, regular Surface for image capture.
+ List<DeferrableSurface> surfaces = child instanceof ImageCapture
+ ? child.getSessionConfig().getSurfaces() :
+ child.getSessionConfig().getRepeatingCaptureConfig().getSurfaces();
+ checkState(surfaces.size() <= 1);
+ if (surfaces.size() == 1) {
+ return surfaces.get(0);
+ }
+ return null;
+ }
+
+ CameraCaptureCallback createCameraCaptureCallback() {
+ return new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+ super.onCaptureCompleted(cameraCaptureResult);
+ for (UseCase child : mChildren) {
+ sendCameraCaptureResultToChild(cameraCaptureResult,
+ child.getSessionConfig());
+ }
+ }
+ };
+ }
+
+ static void sendCameraCaptureResultToChild(
+ @NonNull CameraCaptureResult cameraCaptureResult,
+ @NonNull SessionConfig sessionConfig) {
+ for (CameraCaptureCallback callback :
+ sessionConfig.getRepeatingCameraCaptureCallbacks()) {
+ callback.onCaptureCompleted(new VirtualCameraCaptureResult(
+ sessionConfig.getRepeatingCaptureConfig().getTagBundle(),
+ cameraCaptureResult));
+ }
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java
index cdd2e3e..88d96ad 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java
@@ -16,12 +16,15 @@
package androidx.camera.core.streamsharing;
+import static androidx.camera.core.impl.utils.TransformUtils.within360;
+
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.ForwardingCameraInfo;
+import androidx.camera.core.impl.ImageOutputConfig;
import java.util.UUID;
@@ -32,6 +35,7 @@
public class VirtualCameraInfo extends ForwardingCameraInfo {
private final String mVirtualCameraId;
+ private int mVirtualCameraRotationDegrees;
VirtualCameraInfo(@NonNull CameraInfoInternal cameraInfoInternal) {
super(cameraInfoInternal);
@@ -48,4 +52,19 @@
public String getCameraId() {
return mVirtualCameraId;
}
+
+ /**
+ * Sets the rotation applied by this virtual camera.
+ */
+ void setVirtualCameraRotationDegrees(int virtualCameraRotationDegrees) {
+ mVirtualCameraRotationDegrees = virtualCameraRotationDegrees;
+ }
+
+ @Override
+ public int getSensorRotationDegrees(@ImageOutputConfig.RotationValue int relativeRotation) {
+ // The child UseCase calls this method to get the remaining rotation degrees, which is the
+ // original rotation minus the rotation applied by the virtual camera.
+ return within360(
+ super.getSensorRotationDegrees(relativeRotation) - mVirtualCameraRotationDegrees);
+ }
}
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 4d2bc37..ab1c29f 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
@@ -505,19 +505,6 @@
}
@Test
- fun noCameraTransform_rotationDegreesIsZero() {
- // Act: create preview with hasCameraTransform == false
- frontCamera.hasTransform = false
- val preview = createPreview(
- effect,
- frontCamera,
- targetRotation = ROTATION_90
- )
- // Assert: rotationDegrees is 0.
- assertThat(preview.cameraEdge.rotationDegrees).isEqualTo(0)
- }
-
- @Test
fun setNoCameraTransform_propagatesToCameraEdge() {
// Act: create preview with hasCameraTransform == false
frontCamera.hasTransform = false
@@ -787,6 +774,21 @@
assertThat(preview.isPreviewStabilizationEnabled).isTrue()
}
+ @Test
+ fun canSetDynamicRange() {
+ // Use an unspecified dynamic range that isn't the default, UNSPECIFIED.
+ val preview = Preview.Builder().setDynamicRange(DynamicRange.HDR_UNSPECIFIED_10_BIT).build()
+
+ assertThat(preview.dynamicRange).isEqualTo(DynamicRange.HDR_UNSPECIFIED_10_BIT)
+ }
+
+ @Test
+ fun defaultDynamicRange_isUnspecified() {
+ val preview = Preview.Builder().build()
+
+ assertThat(preview.dynamicRange).isEqualTo(DynamicRange.UNSPECIFIED)
+ }
+
private fun bindToLifecycleAndGetSurfaceRequest(): SurfaceRequest {
return bindToLifecycleAndGetResult(null).first
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index e36141d..f910404 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -136,6 +136,8 @@
streamSharing.unbindFromCamera(streamSharing.camera!!)
}
effectProcessor.release()
+ sharingProcessor.cleanUp()
+ effectProcessor.cleanUp()
shadowOf(getMainLooper()).idle()
}
@@ -220,12 +222,12 @@
fun childTakingPicture_getJpegQuality() {
// Arrange: set up StreamSharing with min latency ImageCapture as child
val imageCapture = ImageCapture.Builder()
- .setTargetRotation(Surface.ROTATION_90)
.setCaptureMode(CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
streamSharing = StreamSharing(camera, setOf(child1, imageCapture), useCaseConfigFactory)
streamSharing.bindToCamera(camera, null, defaultConfig)
streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+ imageCapture.targetRotation = Surface.ROTATION_90
// Act: the child takes a picture.
imageCapture.takePicture(directExecutor(), object : ImageCapture.OnImageCapturedCallback() {
@@ -500,12 +502,12 @@
assertThat(child2.pipelineCreationCount).isEqualTo(2)
shadowOf(getMainLooper()).idle()
// Assert: child Surface are propagated to StreamSharing.
- val child1Surface =
- streamSharing.virtualCamera.mChildrenEdges[child1]!!.deferrableSurfaceForTesting.surface
+ val child1Surface = streamSharing.virtualCameraAdapter.mChildrenEdges[child1]!!
+ .deferrableSurfaceForTesting.surface
assertThat(child1Surface.isDone).isTrue()
assertThat(child1Surface.get()).isEqualTo(surface1)
- val child2Surface =
- streamSharing.virtualCamera.mChildrenEdges[child2]!!.deferrableSurfaceForTesting.surface
+ val child2Surface = streamSharing.virtualCameraAdapter.mChildrenEdges[child2]!!
+ .deferrableSurfaceForTesting.surface
assertThat(child2Surface.isDone).isTrue()
assertThat(child2Surface.get()).isEqualTo(surface2)
@@ -526,6 +528,17 @@
}
@Test
+ fun bindChildToCamera_virtualCameraHasNoRotationDegrees() {
+ // Act.
+ streamSharing.bindToCamera(frontCamera, null, null)
+ // Assert.
+ assertThat(child1.camera!!.cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(0)
+ assertThat(child2.camera!!.cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(0)
+ }
+
+ @Test
fun bindAndUnbindParent_propagatesToChildren() {
// Assert: children not bound to camera by default.
assertThat(child1.camera).isNull()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
new file mode 100644
index 0000000..9c85fe8
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
@@ -0,0 +1,334 @@
+/*
+ * 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.streamsharing
+
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.os.Looper.getMainLooper
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
+import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+import androidx.camera.core.impl.ImageOutputConfig.ROTATION_NOT_SPECIFIED
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionConfig.defaultEmptySessionConfig
+import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.processing.SurfaceEdge
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.impl.fakes.FakeDeferrableSurface
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
+import androidx.camera.testing.impl.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
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [VirtualCameraAdapter].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VirtualCameraAdapterTest {
+
+ companion object {
+ private const val CLOSED = true
+ private const val OPEN = false
+ private const val HAS_PROVIDER = true
+ private const val NO_PROVIDER = false
+ private val INPUT_SIZE = Size(800, 600)
+ private val CROP_RECT = Rect(0, 0, 800, 600)
+
+ // Arbitrary transform to test that the transform is propagated.
+ private val SENSOR_TO_BUFFER = Matrix().apply { setScale(1f, -1f) }
+ private var receivedSessionConfigError: SessionConfig.SessionError? = null
+ private val SESSION_CONFIG_WITH_SURFACE = SessionConfig.Builder()
+ .addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE))
+ .addErrorListener { _, error ->
+ receivedSessionConfigError = error
+ }.build()
+ }
+
+ private val surfaceEdgesToClose = mutableListOf<SurfaceEdge>()
+ private val parentCamera = FakeCamera()
+ private val child1 = FakeUseCaseConfig.Builder().setTargetRotation(Surface.ROTATION_0).build()
+ private val child2 = FakeUseCaseConfig.Builder()
+ .setMirrorMode(MIRROR_MODE_ON)
+ .build()
+ private val childrenEdges = mapOf(
+ Pair(child1 as UseCase, createSurfaceEdge()),
+ Pair(child2 as UseCase, createSurfaceEdge())
+ )
+ private val useCaseConfigFactory = FakeUseCaseConfigFactory()
+ private lateinit var adapter: VirtualCameraAdapter
+ private var snapshotTriggered = false
+
+ @Before
+ fun setUp() {
+ adapter = VirtualCameraAdapter(
+ parentCamera, setOf(child1, child2), useCaseConfigFactory
+ ) { _, _ ->
+ snapshotTriggered = true
+ Futures.immediateFuture(null)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ for (surfaceEdge in surfaceEdgesToClose) {
+ surfaceEdge.close()
+ }
+ }
+
+ @Test
+ fun submitStillCaptureRequests_triggersSnapshot() {
+ // Arrange.
+ adapter.bindChildren()
+
+ // Act: submit a still capture request from a child.
+ val cameraControl = child1.camera!!.cameraControl as CameraControlInternal
+ cameraControl.submitStillCaptureRequests(
+ listOf(CaptureConfig.Builder().build()),
+ CAPTURE_MODE_MINIMIZE_LATENCY,
+ FLASH_MODE_AUTO
+ )
+ shadowOf(getMainLooper()).idle()
+
+ // The StreamSharing.Control is called to take a snapshot.
+ assertThat(snapshotTriggered).isTrue()
+ }
+
+ @Test
+ fun getImageCaptureSurface_returnsNonRepeatingSurface() {
+ assertThat(getUseCaseSurface(ImageCapture.Builder().build())).isNotNull()
+ }
+
+ @Test
+ fun getChildSurface_returnsRepeatingSurface() {
+ // Arrange.
+ val surfaceTexture = SurfaceTexture(0)
+ val surface = Surface(surfaceTexture)
+ val preview = Preview.Builder().build().apply {
+ this.setSurfaceProvider {
+ it.provideSurface(surface, directExecutor()) {
+ surfaceTexture.release()
+ surface.release()
+ }
+ }
+ }
+ // Act & Assert.
+ assertThat(getUseCaseSurface(preview)).isNotNull()
+ // Cleanup.
+ preview.unbindFromCamera(parentCamera)
+ surfaceTexture.release()
+ surface.release()
+ }
+
+ private fun getUseCaseSurface(useCase: UseCase): DeferrableSurface? {
+ useCase.bindToCamera(
+ parentCamera,
+ null,
+ useCase.getDefaultConfig(true, useCaseConfigFactory)
+ )
+ useCase.updateSuggestedStreamSpec(StreamSpec.builder(INPUT_SIZE).build())
+ return VirtualCameraAdapter.getChildSurface(useCase)
+ }
+
+ @Test
+ fun setUseCaseActiveAndInactive_surfaceConnectsAndDisconnects() {
+ // Arrange.
+ adapter.bindChildren()
+ adapter.setChildrenEdges(childrenEdges)
+ child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+ // Assert: edge open by default.
+ verifyEdge(child1, OPEN, NO_PROVIDER)
+ // Set UseCase to active, verify it has provider.
+ child1.notifyActiveForTesting()
+ verifyEdge(child1, OPEN, HAS_PROVIDER)
+ // Set UseCase to inactive, verify it's closed.
+ child1.notifyInactiveForTesting()
+ verifyEdge(child1, CLOSED, HAS_PROVIDER)
+ // Set UseCase to active, verify it becomes open again.
+ child1.notifyActiveForTesting()
+ verifyEdge(child1, OPEN, HAS_PROVIDER)
+ }
+
+ @Test
+ fun resetWithClosedChildSurface_invokesErrorListener() {
+ // Arrange.
+ adapter.bindChildren()
+ adapter.setChildrenEdges(childrenEdges)
+ child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+ child1.notifyActiveForTesting()
+
+ // Act: close the child surface.
+ SESSION_CONFIG_WITH_SURFACE.surfaces[0].close()
+ adapter.onUseCaseReset(child1)
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: error listener is invoked.
+ assertThat(receivedSessionConfigError)
+ .isEqualTo(SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET)
+ }
+
+ @Test
+ fun resetUseCase_edgeInvalidated() {
+ // Arrange: setup and get the old DeferrableSurface.
+ adapter.bindChildren()
+ adapter.setChildrenEdges(childrenEdges)
+ child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+ child1.notifyActiveForTesting()
+ val oldSurface = childrenEdges[child1]!!.deferrableSurfaceForTesting
+ // Act: notify reset.
+ child1.notifyResetForTesting()
+ // Assert: DeferrableSurface is recreated. The old one is closed.
+ assertThat(oldSurface.isClosed).isTrue()
+ assertThat(childrenEdges[child1]!!.deferrableSurfaceForTesting)
+ .isNotSameInstanceAs(oldSurface)
+ verifyEdge(child1, OPEN, HAS_PROVIDER)
+ }
+
+ @Test
+ fun updateUseCaseWithAndWithoutSurface_surfaceConnectsAndDisconnects() {
+ // Arrange
+ adapter.bindChildren()
+ adapter.setChildrenEdges(childrenEdges)
+ child1.notifyActiveForTesting()
+ verifyEdge(child1, OPEN, NO_PROVIDER)
+
+ // Act: set Surface and update
+ child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+ child1.notifyUpdatedForTesting()
+ // Assert: edge is connected.
+ verifyEdge(child1, OPEN, HAS_PROVIDER)
+ // Act: remove Surface and update.
+ child1.updateSessionConfigForTesting(defaultEmptySessionConfig())
+ child1.notifyUpdatedForTesting()
+ // Assert: edge is disconnected.
+ verifyEdge(child1, CLOSED, HAS_PROVIDER)
+ // Act: set Surface and update.
+ child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+ child1.notifyUpdatedForTesting()
+ // Assert: edge is connected again.
+ verifyEdge(child1, OPEN, HAS_PROVIDER)
+ }
+
+ @Test
+ fun getChildrenOutConfigs() {
+ // Arrange.
+ val cropRect = Rect(10, 10, 410, 310)
+ val preview = Preview.Builder().setTargetRotation(Surface.ROTATION_90).build()
+ val imageCapture = ImageCapture.Builder().setTargetRotation(Surface.ROTATION_0).build()
+ adapter = VirtualCameraAdapter(
+ parentCamera, setOf(preview, child2, imageCapture), useCaseConfigFactory
+ ) { _, _ ->
+ Futures.immediateFuture(null)
+ }
+
+ // Act.
+ val outConfigs = adapter.getChildrenOutConfigs(
+ createSurfaceEdge(cropRect = cropRect, rotationDegrees = 90),
+ Surface.ROTATION_90
+ )
+
+ // Assert: preview config
+ val previewOutConfig = outConfigs[preview]!!
+ assertThat(previewOutConfig.format).isEqualTo(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+ assertThat(previewOutConfig.targets).isEqualTo(PREVIEW)
+ assertThat(previewOutConfig.cropRect).isEqualTo(cropRect)
+ // Preview's target rotation matches the parent's, so it only applies the 90° rotation.
+ assertThat(previewOutConfig.size).isEqualTo(Size(300, 400))
+ assertThat(previewOutConfig.rotationDegrees).isEqualTo(90)
+ assertThat(previewOutConfig.mirroring).isFalse()
+ // Assert: ImageCapture config
+ val imageOutConfig = outConfigs[imageCapture]!!
+ assertThat(imageOutConfig.format).isEqualTo(ImageFormat.JPEG)
+ assertThat(imageOutConfig.targets).isEqualTo(IMAGE_CAPTURE)
+ // ImageCapture's target rotation does not match the parent's, so it applies the delta on
+ // top of the 90° rotation.
+ assertThat(imageOutConfig.size).isEqualTo(Size(400, 300))
+ assertThat(imageOutConfig.rotationDegrees).isEqualTo(180)
+ // Assert: child2
+ val outConfig2 = outConfigs[child2]!!
+ assertThat(outConfig2.format).isEqualTo(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+ assertThat(outConfig2.targets).isEqualTo(VIDEO_CAPTURE)
+ assertThat(outConfig2.cropRect).isEqualTo(cropRect)
+ assertThat(outConfig2.mirroring).isTrue()
+ }
+
+ @Test
+ fun updateChildrenSpec_updateAndNotifyChildren() {
+ // Act: update children with the map.
+ adapter.setChildrenEdges(childrenEdges)
+ // Assert: surface size, crop rect and transformation propagated to children
+ assertThat(child1.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
+ assertThat(child2.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
+ assertThat(child1.viewPortCropRect).isEqualTo(CROP_RECT)
+ assertThat(child2.viewPortCropRect).isEqualTo(CROP_RECT)
+ assertThat(child1.sensorToBufferTransformMatrix).isEqualTo(SENSOR_TO_BUFFER)
+ assertThat(child2.sensorToBufferTransformMatrix).isEqualTo(SENSOR_TO_BUFFER)
+ }
+
+ private fun createSurfaceEdge(
+ target: Int = PREVIEW,
+ format: Int = INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ streamSpec: StreamSpec = StreamSpec.builder(INPUT_SIZE).build(),
+ matrix: Matrix = SENSOR_TO_BUFFER,
+ hasCameraTransform: Boolean = true,
+ cropRect: Rect = CROP_RECT,
+ rotationDegrees: Int = 0,
+ mirroring: Boolean = false
+ ): SurfaceEdge {
+ return SurfaceEdge(
+ target,
+ format,
+ streamSpec,
+ matrix,
+ hasCameraTransform,
+ cropRect,
+ rotationDegrees,
+ ROTATION_NOT_SPECIFIED,
+ mirroring
+ ).also { surfaceEdgesToClose.add(it) }
+ }
+
+ private fun verifyEdge(child: UseCase, isClosed: Boolean, hasProvider: Boolean) {
+ assertThat(childrenEdges[child]!!.deferrableSurfaceForTesting.isClosed).isEqualTo(isClosed)
+ assertThat(childrenEdges[child]!!.hasProvider()).isEqualTo(hasProvider)
+ }
+}
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 4681fbb..65446d4 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
@@ -16,45 +16,17 @@
package androidx.camera.core.streamsharing
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.graphics.Rect
-import android.graphics.SurfaceTexture
import android.os.Build
-import android.os.Looper.getMainLooper
-import android.util.Size
import android.view.Surface
-import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
-import androidx.camera.core.CameraEffect.PREVIEW
-import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
-import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
-import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
-import androidx.camera.core.Preview
+import androidx.camera.core.CameraSelector
import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
-import androidx.camera.core.impl.ImageOutputConfig.ROTATION_NOT_SPECIFIED
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.SessionConfig.defaultEmptySessionConfig
-import androidx.camera.core.impl.StreamSpec
-import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
import androidx.camera.core.impl.utils.futures.Futures
-import androidx.camera.core.processing.SurfaceEdge
import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.impl.fakes.FakeDeferrableSurface
-import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
-import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@@ -66,55 +38,32 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class VirtualCameraTest {
- companion object {
- private const val CLOSED = true
- private const val OPEN = false
- private const val HAS_PROVIDER = true
- private const val NO_PROVIDER = false
- private val INPUT_SIZE = Size(800, 600)
- private val CROP_RECT = Rect(0, 0, 800, 600)
+ private val cameraInfo = FakeCameraInfoInternal(90, CameraSelector.LENS_FACING_BACK)
- // Arbitrary transform to test that the transform is propagated.
- private val SENSOR_TO_BUFFER = Matrix().apply { setScale(1f, -1f) }
- private var receivedSessionConfigError: SessionConfig.SessionError? = null
- private val SESSION_CONFIG_WITH_SURFACE = SessionConfig.Builder()
- .addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE))
- .addErrorListener { _, error ->
- receivedSessionConfigError = error
- }.build()
- }
+ private val parentCamera = FakeCamera(null, cameraInfo)
- private val surfaceEdgesToClose = mutableListOf<SurfaceEdge>()
- private val parentCamera = FakeCamera()
- private val child1 = FakeUseCaseConfig.Builder().setTargetRotation(Surface.ROTATION_0).build()
- private val child2 = FakeUseCaseConfig.Builder()
- .setMirrorMode(MIRROR_MODE_ON)
- .build()
- private val childrenEdges = mapOf(
- Pair(child1 as UseCase, createSurfaceEdge()),
- Pair(child2 as UseCase, createSurfaceEdge())
- )
- private val useCaseConfigFactory = FakeUseCaseConfigFactory()
- private lateinit var virtualCamera: VirtualCamera
- private var snapshotTriggered = false
+ private val useCaseStateCallback = object : UseCase.StateChangeCallback {
- @Before
- fun setUp() {
- virtualCamera = VirtualCamera(
- parentCamera, setOf(child1, child2), useCaseConfigFactory
- ) { _, _ ->
- snapshotTriggered = true
- Futures.immediateFuture(null)
+ override fun onUseCaseActive(useCase: UseCase) {
+ }
+
+ override fun onUseCaseInactive(useCase: UseCase) {
+ }
+
+ override fun onUseCaseUpdated(useCase: UseCase) {
+ }
+
+ override fun onUseCaseReset(useCase: UseCase) {
}
}
- @After
- fun tearDown() {
- for (surfaceEdge in surfaceEdgesToClose) {
- surfaceEdge.close()
- }
+ private val streamSharingControl = StreamSharing.Control { _, _ ->
+ Futures.immediateFuture(null)
}
+ private val virtualCamera =
+ VirtualCamera(parentCamera, useCaseStateCallback, streamSharingControl)
+
@Test
fun getCameraId_returnsVirtualCameraId() {
assertThat(virtualCamera.cameraInfoInternal.cameraId)
@@ -122,219 +71,16 @@
}
@Test
- fun submitStillCaptureRequests_triggersSnapshot() {
- // Arrange.
- virtualCamera.bindChildren()
-
- // Act: submit a still capture request from a child.
- val cameraControl = child1.camera!!.cameraControl as CameraControlInternal
- cameraControl.submitStillCaptureRequests(
- listOf(CaptureConfig.Builder().build()),
- CAPTURE_MODE_MINIMIZE_LATENCY,
- FLASH_MODE_AUTO
- )
- shadowOf(getMainLooper()).idle()
-
- // The StreamSharing.Control is called to take a snapshot.
- assertThat(snapshotTriggered).isTrue()
- }
-
- @Test
- fun getImageCaptureSurface_returnsNonRepeatingSurface() {
- assertThat(getUseCaseSurface(ImageCapture.Builder().build())).isNotNull()
- }
-
- @Test
- fun getChildSurface_returnsRepeatingSurface() {
- // Arrange.
- val surfaceTexture = SurfaceTexture(0)
- val surface = Surface(surfaceTexture)
- val preview = Preview.Builder().build().apply {
- this.setSurfaceProvider {
- it.provideSurface(surface, directExecutor()) {
- surfaceTexture.release()
- surface.release()
- }
- }
- }
- // Act & Assert.
- assertThat(getUseCaseSurface(preview)).isNotNull()
- // Cleanup.
- preview.unbindFromCamera(parentCamera)
- }
-
- private fun getUseCaseSurface(useCase: UseCase): DeferrableSurface? {
- useCase.bindToCamera(
- parentCamera,
- null,
- useCase.getDefaultConfig(true, useCaseConfigFactory)
- )
- useCase.updateSuggestedStreamSpec(StreamSpec.builder(INPUT_SIZE).build())
- return VirtualCamera.getChildSurface(useCase)
- }
-
- @Test
- fun setUseCaseActiveAndInactive_surfaceConnectsAndDisconnects() {
- // Arrange.
- virtualCamera.bindChildren()
- virtualCamera.setChildrenEdges(childrenEdges)
- child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
- // Assert: edge open by default.
- verifyEdge(child1, OPEN, NO_PROVIDER)
- // Set UseCase to active, verify it has provider.
- child1.notifyActiveForTesting()
- verifyEdge(child1, OPEN, HAS_PROVIDER)
- // Set UseCase to inactive, verify it's closed.
- child1.notifyInactiveForTesting()
- verifyEdge(child1, CLOSED, HAS_PROVIDER)
- // Set UseCase to active, verify it becomes open again.
- child1.notifyActiveForTesting()
- verifyEdge(child1, OPEN, HAS_PROVIDER)
- }
-
- @Test
- fun resetWithClosedChildSurface_invokesErrorListener() {
- // Arrange.
- virtualCamera.bindChildren()
- virtualCamera.setChildrenEdges(childrenEdges)
- child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
- child1.notifyActiveForTesting()
-
- // Act: close the child surface.
- SESSION_CONFIG_WITH_SURFACE.surfaces[0].close()
- virtualCamera.onUseCaseReset(child1)
- shadowOf(getMainLooper()).idle()
-
- // Assert: error listener is invoked.
- assertThat(receivedSessionConfigError)
- .isEqualTo(SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET)
- }
-
- @Test
- fun resetUseCase_edgeInvalidated() {
- // Arrange: setup and get the old DeferrableSurface.
- virtualCamera.bindChildren()
- virtualCamera.setChildrenEdges(childrenEdges)
- child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
- child1.notifyActiveForTesting()
- val oldSurface = childrenEdges[child1]!!.deferrableSurfaceForTesting
- // Act: notify reset.
- child1.notifyResetForTesting()
- // Assert: DeferrableSurface is recreated. The old one is closed.
- assertThat(oldSurface.isClosed).isTrue()
- assertThat(childrenEdges[child1]!!.deferrableSurfaceForTesting)
- .isNotSameInstanceAs(oldSurface)
- verifyEdge(child1, OPEN, HAS_PROVIDER)
- }
-
- @Test
- fun updateUseCaseWithAndWithoutSurface_surfaceConnectsAndDisconnects() {
- // Arrange
- virtualCamera.bindChildren()
- virtualCamera.setChildrenEdges(childrenEdges)
- child1.notifyActiveForTesting()
- verifyEdge(child1, OPEN, NO_PROVIDER)
-
- // Act: set Surface and update
- child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
- child1.notifyUpdatedForTesting()
- // Assert: edge is connected.
- verifyEdge(child1, OPEN, HAS_PROVIDER)
- // Act: remove Surface and update.
- child1.updateSessionConfigForTesting(defaultEmptySessionConfig())
- child1.notifyUpdatedForTesting()
- // Assert: edge is disconnected.
- verifyEdge(child1, CLOSED, HAS_PROVIDER)
- // Act: set Surface and update.
- child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
- child1.notifyUpdatedForTesting()
- // Assert: edge is connected again.
- verifyEdge(child1, OPEN, HAS_PROVIDER)
- }
-
- @Test
- fun virtualCameraInheritsParentProperties() {
+ fun getCameraState_returnsParentState() {
assertThat(virtualCamera.cameraState).isEqualTo(parentCamera.cameraState)
- assertThat(virtualCamera.cameraInfoInternal.implementation)
- .isEqualTo(virtualCamera.cameraInfoInternal.implementation)
}
@Test
- fun getChildrenOutConfigs() {
- // Arrange.
- val cropRect = Rect(10, 10, 410, 310)
- val preview = Preview.Builder().setTargetRotation(Surface.ROTATION_90).build()
- val imageCapture = ImageCapture.Builder().build()
- virtualCamera = VirtualCamera(
- parentCamera, setOf(preview, child2, imageCapture), useCaseConfigFactory
- ) { _, _ ->
- Futures.immediateFuture(null)
- }
-
- // Act.
- val outConfigs = virtualCamera.getChildrenOutConfigs(
- createSurfaceEdge(cropRect = cropRect)
- )
-
- // Assert: preview config
- val previewOutConfig = outConfigs[preview]!!
- assertThat(previewOutConfig.format).isEqualTo(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
- assertThat(previewOutConfig.targets).isEqualTo(PREVIEW)
- assertThat(previewOutConfig.cropRect).isEqualTo(cropRect)
- assertThat(previewOutConfig.size).isEqualTo(Size(300, 400))
- assertThat(previewOutConfig.rotationDegrees).isEqualTo(270)
- assertThat(previewOutConfig.mirroring).isFalse()
- // Assert: ImageCapture config
- val imageOutConfig = outConfigs[imageCapture]!!
- assertThat(imageOutConfig.format).isEqualTo(ImageFormat.JPEG)
- assertThat(imageOutConfig.targets).isEqualTo(IMAGE_CAPTURE)
- // Assert: child2
- val outConfig2 = outConfigs[child2]!!
- assertThat(outConfig2.format).isEqualTo(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
- assertThat(outConfig2.targets).isEqualTo(VIDEO_CAPTURE)
- 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)
- // Assert: surface size, crop rect and transformation propagated to children
- assertThat(child1.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
- assertThat(child2.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
- assertThat(child1.viewPortCropRect).isEqualTo(CROP_RECT)
- assertThat(child2.viewPortCropRect).isEqualTo(CROP_RECT)
- assertThat(child1.sensorToBufferTransformMatrix).isEqualTo(SENSOR_TO_BUFFER)
- assertThat(child2.sensorToBufferTransformMatrix).isEqualTo(SENSOR_TO_BUFFER)
- }
-
- private fun createSurfaceEdge(
- target: Int = PREVIEW,
- format: Int = INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
- streamSpec: StreamSpec = StreamSpec.builder(INPUT_SIZE).build(),
- matrix: Matrix = SENSOR_TO_BUFFER,
- hasCameraTransform: Boolean = true,
- cropRect: Rect = CROP_RECT,
- rotationDegrees: Int = 0,
- mirroring: Boolean = false
- ): SurfaceEdge {
- return SurfaceEdge(
- target,
- format,
- streamSpec,
- matrix,
- hasCameraTransform,
- cropRect,
- rotationDegrees,
- ROTATION_NOT_SPECIFIED,
- mirroring
- ).also { surfaceEdgesToClose.add(it) }
- }
-
- private fun verifyEdge(child: UseCase, isClosed: Boolean, hasProvider: Boolean) {
- assertThat(childrenEdges[child]!!.deferrableSurfaceForTesting.isClosed).isEqualTo(isClosed)
- assertThat(childrenEdges[child]!!.hasProvider()).isEqualTo(hasProvider)
+ fun setRotationDegrees_offsetsParentRotationDegrees() {
+ assertThat(parentCamera.cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(90)
+ virtualCamera.setRotationDegrees(180)
+ assertThat(virtualCamera.cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
+ .isEqualTo(270)
}
}
diff --git a/camera/camera-effects/api/current.txt b/camera/camera-effects/api/current.txt
index e6f50d0..0fa6f1a 100644
--- a/camera/camera-effects/api/current.txt
+++ b/camera/camera-effects/api/current.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package androidx.camera.effects {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract boolean getMirroring();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ }
+
+ @RequiresApi(21) public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-effects/api/restricted_current.txt b/camera/camera-effects/api/restricted_current.txt
index e6f50d0..0fa6f1a 100644
--- a/camera/camera-effects/api/restricted_current.txt
+++ b/camera/camera-effects/api/restricted_current.txt
@@ -1 +1,30 @@
// Signature format: 4.0
+package androidx.camera.effects {
+
+ @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class Frame {
+ ctor public Frame();
+ method public abstract android.graphics.Rect getCropRect();
+ method public abstract boolean getMirroring();
+ method public android.graphics.Canvas getOverlayCanvas();
+ method public abstract int getRotationDegrees();
+ method public abstract android.graphics.Matrix getSensorToBufferTransform();
+ method public abstract android.util.Size getSize();
+ method public abstract long getTimestampNanos();
+ }
+
+ @RequiresApi(21) public class OverlayEffect extends androidx.camera.core.CameraEffect implements java.lang.AutoCloseable {
+ ctor public OverlayEffect(int, int, android.os.Handler, androidx.core.util.Consumer<java.lang.Throwable!>);
+ method public void clearOnDrawListener();
+ method public void close();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> drawFrameAsync(long);
+ method public android.os.Handler getHandler();
+ method public int getQueueDepth();
+ method public void setOnDrawListener(androidx.arch.core.util.Function<androidx.camera.effects.Frame!,java.lang.Boolean!>);
+ field public static final int RESULT_CANCELLED_BY_CALLER = 4; // 0x4
+ field public static final int RESULT_FRAME_NOT_FOUND = 2; // 0x2
+ field public static final int RESULT_INVALID_SURFACE = 3; // 0x3
+ field public static final int RESULT_SUCCESS = 1; // 0x1
+ }
+
+}
+
diff --git a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt
index 8378175..2eca7e1 100644
--- a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt
+++ b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/internal/SurfaceProcessorImplDeviceTest.kt
@@ -242,7 +242,7 @@
val cachedFrame = processor.buffer.frames.single()
// Act: draw the cached frame.
- val drawFuture = processor.drawFrame(cachedFrame.timestampNs)
+ val drawFuture = processor.drawFrameAsync(cachedFrame.timestampNanos)
// Assert: the future completes with RESULT_SUCCESS and the output receives the frame.
assertThat(drawFuture.get()).isEqualTo(OverlayEffect.RESULT_SUCCESS)
@@ -256,7 +256,7 @@
val frame = processor.buffer.frames.single()
// Act: draw the frame with a wrong timestamp.
- val drawFuture = processor.drawFrame(frame.timestampNs - 1)
+ val drawFuture = processor.drawFrameAsync(frame.timestampNanos - 1)
// Assert: the future completes with RESULT_FRAME_NOT_FOUND and the output does not receive
// the frame.
@@ -275,7 +275,7 @@
val frame = processor.buffer.frames.single()
// Act: draw the frame.
- val drawFuture = processor.drawFrame(frame.timestampNs)
+ val drawFuture = processor.drawFrameAsync(frame.timestampNanos)
// Assert: the future completes with RESULT_CANCELLED_BY_CALLER and the output does not
// receive the frame.
@@ -303,7 +303,7 @@
}
// Act: draw the buffered frame.
- val drawFuture = processor.drawFrame(frame.timestampNs)
+ val drawFuture = processor.drawFrameAsync(frame.timestampNanos)
// Assert: the future completes with RESULT_INVALID_SURFACE and the output does not
// receive the frame.
@@ -318,7 +318,7 @@
processor.release()
// Act: release the processor and draw a frame.
- val drawFuture = processor.drawFrame(0)
+ val drawFuture = processor.drawFrameAsync(0)
// Assert: the future completes with an exception.
try {
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java b/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java
index 47c353b..102a961 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/Frame.java
@@ -22,7 +22,6 @@
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraCharacteristics;
import android.util.Size;
import android.view.Surface;
@@ -30,21 +29,20 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageInfo;
+import androidx.camera.core.Preview;
import androidx.camera.core.SurfaceRequest;
import com.google.auto.value.AutoValue;
/**
- * Represents a frame that will be rendered next.
+ * Represents a frame that is about to be rendered.
*
- * <p>This class can be used to overlay graphics or data on camera output. It contains
- * information for drawing an overlay, including sensor-to-buffer transform, size, crop rect,
- * rotation, mirroring, and timestamp. It also provides a {@link Canvas} for the drawing.
- *
- * TODO(b/297509601): Make it public API in 1.4.
+ * <p>Use this class to draw overlay on camera output. It contains a {@link Canvas} for the
+ * drawing. It also provides metadata for positioning the overlay correctly, including
+ * sensor-to-buffer transform, size, crop rect, rotation, mirroring, and timestamp.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
@AutoValue
public abstract class Frame {
@@ -61,12 +59,12 @@
@NonNull
public static Frame of(
@NonNull Surface overlaySurface,
- long timestampNs,
+ long timestampNanos,
@NonNull Size size,
@NonNull SurfaceRequest.TransformationInfo transformationInfo) {
Frame frame = new AutoValue_Frame(transformationInfo.getSensorToBufferTransform(), size,
transformationInfo.getCropRect(), transformationInfo.getRotationDegrees(),
- transformationInfo.getMirroring(), timestampNs);
+ transformationInfo.getMirroring(), timestampNanos);
frame.mOverlaySurface = overlaySurface;
return frame;
}
@@ -75,8 +73,8 @@
* Returns the sensor to image buffer transform matrix.
*
* <p>The value is a mapping from sensor coordinates to buffer coordinates, which is,
- * from the rect of {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE} to the
- * rect defined by {@code (0, 0, #getSize()#getWidth(), #getSize()#getHeight())}.
+ * from the rect of the camera sensor to the rect defined by {@code (0, 0, #getSize()
+ * #getWidth(), #getSize()#getHeight())}.
*
* <p>The value can be set on the {@link Canvas} using {@link Canvas#setMatrix} API. This
* transforms the {@link Canvas} to the sensor coordinate system.
@@ -89,18 +87,20 @@
/**
* Returns the resolution of the frame.
*
+ * <p>This is the size of the input {@link SurfaceTexture} created by the effect.
+ *
* @see SurfaceRequest#getResolution()
*/
@NonNull
public abstract Size getSize();
/**
- * Returns the crop rect rectangle.
+ * Returns the crop rect.
*
- * <p>The value represents how the frame will be cropped by the CameraX pipeline. The crop
- * rectangle specifies the region of valid pixels in the frame, using coordinates from (0, 0)
- * to the (width, height) of {@link #getSize()}. Only the overlay drawn within the bound of
- * the crop rect will be visible to the end users.
+ * <p>The value represents how CameraX intends to crop the input frame. The crop rect specifies
+ * the region of valid pixels in the frame, using coordinates from (0, 0) to the (width,
+ * height) of {@link #getSize()}. Only the overlay drawn within the bound of the crop rect
+ * will be visible to the end users.
*
* <p>The crop rect is applied before the rotating and mirroring. The order of the operations
* is as follows: 1) cropping, 2) rotating and 3) mirroring.
@@ -114,9 +114,10 @@
* Returns the rotation degrees of the frame.
*
* <p>This is a clockwise rotation in degrees that needs to be applied to the frame. The
- * rotation will be determined by {@link CameraCharacteristics} and UseCase configuration.
- * The app must draw the overlay according to the rotation degrees to ensure it is
- * displayed correctly to the end users.
+ * rotation will be determined by camera sensor orientation and UseCase configuration
+ * such as {@link Preview#setTargetRotation}. The app must draw the overlay according to the
+ * rotation degrees to ensure it is displayed correctly to the end users. For example, to
+ * overlay a text, make sure the text's orientation is aligned with the rotation degrees.
*
* <p>The rotation is applied after the cropping but before the mirroring. The order of the
* operations is as follows: 1) cropping, 2) rotating and 3) mirroring.
@@ -128,7 +129,7 @@
/**
* Returns whether the buffer will be mirrored.
*
- * <p>This flag indicates whether the buffer will be mirrored by the pipeline vertically. For
+ * <p>This flag indicates whether the buffer will be mirrored vertically by the pipeline. For
* example, for front camera preview, the buffer is usually mirrored before displayed to end
* users.
*
@@ -142,21 +143,26 @@
/**
* Returns the timestamp of the frame in nanoseconds.
*
+ * <p>This value will match the frames from other streams. For example, for a
+ * {@link ImageAnalysis} output that is originated from the same frame, this value will match
+ * the value of {@link ImageInfo#getTimestamp()}.
+ *
* @see SurfaceTexture#getTimestamp()
* @see ImageInfo#getTimestamp()
*/
- public abstract long getTimestampNs();
+ public abstract long getTimestampNanos();
/**
* Get the canvas for drawing the overlay.
*
* <p>Call this method to get the {@link Canvas} for drawing an overlay on top of the frame.
- * The {@link Canvas} is backed by a {@link SurfaceTexture} with the sizes equals
+ * The {@link Canvas} is backed by a {@link SurfaceTexture} with a size equal to
* {@link #getSize()}. To draw object in camera sensor coordinates, apply
* {@link #getSensorToBufferTransform()} via {@link Canvas#setMatrix(Matrix)} before drawing.
*
- * <p>The caller should only invoke this method when there's a requirement to overlay on the
- * frame. Using this method introduce wait times to synchronize frame updates.
+ * <p>Using this method introduces wait times to synchronize frame updates. The caller should
+ * only invoke this method when it needs to draw overlay. For example, when an object is
+ * detected in the frame.
*/
@NonNull
public Canvas getOverlayCanvas() {
@@ -166,7 +172,6 @@
return mOverlayCanvas;
}
-
/**
* Internal API to check whether the overlay canvas has been drawn into.
*/
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java b/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java
index 642f8e6..f274630 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/OverlayEffect.java
@@ -16,29 +16,65 @@
package androidx.camera.effects;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.arch.core.util.Function;
import androidx.camera.core.CameraEffect;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageInfo;
+import androidx.camera.core.ProcessingException;
+import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.UseCase;
+import androidx.camera.effects.internal.SurfaceProcessorImpl;
+import androidx.core.util.Consumer;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* A {@link CameraEffect} for drawing overlay on top of the camera frames.
- * TODO(b/297509601): Make it public API in 1.4.
+ *
+ * <p>This class manages and processes camera frames with OpenGL. Upon arrival, frames are
+ * enqueued into an array of GL textures for deferred rendering. Calling
+ * {@link #drawFrameAsync(long)} dequeues frames and renders them to the output. Additionally, when
+ * the texture queue reaches its capacity, the oldest frame is automatically dequeued and
+ * rendered. The size of the texture queue can be defined in the constructor.
+ *
+ * <p>The queuing mechanism provides the flexibility to postpone frame rendering until analysis
+ * results are available. For instance, to highlight on a QR code in a preview, one can apply a
+ * QR code detection algorithm using {@link ImageAnalysis}. Once the frame's analysis result
+ * is ready, invoke {@link #drawFrameAsync(long)} and pass in the
+ * {@link ImageInfo#getTimestamp()} to release the frame and draw overlay. If the app
+ * doesn't render real-time analysis results, set the queue depth to 0 to avoid unnecessary
+ * buffer copies. For example, when laying over a static watermark.
+ *
+ * <p>Prior to rendering a frame, the {@link OverlayEffect} invokes the listener set in
+ * {@link #setOnDrawListener(Function)}. This listener provides a {@link Frame} object, which
+ * contains both a {@link Canvas} object for drawing the overlay and the metadata like crop rect,
+ * rotation degrees, etc to calculate how the overlay should be positioned. Once the listener
+ * returns, {@link OverlayEffect} updates the {@link SurfaceTexture} behind the {@link Canvas}
+ * and blends it with the camera frame before rendering to the output.
+ *
+ * <p>This class is thread-safe. The methods can be invoked on any thread. The app provides a
+ * {@link Handler} object in the constructor which is used for listening for Surface updates,
+ * performing OpenGL operations and invoking app provided listeners.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
-public class OverlayEffect {
+public class OverlayEffect extends CameraEffect implements AutoCloseable {
/**
- * {@link #drawFrame(long)} result code
+ * {@link #drawFrameAsync(long)} result code.
*/
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -51,21 +87,20 @@
}
/**
- * The {@link #drawFrame(long)} call was successful. The frame with the exact timestamp was
+ * The {@link #drawFrameAsync(long)} call was successful. The frame with the exact timestamp was
* drawn to the output surface.
*/
public static final int RESULT_SUCCESS = 1;
/**
- * The {@link #drawFrame(long)} call failed because the frame with the exact timestamp was
+ * The {@link #drawFrameAsync(long)} call failed because the frame with the exact timestamp was
* not found in the queue. It could be one of the following reasons:
*
* <ul>
- * <li>the timestamp was incorrect, or
- * <li>the frame was not yet available, or
- * <li>the frame was removed because {@link #drawFrame} had been called with a newer
+ * <li>the timestamp provided was incorrect, or
+ * <li>the frame was removed because {@link #drawFrameAsync} had been called with a newer
* timestamp, or
- * <li>the frame was removed due to the queue is full.
+ * <li>the frame was removed due to the queue being full.
* </ul>
*
* If it's the last case, the caller may avoid this issue by increasing the queue depth.
@@ -73,30 +108,137 @@
public static final int RESULT_FRAME_NOT_FOUND = 2;
/**
- * The {@link #drawFrame(long)} call failed because the output surface is missing, or the
- * output surface no longer matches the frame. It could be because the {@link UseCase}
- * was unbound, causing the original surface to be replaced or disabled.
+ * The {@link #drawFrameAsync(long)} call failed because the output surface is missing, or the
+ * output surface no longer matches the frame. It can happen when the output Surface is
+ * replaced or disabled. For example, when the {@link UseCase} was unbound.
*/
public static final int RESULT_INVALID_SURFACE = 3;
/**
- * The {@link #drawFrame(long)} call failed because the caller cancelled the drawing. This
- * happens when the listener provided via {@link #setOnDrawListener(Function)} returned false.
+ * The {@link #drawFrameAsync(long)} call failed because the caller cancelled the drawing. This
+ * happens when the listener in {@link #setOnDrawListener(Function)} returned false.
*/
public static final int RESULT_CANCELLED_BY_CALLER = 4;
/**
- * TODO(b/297509601): add JavaDoc
+ * Creates an {@link OverlayEffect}.
+ *
+ * @param targets The targets the effect applies to. For example,
+ * {@link CameraEffect#PREVIEW} | {@link CameraEffect#VIDEO_CAPTURE}. See
+ * {@link UseCaseGroup.Builder#addEffect} for supported targets
+ * combinations.
+ * @param queueDepth The depth of the queue. This value indicates how many frames can be
+ * queued before the oldest frame being automatically released.
+ * {@link OverlayEffect} allocates an array of OpenGL 2D textures that
+ * matches this size. The maximum value of the queueDepth depends on the
+ * size of the image and the device capabilities. Set a larger value if
+ * an ImageAnalysis processing takes a long time to produce a result to
+ * be used for overlay, so the frame is not auto-released before the
+ * result is ready. If the queue depth is 0, the input frames are
+ * rendered immediately without queuing.
+ * @param handler The Handler for listening for the input Surface updates and for
+ * performing OpenGL operations.
+ * @param errorListener invoked if the effect runs into unrecoverable errors. The
+ * {@link Throwable} will be the error thrown by this
+ * {@link CameraEffect}. For example, {@link ProcessingException}.
+ * This is invoked on the provided {@param Handler}.
*/
- @NonNull
- public ListenableFuture<Integer> drawFrame(long timestampNs) {
- throw new UnsupportedOperationException("Not implemented yet");
+ public OverlayEffect(int targets, int queueDepth, @NonNull Handler handler,
+ @NonNull Consumer<Throwable> errorListener) {
+ this(targets, new SurfaceProcessorImpl(queueDepth, handler), errorListener);
+ }
+
+ private OverlayEffect(int targets, @NonNull SurfaceProcessorImpl surfaceProcessor,
+ @NonNull Consumer<Throwable> errorListener) {
+ this(targets, surfaceProcessor.getGlExecutor(), surfaceProcessor,
+ errorListener);
+ }
+
+ private OverlayEffect(int targets, @NonNull Executor executor,
+ @NonNull SurfaceProcessor surfaceProcessor,
+ @NonNull Consumer<Throwable> errorListener) {
+ super(targets, executor, surfaceProcessor, errorListener);
}
/**
- * TODO(b/297509601): add JavaDoc
+ * Draws the queued frame with the given timestamp.
+ *
+ * <p>Once invoked, {@link OverlayEffect} retrieves the queued frame with the given timestamp
+ * and draws it to the output Surface. If the frame is successfully drawn,
+ * {@link ListenableFuture} completes with {@link #RESULT_SUCCESS}. Otherwise, it completes
+ * with one of the following results: {@link #RESULT_FRAME_NOT_FOUND},
+ * {@link #RESULT_INVALID_SURFACE} or {@link #RESULT_CANCELLED_BY_CALLER}. If this method is
+ * called after the {@link OverlayEffect} is released, the {@link ListenableFuture} completes
+ * with an {@link IllegalStateException}.
+ *
+ * <p>This method is thread safe. When calling from the {@link #getHandler()} thread, it's
+ * executed right away; otherwise, it posts the execution on the {@link #getHandler()}. It's
+ * recommended to call this method from the {@link #getHandler()} thread to avoid thread
+ * hopping.
+ */
+ @NonNull
+ public ListenableFuture<Integer> drawFrameAsync(long timestampNs) {
+ return getSurfaceProcessorImpl().drawFrameAsync(timestampNs);
+ }
+
+ /**
+ * Sets the listener for drawing overlay.
+ *
+ * <p>Each time before {@link OverlayEffect} draws a frame to the output, the listener
+ * receives a {@link Frame} object, which contains the necessary APIs for drawing overlay.
+ *
+ * <p>To draw an overlay, first call {@link Frame#getOverlayCanvas()} ()} to get a
+ * {@link Canvas} object. The {@link Canvas} object is backed by a {@link SurfaceTexture}
+ * with the size of {@link Frame#getSize()}. {@link Frame#getSensorToBufferTransform()}
+ * represents the mapping from camera sensor coordinates to the frame's coordinates. To draw
+ * objects in the sensor coordinates, call {@link Canvas#setMatrix(Matrix)} with the value of
+ * {@link Frame#getSensorToBufferTransform()}.
+ *
+ * <p>Once the drawing is done, the listener should return true for the {@link OverlayEffect}
+ * to draw it to the output Surface. If it returns false, the frame will be dropped.
+ *
+ * <p>{@link OverlayEffect} invokes the listener on the {@link #getHandler()} thread.
+ *
+ * @see Frame
*/
public void setOnDrawListener(@NonNull Function<Frame, Boolean> onDrawListener) {
- throw new UnsupportedOperationException("Not implemented yet");
+ getSurfaceProcessorImpl().setOnDrawListener(onDrawListener);
+ }
+
+ /**
+ * Clears the listener set in {@link #setOnDrawListener(Function)}.
+ */
+ public void clearOnDrawListener() {
+ getSurfaceProcessorImpl().setOnDrawListener(null);
+ }
+
+ /**
+ * Closes the {@link OverlayEffect}.
+ *
+ * <p>Once closed, the {@link OverlayEffect} can no longer be used.
+ */
+ @Override
+ public void close() {
+ getSurfaceProcessorImpl().release();
+ }
+
+ /**
+ * Gets the depth of the queue set in the constructor.
+ */
+ public int getQueueDepth() {
+ return getSurfaceProcessorImpl().getQueueDepth();
+ }
+
+ /**
+ * Gets the {@link Handler} set in the constructor.
+ */
+ @NonNull
+ public Handler getHandler() {
+ return getSurfaceProcessorImpl().getGlHandler();
+ }
+
+ @NonNull
+ private SurfaceProcessorImpl getSurfaceProcessorImpl() {
+ return (SurfaceProcessorImpl) Objects.requireNonNull(getSurfaceProcessor());
}
}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java
index b85c79f..6422c8f 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/SurfaceProcessorImpl.java
@@ -96,11 +96,14 @@
private boolean mIsReleased = false;
+ private final int mQueueDepth;
+
// Thread and handler for receiving overlay texture updates.
private final HandlerThread mOverlayHandlerThread;
private final Handler mOverlayHandler;
public SurfaceProcessorImpl(int queueDepth, @NonNull Handler glHandler) {
+ mQueueDepth = queueDepth;
mGlHandler = glHandler;
mGlExecutor = CameraXExecutors.newHandlerExecutor(mGlHandler);
mGlRenderer = new GlRenderer(queueDepth);
@@ -267,7 +270,7 @@
* exception.
*/
@NonNull
- public ListenableFuture<Integer> drawFrame(long timestampNs) {
+ public ListenableFuture<Integer> drawFrameAsync(long timestampNs) {
return CallbackToFutureAdapter.getFuture(completer -> {
runOnGlThread(() -> {
if (mIsReleased) {
@@ -286,6 +289,21 @@
});
}
+ /**
+ * Gets the depth of the buffer.
+ */
+ public int getQueueDepth() {
+ return mQueueDepth;
+ }
+
+ /**
+ * Gets the GL handler.
+ */
+ @NonNull
+ public Handler getGlHandler() {
+ return mGlHandler;
+ }
+
// *** Private methods ***
private void runOnGlThread(@NonNull Runnable runnable) {
@@ -331,10 +349,10 @@
return OverlayEffect.RESULT_INVALID_SURFACE;
}
// Only draw if frame is associated with the current output surface.
- if (drawOverlay(frame.getTimestampNs())) {
+ if (drawOverlay(frame.getTimestampNanos())) {
mGlRenderer.renderQueueTextureToSurface(
frame.getTextureId(),
- frame.getTimestampNs(),
+ frame.getTimestampNanos(),
frame.getTransform(),
frame.getSurface());
return OverlayEffect.RESULT_SUCCESS;
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
index d284439..6a1a542 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrame.java
@@ -40,7 +40,7 @@
private final int mTextureId;
- private long mTimestampNs = NO_VALUE;
+ private long mTimestampNanos = NO_VALUE;
@Nullable
private Surface mSurface;
@@ -61,7 +61,7 @@
* with new content.
*/
boolean isEmpty() {
- return mTimestampNs == NO_VALUE;
+ return mTimestampNanos == NO_VALUE;
}
/**
@@ -71,7 +71,7 @@
*/
void markEmpty() {
checkState(!isEmpty(), "Frame is already empty");
- mTimestampNs = NO_VALUE;
+ mTimestampNanos = NO_VALUE;
mSurface = null;
}
@@ -88,7 +88,7 @@
*/
void markFilled(long timestampNs, @NonNull float[] transform, @NonNull Surface surface) {
checkState(isEmpty(), "Frame is already filled");
- mTimestampNs = timestampNs;
+ mTimestampNanos = timestampNs;
System.arraycopy(transform, 0, mTransform, 0, transform.length);
mSurface = surface;
}
@@ -98,8 +98,8 @@
*
* <p>This value is used in {@link GlRenderer#renderQueueTextureToSurface}.
*/
- long getTimestampNs() {
- return mTimestampNs;
+ long getTimestampNanos() {
+ return mTimestampNanos;
}
/**
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
index e75b292..8c5b854 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/internal/TextureFrameBuffer.java
@@ -73,9 +73,9 @@
if (frame.isEmpty()) {
continue;
}
- if (frame.getTimestampNs() == timestampNs) {
+ if (frame.getTimestampNanos() == timestampNs) {
frameToReturn = frame;
- } else if (frame.getTimestampNs() < timestampNs) {
+ } else if (frame.getTimestampNanos() < timestampNs) {
frame.markEmpty();
}
}
@@ -95,8 +95,8 @@
for (TextureFrame frame : mFrames) {
if (frame.isEmpty()) {
return frame;
- } else if (frame.getTimestampNs() < minTimestampNs) {
- minTimestampNs = frame.getTimestampNs();
+ } else if (frame.getTimestampNanos() < minTimestampNs) {
+ minTimestampNs = frame.getTimestampNanos();
oldestFrame = frame;
}
}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
index 9158e9a..c2aeac5 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
@@ -43,7 +43,7 @@
* <li>Rendering a texture in the queue to the output Surface.
* </ul>
*
- * <p>It also allows the caller to upload a bitmap and overlay it when rendering to Surface.
+ * <p>It also allows the caller to overlay a texture when rendering to Surface.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
index 757373c..5f7edda 100644
--- a/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
+++ b/camera/camera-effects/src/test/java/androidx/camera/effects/internal/TextureFrameBufferTest.kt
@@ -92,7 +92,7 @@
// Assert: the frame has the correct values.
assertThat(frame.textureId).isEqualTo(1)
- assertThat(frame.timestampNs).isEqualTo(TIMESTAMP_1)
+ assertThat(frame.timestampNanos).isEqualTo(TIMESTAMP_1)
assertThat(frame.transform.contentEquals(transform1)).isTrue()
assertThat(frame.transform).isNotSameInstanceAs(transform1)
assertThat(frame.surface).isSameInstanceAs(surface1)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 68a27ed..5048cbb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -26,6 +26,7 @@
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraState;
import androidx.camera.core.DynamicRange;
@@ -35,6 +36,7 @@
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.DynamicRanges;
import androidx.camera.core.impl.EncoderProfilesProvider;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.Quirk;
@@ -260,6 +262,30 @@
return mSupportedDynamicRanges;
}
+ /**
+ * Returns the supported dynamic ranges of this camera from a set of candidate dynamic ranges.
+ *
+ * <p>The dynamic ranges which represent what the camera supports will come from the dynamic
+ * ranges set on {@link #setSupportedDynamicRanges(Set)}, or will consist of {@code {SDR}} if
+ * {@code setSupportedDynamicRanges(Set)} has not been called. In order to stay compliant
+ * with the API contract of
+ * {@link androidx.camera.core.CameraInfo#querySupportedDynamicRanges(Set)}, it is
+ * required that the {@link Set} provided to {@code setSupportedDynamicRanges(Set)} should
+ * always contain {@link DynamicRange#SDR} and should never contain under-specified dynamic
+ * ranges, such as {@link DynamicRange#UNSPECIFIED} and
+ * {@link DynamicRange#HDR_UNSPECIFIED_10_BIT}.
+ *
+ * @see androidx.camera.core.CameraInfo#querySupportedDynamicRanges(Set)
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Set<DynamicRange> querySupportedDynamicRanges(
+ @NonNull Set<DynamicRange> candidateDynamicRanges) {
+ return DynamicRanges.findAllPossibleMatches(
+ candidateDynamicRanges, getSupportedDynamicRanges());
+ }
+
@Override
public void addSessionCaptureCallback(@NonNull Executor executor,
@NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceProcessor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceProcessor.java
index c0aed34..d97e4fe 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceProcessor.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceProcessor.java
@@ -43,7 +43,6 @@
private final Executor mExecutor;
private final boolean mAutoCloseSurfaceOutput;
-
@Nullable
private SurfaceRequest mSurfaceRequest;
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
index 561cf39..9d2eb01 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
@@ -16,11 +16,7 @@
package androidx.camera.video;
-import static androidx.camera.core.DynamicRange.BIT_DEPTH_UNSPECIFIED;
-import static androidx.camera.core.DynamicRange.ENCODING_HDR_UNSPECIFIED;
import static androidx.camera.core.DynamicRange.ENCODING_HLG;
-import static androidx.camera.core.DynamicRange.ENCODING_SDR;
-import static androidx.camera.core.DynamicRange.ENCODING_UNSPECIFIED;
import static androidx.camera.core.DynamicRange.SDR;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
import static androidx.camera.video.Quality.getSortedQualities;
@@ -40,6 +36,7 @@
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.DynamicRanges;
import androidx.camera.core.impl.EncoderProfilesProvider;
import androidx.camera.core.impl.EncoderProfilesProxy;
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
@@ -54,7 +51,6 @@
import androidx.camera.video.internal.encoder.VideoEncoderInfo;
import androidx.camera.video.internal.workaround.QualityResolutionModifiedEncoderProfilesProvider;
import androidx.camera.video.internal.workaround.QualityValidatedEncoderProfilesProvider;
-import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.HashMap;
@@ -241,7 +237,7 @@
@Nullable
private CapabilitiesByQuality generateCapabilitiesForNonFullySpecifiedDynamicRange(
@NonNull DynamicRange dynamicRange) {
- if (!canResolve(dynamicRange, getSupportedDynamicRanges())) {
+ if (!DynamicRanges.canResolve(dynamicRange, getSupportedDynamicRanges())) {
return null;
}
@@ -251,56 +247,4 @@
new DynamicRangeMatchedEncoderProfilesProvider(mProfilesProvider, dynamicRange);
return new CapabilitiesByQuality(constrainedProvider);
}
-
- /**
- * Returns {@code true} if the test dynamic range can resolve to the fully specified dynamic
- * range set.
- *
- * <p>A range can resolve if test fields are unspecified and appropriately match the fields
- * of the fully specified dynamic range, or the test fields exactly match the fields of
- * the fully specified dynamic range.
- */
- private static boolean canResolve(@NonNull DynamicRange dynamicRangeToTest,
- @NonNull Set<DynamicRange> fullySpecifiedDynamicRanges) {
- if (dynamicRangeToTest.isFullySpecified()) {
- return fullySpecifiedDynamicRanges.contains(dynamicRangeToTest);
- } else {
- for (DynamicRange fullySpecifiedDynamicRange : fullySpecifiedDynamicRanges) {
- if (canMatchBitDepth(dynamicRangeToTest, fullySpecifiedDynamicRange)
- && canMatchEncoding(dynamicRangeToTest, fullySpecifiedDynamicRange)) {
- return true;
- }
- }
-
- return false;
- }
- }
-
- private static boolean canMatchBitDepth(@NonNull DynamicRange dynamicRangeToTest,
- @NonNull DynamicRange fullySpecifiedDynamicRange) {
- Preconditions.checkState(fullySpecifiedDynamicRange.isFullySpecified(), "Fully specified "
- + "range is not actually fully specified.");
- if (dynamicRangeToTest.getBitDepth() == BIT_DEPTH_UNSPECIFIED) {
- return true;
- }
-
- return dynamicRangeToTest.getBitDepth() == fullySpecifiedDynamicRange.getBitDepth();
- }
-
- private static boolean canMatchEncoding(@NonNull DynamicRange dynamicRangeToTest,
- @NonNull DynamicRange fullySpecifiedDynamicRange) {
- Preconditions.checkState(fullySpecifiedDynamicRange.isFullySpecified(), "Fully specified "
- + "range is not actually fully specified.");
- int encodingToTest = dynamicRangeToTest.getEncoding();
- if (encodingToTest == ENCODING_UNSPECIFIED) {
- return true;
- }
-
- int fullySpecifiedEncoding = fullySpecifiedDynamicRange.getEncoding();
- if (encodingToTest == ENCODING_HDR_UNSPECIFIED && fullySpecifiedEncoding != ENCODING_SDR) {
- return true;
- }
-
- return encodingToTest == fullySpecifiedEncoding;
- }
}
diff --git a/camera/camera-viewfinder-core/api/current.txt b/camera/camera-viewfinder-core/api/current.txt
index e6f50d0..c56eefc 100644
--- a/camera/camera-viewfinder-core/api/current.txt
+++ b/camera/camera-viewfinder-core/api/current.txt
@@ -1 +1,68 @@
// Signature format: 4.0
+package @RequiresApi(21) androidx.camera.viewfinder.surface {
+
+ public enum ImplementationMode {
+ method public static androidx.camera.viewfinder.surface.ImplementationMode valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.camera.viewfinder.surface.ImplementationMode[] values();
+ enum_constant public static final androidx.camera.viewfinder.surface.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.viewfinder.surface.ImplementationMode PERFORMANCE;
+ field public static final androidx.camera.viewfinder.surface.ImplementationMode.Companion Companion;
+ }
+
+ public static final class ImplementationMode.Companion {
+ }
+
+ public interface ViewfinderSurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest request);
+ }
+
+ public final class ViewfinderSurfaceRequest {
+ method public androidx.camera.viewfinder.surface.ImplementationMode? getImplementationMode();
+ method public int getLensFacing();
+ method public android.util.Size getResolution();
+ method public int getSensorOrientation();
+ method public void markSurfaceSafeToRelease();
+ method public void provideSurface(android.view.Surface surface, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result> resultListener);
+ method public boolean willNotProvideSurface();
+ property public final androidx.camera.viewfinder.surface.ImplementationMode? implementationMode;
+ property public final int lensFacing;
+ property public final android.util.Size resolution;
+ property public final int sensorOrientation;
+ field public static final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Companion Companion;
+ }
+
+ public static final class ViewfinderSurfaceRequest.Builder {
+ ctor public ViewfinderSurfaceRequest.Builder(android.util.Size resolution);
+ ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest);
+ ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder builder);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest build();
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.surface.ImplementationMode? implementationMode);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setLensFacing(int lensFacing);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setSensorOrientation(int sensorOrientation);
+ }
+
+ public static final class ViewfinderSurfaceRequest.Companion {
+ }
+
+ @com.google.auto.value.AutoValue public static final class ViewfinderSurfaceRequest.Result {
+ ctor public ViewfinderSurfaceRequest.Result(int code, android.view.Surface surface);
+ method public int component1();
+ method public android.view.Surface component2();
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result copy(int code, android.view.Surface surface);
+ method public int getCode();
+ method public android.view.Surface getSurface();
+ property public final int code;
+ property public final android.view.Surface surface;
+ field public static final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result.Companion Companion;
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ public static final class ViewfinderSurfaceRequest.Result.Companion {
+ }
+
+}
+
diff --git a/camera/camera-viewfinder-core/api/restricted_current.txt b/camera/camera-viewfinder-core/api/restricted_current.txt
index e6f50d0..c56eefc 100644
--- a/camera/camera-viewfinder-core/api/restricted_current.txt
+++ b/camera/camera-viewfinder-core/api/restricted_current.txt
@@ -1 +1,68 @@
// Signature format: 4.0
+package @RequiresApi(21) androidx.camera.viewfinder.surface {
+
+ public enum ImplementationMode {
+ method public static androidx.camera.viewfinder.surface.ImplementationMode valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.camera.viewfinder.surface.ImplementationMode[] values();
+ enum_constant public static final androidx.camera.viewfinder.surface.ImplementationMode COMPATIBLE;
+ enum_constant public static final androidx.camera.viewfinder.surface.ImplementationMode PERFORMANCE;
+ field public static final androidx.camera.viewfinder.surface.ImplementationMode.Companion Companion;
+ }
+
+ public static final class ImplementationMode.Companion {
+ }
+
+ public interface ViewfinderSurfaceProvider {
+ method public void onSurfaceRequested(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest request);
+ }
+
+ public final class ViewfinderSurfaceRequest {
+ method public androidx.camera.viewfinder.surface.ImplementationMode? getImplementationMode();
+ method public int getLensFacing();
+ method public android.util.Size getResolution();
+ method public int getSensorOrientation();
+ method public void markSurfaceSafeToRelease();
+ method public void provideSurface(android.view.Surface surface, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result> resultListener);
+ method public boolean willNotProvideSurface();
+ property public final androidx.camera.viewfinder.surface.ImplementationMode? implementationMode;
+ property public final int lensFacing;
+ property public final android.util.Size resolution;
+ property public final int sensorOrientation;
+ field public static final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Companion Companion;
+ }
+
+ public static final class ViewfinderSurfaceRequest.Builder {
+ ctor public ViewfinderSurfaceRequest.Builder(android.util.Size resolution);
+ ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest);
+ ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder builder);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest build();
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.surface.ImplementationMode? implementationMode);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setLensFacing(int lensFacing);
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder setSensorOrientation(int sensorOrientation);
+ }
+
+ public static final class ViewfinderSurfaceRequest.Companion {
+ }
+
+ @com.google.auto.value.AutoValue public static final class ViewfinderSurfaceRequest.Result {
+ ctor public ViewfinderSurfaceRequest.Result(int code, android.view.Surface surface);
+ method public int component1();
+ method public android.view.Surface component2();
+ method public androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result copy(int code, android.view.Surface surface);
+ method public int getCode();
+ method public android.view.Surface getSurface();
+ property public final int code;
+ property public final android.view.Surface surface;
+ field public static final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result.Companion Companion;
+ field public static final int RESULT_INVALID_SURFACE = 2; // 0x2
+ field public static final int RESULT_REQUEST_CANCELLED = 1; // 0x1
+ field public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3; // 0x3
+ field public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0; // 0x0
+ field public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4; // 0x4
+ }
+
+ public static final class ViewfinderSurfaceRequest.Result.Companion {
+ }
+
+}
+
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/package-info.java b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/package-info.java
new file mode 100644
index 0000000..7b9fae3
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ *
+ */
+@RequiresApi(21)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.viewfinder.impl;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/surface/DeferredSurface.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/surface/DeferredSurface.kt
new file mode 100644
index 0000000..6b27dd9
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/impl/surface/DeferredSurface.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.viewfinder.impl.surface
+
+import android.view.Surface
+import androidx.annotation.GuardedBy
+import androidx.camera.impl.utils.Logger
+import androidx.camera.impl.utils.futures.Futures.nonCancellationPropagating
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A class for creating and tracking use of a [Surface] in an asynchronous manner.
+ */
+abstract class DeferredSurface : AutoCloseable {
+ private val lock = Any()
+ private val terminationFuture: ListenableFuture<Void?>
+
+ @GuardedBy("mLock")
+ private var closed = false
+
+ @GuardedBy("mLock")
+ private var terminationCompleterInternal: CallbackToFutureAdapter.Completer<Void?>? = null
+
+ init {
+ terminationFuture =
+ CallbackToFutureAdapter.getFuture {
+ synchronized(lock) {
+ terminationCompleterInternal = it
+ }
+ "ViewfinderSurface-termination(" + this@DeferredSurface + ")"
+ }
+ }
+
+ fun getSurfaceAsync(): ListenableFuture<Surface> {
+ return provideSurfaceAsync()
+ }
+
+ fun getTerminationFutureAsync(): ListenableFuture<Void?> {
+ return nonCancellationPropagating(terminationFuture)
+ }
+
+ /**
+ * Close the surface.
+ *
+ *
+ * After closing, the underlying surface resources can be safely released by
+ * [SurfaceView] or [TextureView] implementation.
+ */
+ override fun close() {
+ var terminationCompleter: CallbackToFutureAdapter.Completer<Void?>? = null
+ synchronized(lock) {
+ if (!closed) {
+ closed = true
+ terminationCompleter = terminationCompleterInternal
+ terminationCompleterInternal = null
+ Logger.d(
+ TAG,
+ "surface closed, closed=true $this"
+ )
+ }
+ }
+ if (terminationCompleter != null) {
+ terminationCompleter!!.set(null)
+ }
+ }
+
+ protected abstract fun provideSurfaceAsync(): ListenableFuture<Surface>
+
+ /**
+ * The exception that is returned by the ListenableFuture of [getSurfaceAsync] if the
+ * deferrable surface is unable to produce a [Surface].
+ */
+ class SurfaceUnavailableException(message: String) : Exception(message)
+ companion object {
+ private const val TAG = "DeferredSurface"
+ }
+}
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ImplementationMode.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ImplementationMode.kt
new file mode 100644
index 0000000..6afd5e6
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ImplementationMode.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.viewfinder.surface
+
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.annotation.RestrictTo
+
+/**
+ * The implementation mode of a viewfinder.
+ *
+ *
+ * User preference on how the viewfinder should render the viewfinder.
+ * The viewfinder is displayed with either a [SurfaceView] or a
+ * [TextureView]. A [SurfaceView] is generally better than a [TextureView]
+ * when it comes to certain key metrics, including power and latency. On the other hand,
+ * [TextureView] is better supported by a wider range of devices. The option is used to decide what
+ * is the best internal implementation given the device capabilities and user configurations.
+ */
+enum class ImplementationMode(private val id: Int) {
+ /**
+ * Use a [SurfaceView] for the viewfinder when possible. A SurfaceView has somewhat
+ * lower latency and less performance and power overhead than a TextureView. [SurfaceView]
+ * offers more control on a single drawing board, but does not support certain animations.
+ */
+ PERFORMANCE(0),
+
+ /**
+ * Use a [TextureView] for the viewfinder.
+ */
+ COMPATIBLE(1);
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ fun getId(): Int {
+ return id
+ }
+
+ companion object {
+ /**
+ * Convert an Int id to ImplementationMode
+ * @throws IllegalArgumentException if id doesn't below to any ImplementationMode
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @JvmStatic
+ fun fromId(id: Int): ImplementationMode {
+ for (implementationMode in ImplementationMode.values()) {
+ if (implementationMode.id == id) {
+ return implementationMode
+ }
+ }
+ throw IllegalArgumentException("Unknown implementation mode id $id")
+ }
+ }
+}
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceProvider.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceProvider.kt
new file mode 100644
index 0000000..c98d532
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceProvider.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.viewfinder.surface
+
+import android.view.Surface
+
+/**
+ * A interface implemented by the application to provide a [Surface] for viewfinder.
+ *
+ * This interface is implemented by the application to provide a [Surface]. This
+ * will be called by application when it needs a Surface for viewfinder. It also signals when the
+ * Surface is no longer in use.
+ */
+interface ViewfinderSurfaceProvider {
+ /**
+ * Called when a new [Surface] has been requested by the camera.
+ *
+ *
+ * This is called every time a new surface is required to keep the viewfinder running.
+ * The camera may repeatedly request surfaces, but only a single request will be active at a
+ * time.
+ *
+ * @param request the request for a surface which contains the requirements of the
+ * surface and methods for completing the request.
+ */
+ fun onSurfaceRequested(request: ViewfinderSurfaceRequest)
+}
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequest.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequest.kt
new file mode 100644
index 0000000..456280db
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequest.kt
@@ -0,0 +1,631 @@
+/*
+ * 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.viewfinder.surface
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraMetadata
+import android.util.Size
+import android.view.Surface
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.camera.impl.utils.Logger
+import androidx.camera.impl.utils.executor.CameraExecutors
+import androidx.camera.impl.utils.futures.FutureCallback
+import androidx.camera.impl.utils.futures.Futures
+import androidx.camera.viewfinder.impl.surface.DeferredSurface
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.core.util.Consumer
+import androidx.core.util.Preconditions
+import com.google.auto.value.AutoValue
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CancellationException
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * The request to get a [Surface] to display camera feed.
+ *
+ *
+ * This request contains requirements for the surface resolution and camera
+ * device information from [CameraCharacteristics].
+ *
+ * Calling [ViewfinderSurfaceRequest.markSurfaceSafeToRelease] will notify the
+ * surface provider that the surface is not needed and related resources can be released.
+ *
+ * Creates a new surface request with surface resolution, camera device, lens facing and
+ * sensor orientation information.
+ *
+ * @param resolution The requested surface resolution. It is the output surface size
+ * the camera is configured with, instead of {@link CameraViewfinder}
+ * view size.
+ * @param lensFacing The camera lens facing.
+ * @param sensorOrientation THe camera sensor orientation.
+ * @param implementationMode The {@link ImplementationMode} to apply to the viewfinder.
+ */
+class ViewfinderSurfaceRequest internal constructor(
+ val resolution: Size,
+ @LensFacingValue val lensFacing: Int,
+ @SensorOrientationDegreesValue val sensorOrientation: Int,
+ val implementationMode: ImplementationMode?
+) {
+ private val mInternalDeferredSurface: DeferredSurface
+ private val cancellationCompleter: CallbackToFutureAdapter.Completer<Void?>
+ private val sessionStatusFuture: ListenableFuture<Void?>
+ private val surfaceCompleter: CallbackToFutureAdapter.Completer<Surface>
+
+ private val surfaceFutureAsync: ListenableFuture<Surface>
+
+ init {
+ // To ensure concurrency and ordering, operations are chained. Completion can only be
+ // triggered externally by the top-level completer (mSurfaceCompleter). The other future
+ // completers are only completed by callbacks set up within the constructor of this class
+ // to ensure correct ordering of events.
+
+ // Cancellation listener must be called last to ensure the result can be retrieved from
+ // the session listener.
+ val surfaceRequestString =
+ "SurfaceRequest[size: " + resolution + ", id: " + this.hashCode() + "]"
+ val cancellationCompleterRef =
+ AtomicReference<CallbackToFutureAdapter.Completer<Void?>?>(null)
+ val requestCancellationFuture =
+ CallbackToFutureAdapter.getFuture {
+ cancellationCompleterRef.set(it)
+ "$surfaceRequestString-cancellation"
+ }
+ val requestCancellationCompleter =
+ Preconditions.checkNotNull(cancellationCompleterRef.get())
+ cancellationCompleter = requestCancellationCompleter
+
+ // Surface session status future completes and is responsible for finishing the
+ // cancellation listener.
+ val sessionStatusCompleterRef =
+ AtomicReference<CallbackToFutureAdapter.Completer<Void?>?>(null)
+ sessionStatusFuture =
+ CallbackToFutureAdapter.getFuture<Void?> {
+ sessionStatusCompleterRef.set(it)
+ "$surfaceRequestString-status"
+ }
+ Futures.addCallback<Void?>(sessionStatusFuture, object : FutureCallback<Void?> {
+ override fun onSuccess(result: Void?) {
+ // Cancellation didn't occur, so complete the cancellation future. There
+ // shouldn't ever be any standard listeners on this future, so nothing should be
+ // invoked.
+ Preconditions.checkState(requestCancellationCompleter.set(null))
+ }
+
+ override fun onFailure(t: Throwable) {
+ if (t is RequestCancelledException) {
+ // Cancellation occurred. Notify listeners.
+ Preconditions.checkState(
+ requestCancellationFuture.cancel(false)
+ )
+ } else {
+ // Cancellation didn't occur, complete the future so cancellation listeners
+ // are not invoked.
+ Preconditions.checkState(requestCancellationCompleter.set(null))
+ }
+ }
+ }, CameraExecutors.directExecutor())
+
+ // Create the surface future/completer. This will be used to complete the rest of the
+ // future chain and can be set externally via SurfaceRequest methods.
+ val sessionStatusCompleter = Preconditions.checkNotNull(sessionStatusCompleterRef.get())
+ val surfaceCompleterRef =
+ AtomicReference<CallbackToFutureAdapter.Completer<Surface>?>(null)
+ surfaceFutureAsync =
+ CallbackToFutureAdapter.getFuture {
+ surfaceCompleterRef.set(it)
+ "$surfaceRequestString-Surface"
+ }
+ surfaceCompleter = Preconditions.checkNotNull(surfaceCompleterRef.get())
+
+ // Create the viewfinder surface which will be used for communicating when the
+ // camera and consumer are done using the surface. Note this anonymous inner class holds
+ // an implicit reference to the ViewfinderSurfaceRequest. This is by design, and ensures the
+ // ViewfinderSurfaceRequest and all contained future completers will not be garbage
+ // collected as long as the ViewfinderSurface is referenced externally (via
+ // getViewfinderSurface()).
+ mInternalDeferredSurface =
+ object : DeferredSurface() {
+ override fun provideSurfaceAsync(): ListenableFuture<Surface> {
+ Logger.d(
+ TAG,
+ "mInternalViewfinderSurface + $this provideSurface"
+ )
+ return surfaceFutureAsync
+ }
+ }
+ val terminationFuture: ListenableFuture<Void?> =
+ mInternalDeferredSurface.getTerminationFutureAsync()
+
+ // Propagate surface completion to the session future.
+ Futures.addCallback<Surface>(surfaceFutureAsync, object : FutureCallback<Surface?> {
+ override fun onSuccess(result: Surface?) {
+ // On successful setting of a surface, defer completion of the session future to
+ // the ViewfinderSurface termination future. Once that future completes, then it
+ // is safe to release the Surface and associated resources.
+ Futures.propagate(terminationFuture, sessionStatusCompleter)
+ }
+
+ override fun onFailure(t: Throwable) {
+ // Translate cancellation into a SurfaceRequestCancelledException. Other
+ // exceptions mean either the request was completed via willNotProvideSurface() or a
+ // programming error occurred. In either case, the user will never see the
+ // session future (an immediate future will be returned instead), so complete the
+ // future so cancellation listeners are never called.
+ if (t is CancellationException) {
+ Preconditions.checkState(
+ sessionStatusCompleter.setException(
+ RequestCancelledException(
+ "$surfaceRequestString cancelled.", t
+ )
+ )
+ )
+ } else {
+ sessionStatusCompleter.set(null)
+ }
+ }
+ }, CameraExecutors.directExecutor())
+
+ // If the viewfinder surface is terminated, there are two cases:
+ // 1. The surface has not yet been provided to the camera (or marked as 'will not
+ // complete'). Treat this as if the surface request has been cancelled.
+ // 2. The surface was already provided to the camera. In this case the camera is now
+ // finished with the surface, so cancelling the surface future below will be a no-op.
+ terminationFuture.addListener({
+ Logger.d(
+ TAG,
+ ("mInternalViewfinderSurface + " + mInternalDeferredSurface + " " +
+ "terminateFuture triggered")
+ )
+ surfaceFutureAsync.cancel(true)
+ }, CameraExecutors.directExecutor())
+ }
+
+ /**
+ * Closes the viewfinder surface to mark it as safe to release.
+ *
+ *
+ * This method should be called by the user when the requested surface is not needed and
+ * related resources can be released.
+ */
+ fun markSurfaceSafeToRelease() {
+ mInternalDeferredSurface.close()
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ fun getSurface(): DeferredSurface {
+ return mInternalDeferredSurface
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @SuppressLint("PairedRegistration")
+ fun addRequestCancellationListener(
+ executor: Executor,
+ listener: Runnable
+ ) {
+ cancellationCompleter.addCancellationListener(listener, executor)
+ }
+
+ /**
+ * Completes the request for a [Surface] if it has not already been
+ * completed or cancelled.
+ *
+ *
+ * Once the camera no longer needs the provided surface, the `resultListener` will be
+ * invoked with a [Result] containing [Result.RESULT_SURFACE_USED_SUCCESSFULLY].
+ * At this point it is safe to release the surface and any underlying resources. Releasing
+ * the surface before receiving this signal may cause undesired behavior on lower API levels.
+ *
+ *
+ * If the request is cancelled by the camera before successfully attaching the
+ * provided surface to the camera, then the `resultListener` will be invoked with a
+ * [Result] containing [Result.RESULT_REQUEST_CANCELLED].
+ *
+ *
+ * If the request was previously completed via [.willNotProvideSurface], then
+ * `resultListener` will be invoked with a [Result] containing
+ * [Result.RESULT_WILL_NOT_PROVIDE_SURFACE].
+ *
+ *
+ * Upon returning from this method, the surface request is guaranteed to be complete.
+ * However, only the `resultListener` provided to the first invocation of this method
+ * should be used to track when the provided [Surface] is no longer in use by the
+ * camera, as subsequent invocations will always invoke the `resultListener` with a
+ * [Result] containing [Result.RESULT_SURFACE_ALREADY_PROVIDED].
+ *
+ * @param surface The surface which will complete the request.
+ * @param executor Executor used to execute the `resultListener`.
+ * @param resultListener Listener used to track how the surface is used by the camera in
+ * response to being provided by this method.
+ */
+ fun provideSurface(
+ surface: Surface,
+ executor: Executor,
+ resultListener: Consumer<Result?>
+ ) {
+ if (surfaceCompleter.set(surface) || surfaceFutureAsync.isCancelled) {
+ // Session will be pending completion (or surface request was cancelled). Return the
+ // session future.
+ Futures.addCallback(sessionStatusFuture, object : FutureCallback<Void?> {
+ override fun onSuccess(result: Void?) {
+ resultListener.accept(
+ Result(
+ Result.RESULT_SURFACE_USED_SUCCESSFULLY,
+ surface
+ )
+ )
+ }
+
+ override fun onFailure(t: Throwable) {
+ Preconditions.checkState(
+ t is RequestCancelledException, ("Camera " +
+ "surface session should only fail with request " +
+ "cancellation. Instead failed due to:\n" + t)
+ )
+ resultListener.accept(Result(Result.RESULT_REQUEST_CANCELLED, surface))
+ }
+ }, executor)
+ } else {
+ // Surface request is already complete
+ Preconditions.checkState(surfaceFutureAsync.isDone)
+ try {
+ surfaceFutureAsync.get()
+ // Getting this far means the surface was already provided.
+ executor.execute {
+ resultListener.accept(
+ Result(
+ Result.RESULT_SURFACE_ALREADY_PROVIDED,
+ surface
+ )
+ )
+ }
+ } catch (e: InterruptedException) {
+ executor.execute {
+ resultListener.accept(
+ Result(
+ Result.RESULT_WILL_NOT_PROVIDE_SURFACE,
+ surface
+ )
+ )
+ }
+ } catch (e: ExecutionException) {
+ executor.execute {
+ resultListener.accept(
+ Result(
+ Result.RESULT_WILL_NOT_PROVIDE_SURFACE,
+ surface
+ )
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Signals that the request will never be fulfilled.
+ *
+ *
+ * This may be called in the case where the application may be shutting down and a
+ * surface will never be produced to fulfill the request.
+ *
+ *
+ * This will be called by CameraViewfinder as soon as it is known that the request will not
+ * be fulfilled. Failure to complete the SurfaceRequest via `willNotProvideSurface()`
+ * or [.provideSurface] may cause long delays in shutting
+ * down the camera.
+ *
+ *
+ * Upon returning from this method, the request is guaranteed to be complete, regardless
+ * of the return value. If the request was previously successfully completed by
+ * [.provideSurface], invoking this method will return
+ * `false`, and will have no effect on how the surface is used by the camera.
+ *
+ * @return `true` if this call to `willNotProvideSurface()` successfully
+ * completes the request, i.e., the request has not already been completed via
+ * [.provideSurface] or by a previous call to
+ * `willNotProvideSurface()` and has not already been cancelled by the camera.
+ */
+ fun willNotProvideSurface(): Boolean {
+ return surfaceCompleter.setException(
+ DeferredSurface.SurfaceUnavailableException(
+ ("Surface request will not complete.")
+ )
+ )
+ }
+
+ /**
+ * Builder for [ViewfinderSurfaceRequest].
+ */
+ class Builder {
+ private val resolution: Size
+
+ @LensFacingValue
+ private var lensFacing = CameraMetadata.LENS_FACING_BACK
+
+ @SensorOrientationDegreesValue
+ private var sensorOrientation = 0
+ private var implementationMode: ImplementationMode? = null
+
+ /**
+ * Constructor for [Builder].
+ *
+ *
+ * Creates a builder with viewfinder resolution.
+ *
+ * @param resolution viewfinder resolution.
+ */
+ constructor(resolution: Size) {
+ this.resolution = resolution
+ }
+
+ /**
+ * Constructor for [Builder].
+ *
+ *
+ * Creates a builder with other builder instance. The returned builder will be
+ * pre-populated with the state of the provided builder.
+ *
+ * @param builder [Builder] instance.
+ */
+ constructor(builder: Builder) {
+ resolution = builder.resolution
+ implementationMode = builder.implementationMode
+ lensFacing = builder.lensFacing
+ sensorOrientation = builder.sensorOrientation
+ }
+
+ /**
+ * Constructor for [Builder].
+ *
+ *
+ * Creates a builder with other [ViewfinderSurfaceRequest] instance. The
+ * returned builder will be pre-populated with the state of the provided
+ * [ViewfinderSurfaceRequest] instance.
+ *
+ * @param surfaceRequest [ViewfinderSurfaceRequest] instance.
+ */
+ constructor(surfaceRequest: ViewfinderSurfaceRequest) {
+ resolution = surfaceRequest.resolution
+ implementationMode = surfaceRequest.implementationMode
+ lensFacing = surfaceRequest.lensFacing
+ sensorOrientation = surfaceRequest.sensorOrientation
+ }
+
+ /**
+ * Sets the [ImplementationMode].
+ *
+ *
+ * **Possible values:**
+ *
+ * * [PERFORMANCE][ImplementationMode.PERFORMANCE]
+ * * [COMPATIBLE][ImplementationMode.COMPATIBLE]
+ *
+ * @param implementationMode The [ImplementationMode].
+ * @return This builder.
+ */
+ fun setImplementationMode(implementationMode: ImplementationMode?): Builder {
+ this.implementationMode = implementationMode
+ return this
+ }
+
+ /**
+ * Sets the lens facing.
+ *
+ *
+ * **Possible values:**
+ *
+ * * [FRONT][CameraMetadata.LENS_FACING_FRONT]
+ * * [BACK][CameraMetadata.LENS_FACING_BACK]
+ * * [EXTERNAL][CameraMetadata.LENS_FACING_EXTERNAL]
+ *
+ *
+ *
+ * The value can be retrieved from [CameraCharacteristics] by key
+ * [CameraCharacteristics.LENS_FACING]. If not set,
+ * [CameraMetadata.LENS_FACING_BACK] will be used by default.
+ *
+ * @param lensFacing The lens facing.
+ * @return This builder.
+ */
+ fun setLensFacing(@LensFacingValue lensFacing: Int): Builder {
+ this.lensFacing = lensFacing
+ return this
+ }
+
+ /**
+ * Sets the sensor orientation.
+ *
+ *
+ * **Range of valid values:**<br></br>
+ * 0, 90, 180, 270
+ *
+ *
+ * The value can be retrieved from [CameraCharacteristics] by key
+ * [CameraCharacteristics.SENSOR_ORIENTATION]. If it is not
+ * set, 0 will be used by default.
+ *
+ * @param sensorOrientation
+ * @return this builder.
+ */
+ fun setSensorOrientation(@SensorOrientationDegreesValue sensorOrientation: Int): Builder {
+ this.sensorOrientation = sensorOrientation
+ return this
+ }
+
+ /**
+ * Builds the [ViewfinderSurfaceRequest].
+ * @return the instance of [ViewfinderSurfaceRequest].
+ *
+ * @throws IllegalArgumentException
+ */
+ fun build(): ViewfinderSurfaceRequest {
+ if ((lensFacing != CameraMetadata.LENS_FACING_FRONT
+ ) && (lensFacing != CameraMetadata.LENS_FACING_BACK
+ ) && (lensFacing != CameraMetadata.LENS_FACING_EXTERNAL)
+ ) {
+ throw IllegalArgumentException(
+ ("Lens facing value: $lensFacing is invalid")
+ )
+ }
+ if ((sensorOrientation != 0
+ ) && (sensorOrientation != 90
+ ) && (sensorOrientation != 180
+ ) && (sensorOrientation != 270)
+ ) {
+ throw IllegalArgumentException(
+ ("Sensor orientation value: $sensorOrientation is invalid")
+ )
+ }
+ return ViewfinderSurfaceRequest(
+ resolution,
+ lensFacing,
+ sensorOrientation,
+ implementationMode
+ )
+ }
+ }
+
+ internal class RequestCancelledException(message: String, cause: Throwable) :
+ RuntimeException(message, cause)
+
+ /**
+ * Result of providing a surface to a [ViewfinderSurfaceRequest] via
+ * [.provideSurface].
+ *
+ * Can be used to compare to results returned to `resultListener` in
+ * [.provideSurface].
+ *
+ * @param code One of [.RESULT_SURFACE_USED_SUCCESSFULLY],
+ * [.RESULT_REQUEST_CANCELLED], [.RESULT_INVALID_SURFACE],
+ * [.RESULT_SURFACE_ALREADY_PROVIDED], or
+ * [.RESULT_WILL_NOT_PROVIDE_SURFACE].
+ * @param surface The [Surface] used to complete the [ViewfinderSurfaceRequest].
+ */
+ @AutoValue
+ data class Result(@ResultCode val code: Int, val surface: Surface) {
+ /**
+ * Possible result codes.
+ *
+ */
+ @IntDef(
+ RESULT_SURFACE_USED_SUCCESSFULLY,
+ RESULT_REQUEST_CANCELLED,
+ RESULT_INVALID_SURFACE,
+ RESULT_SURFACE_ALREADY_PROVIDED,
+ RESULT_WILL_NOT_PROVIDE_SURFACE
+ )
+ @Retention(
+ AnnotationRetention.SOURCE
+ )
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ annotation class ResultCode()
+ companion object {
+ /**
+ * Provided surface was successfully used by the camera and eventually detached once no
+ * longer needed by the camera.
+ *
+ *
+ * This result denotes that it is safe to release the [Surface] and any underlying
+ * resources.
+ *
+ *
+ * For compatibility reasons, the [Surface] object should not be reused by
+ * future [SurfaceRequests][ViewfinderSurfaceRequest], and a new surface should be
+ * created instead.
+ */
+ const val RESULT_SURFACE_USED_SUCCESSFULLY = 0
+
+ /**
+ * Provided surface was never attached to the camera due to the
+ * [ViewfinderSurfaceRequest] being cancelled by the camera.
+ *
+ *
+ * It is safe to release or reuse [Surface], assuming it was not previously
+ * attached to a camera via [.provideSurface]. If
+ * reusing the surface for a future surface request, it should be verified that the
+ * surface still matches the resolution specified by
+ * [ViewfinderSurfaceRequest.resolution].
+ */
+ const val RESULT_REQUEST_CANCELLED = 1
+
+ /**
+ * Provided surface could not be used by the camera.
+ *
+ *
+ * This is likely due to the [Surface] being closed prematurely or the resolution
+ * of the surface not matching the resolution specified by
+ * [ViewfinderSurfaceRequest.resolution].
+ */
+ const val RESULT_INVALID_SURFACE = 2
+
+ /**
+ * Surface was not attached to the camera through this invocation of
+ * [.provideSurface] due to the
+ * [ViewfinderSurfaceRequest] already being complete with a surface.
+ *
+ *
+ * The [ViewfinderSurfaceRequest] has already been completed by a previous
+ * invocation of [.provideSurface].
+ *
+ *
+ * It is safe to release or reuse the [Surface], assuming it was not previously
+ * attached to a camera via [.provideSurface].
+ */
+ const val RESULT_SURFACE_ALREADY_PROVIDED = 3
+
+ /**
+ * Surface was not attached to the camera through this invocation of
+ * [.provideSurface] due to the
+ * [ViewfinderSurfaceRequest] already being marked as "will not provide surface".
+ *
+ *
+ * The [ViewfinderSurfaceRequest] has already been marked as 'will not provide
+ * surface' by a previous invocation of [.willNotProvideSurface].
+ *
+ *
+ * It is safe to release or reuse the [Surface], assuming it was not previously
+ * attached to a camera via [.provideSurface].
+ */
+ const val RESULT_WILL_NOT_PROVIDE_SURFACE = 4
+ }
+ }
+
+ /**
+ * Valid integer sensor orientation degrees values.
+ */
+ @IntDef(0, 90, 180, 270)
+ @Retention(AnnotationRetention.SOURCE)
+ internal annotation class SensorOrientationDegreesValue()
+
+ /**
+ * Valid integer sensor orientation degrees values.
+ */
+ @IntDef(
+ CameraMetadata.LENS_FACING_FRONT,
+ CameraMetadata.LENS_FACING_BACK,
+ CameraMetadata.LENS_FACING_EXTERNAL
+ )
+ @Retention(
+ AnnotationRetention.SOURCE
+ )
+ internal annotation class LensFacingValue()
+ companion object {
+ private const val TAG = "SurfaceRequest"
+ }
+}
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/package-info.java b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/package-info.java
new file mode 100644
index 0000000..e8f0a15
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ *
+ */
+@RequiresApi(21)
+package androidx.camera.viewfinder.surface;
+
+import androidx.annotation.RequiresApi;
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraInfoDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraInfoDeviceTest.kt
new file mode 100644
index 0000000..e73da7a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraInfoDeviceTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.integration.core
+
+import android.content.Context
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.DynamicRange
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.CoreAppTestUtil
+import androidx.concurrent.futures.await
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class CameraInfoDeviceTest(
+ private val implName: String,
+ private val cameraXConfig: CameraXConfig
+) {
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(
+ if (implName == Camera2Config::class.simpleName) {
+ Camera2Config.defaultConfig()
+ } else {
+ CameraPipeConfig.defaultConfig()
+ }
+ )
+ )
+
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ )
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var cameraProvider: ProcessCameraProvider
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
+ @Before
+ fun setUp() = runBlocking {
+ assumeTrue(CameraUtil.deviceHasCamera())
+ CoreAppTestUtil.assumeCompatibleDevice()
+
+ withTimeout(10000) {
+ ProcessCameraProvider.configureInstance(cameraXConfig)
+ cameraProvider = ProcessCameraProvider.getInstance(context).await()
+ }
+ }
+
+ @After
+ fun tearDown() {
+ if (::cameraProvider.isInitialized) {
+ cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+ }
+ }
+
+ @Test
+ fun allCamerasAdvertiseSdr() {
+ cameraProvider.availableCameraInfos.forEach { cameraInfo ->
+ assertThat(cameraInfo.querySupportedDynamicRanges(setOf(DynamicRange.UNSPECIFIED)))
+ .contains(DynamicRange.SDR)
+ }
+ }
+
+ @Test
+ fun underSpecifiedDynamicRange_neverReturnedFromQuery() {
+ cameraProvider.availableCameraInfos.forEach { cameraInfo ->
+ cameraInfo.querySupportedDynamicRanges(setOf(DynamicRange.UNSPECIFIED)).forEach {
+ assertWithMessage(
+ "$cameraInfo advertises under-specified dynamic range: $it"
+ ).that(
+ it.isFullySpecified
+ ).isTrue()
+ }
+ }
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index e89bff2..0bde846 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -345,8 +345,10 @@
private OpenGLRenderer mPreviewRenderer;
private DisplayManager.DisplayListener mDisplayListener;
private RecordUi mRecordUi;
+ private DynamicRangeUi mDynamicRangeUi;
private Quality mVideoQuality;
private DynamicRange mDynamicRange = DynamicRange.SDR;
+ private Set<DynamicRange> mDisplaySupportedHighDynamicRanges = Collections.emptySet();
private final Set<DynamicRange> mSelectableDynamicRanges = new HashSet<>();
private int mVideoMirrorMode = MIRROR_MODE_ON_FRONT_ONLY;
private boolean mIsPreviewStabilizationOn = false;
@@ -716,42 +718,6 @@
});
// Final reference to this record UI
- mRecordUi.getButtonDynamicRange().setText(getDynamicRangeIconName(mDynamicRange));
- mRecordUi.getButtonDynamicRange().setOnClickListener(view -> {
- PopupMenu popup = new PopupMenu(this, view);
- Menu menu = popup.getMenu();
-
- final int groupId = Menu.NONE;
- for (DynamicRange dynamicRange : mSelectableDynamicRanges) {
- int itemId = dynamicRangeToItemId(dynamicRange);
- menu.add(groupId, itemId, itemId, getDynamicRangeMenuItemName(dynamicRange));
- if (Objects.equals(dynamicRange, mDynamicRange)) {
- // Apply the checked item for the selected dynamic range to the menu.
- menu.findItem(itemId).setChecked(true);
- }
- }
-
- // Make menu single checkable
- menu.setGroupCheckable(groupId, true, true);
-
- popup.setOnMenuItemClickListener(item -> {
- DynamicRange dynamicRange = itemIdToDynamicRange(item.getItemId());
- if (!Objects.equals(dynamicRange, mDynamicRange)) {
- mDynamicRange = dynamicRange;
- if (Build.VERSION.SDK_INT >= 26) {
- updateWindowColorMode();
- }
- mRecordUi.getButtonDynamicRange()
- .setText(getDynamicRangeIconName(mDynamicRange));
- // Dynamic range changed, rebind UseCases
- tryBindUseCases();
- }
- return true;
- });
-
- popup.show();
- });
-
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality));
mRecordUi.getButtonQuality().setOnClickListener(view -> {
PopupMenu popup = new PopupMenu(this, view);
@@ -799,6 +765,47 @@
});
}
+ private void setUpDynamicRangeButton() {
+ mDynamicRangeUi.setDisplayedDynamicRange(mDynamicRange);
+ mDynamicRangeUi.getButton().setOnClickListener(view -> {
+ PopupMenu popup = new PopupMenu(this, view);
+ Menu menu = popup.getMenu();
+
+ final int groupId = Menu.NONE;
+ for (DynamicRange dynamicRange : mSelectableDynamicRanges) {
+ int itemId = dynamicRangeToItemId(dynamicRange);
+ menu.add(groupId, itemId, itemId, getDynamicRangeMenuItemName(dynamicRange));
+ if (Objects.equals(dynamicRange, mDynamicRange)) {
+ // Apply the checked item for the selected dynamic range to the menu.
+ menu.findItem(itemId).setChecked(true);
+ }
+ }
+
+ // Make menu single checkable
+ menu.setGroupCheckable(groupId, true, true);
+
+ popup.setOnMenuItemClickListener(item -> {
+ DynamicRange dynamicRange = itemIdToDynamicRange(item.getItemId());
+ if (!Objects.equals(dynamicRange, mDynamicRange)) {
+ setSelectedDynamicRange(dynamicRange);
+ // Dynamic range changed, rebind UseCases
+ tryBindUseCases();
+ }
+ return true;
+ });
+
+ popup.show();
+ });
+ }
+
+ private void setSelectedDynamicRange(@NonNull DynamicRange dynamicRange) {
+ mDynamicRange = dynamicRange;
+ if (Build.VERSION.SDK_INT >= 26) {
+ updateWindowColorMode();
+ }
+ mDynamicRangeUi.setDisplayedDynamicRange(mDynamicRange);
+ }
+
@RequiresApi(26)
private void updateWindowColorMode() {
int colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
@@ -1155,6 +1162,7 @@
mPhotoToggle.setVisibility(View.GONE);
mPreviewToggle.setVisibility(View.GONE);
mAnalysisToggle.setVisibility(View.GONE);
+ mDynamicRangeUi.getButton().setVisibility(View.GONE);
mRecordUi.hideUi();
if (!testCase.equals(SWITCH_TEST_CASE)) {
mCameraDirectionButton.setVisibility(View.GONE);
@@ -1190,10 +1198,30 @@
}
}
+ private void updateDynamicRangeUiState() {
+ // Only show dynamic range if video or preview are enabled
+ boolean visible = (mVideoToggle.isChecked() || mPreviewToggle.isChecked());
+ // Dynamic range is configurable if it's visible, there's more than 1 choice, and there
+ // isn't a recording in progress
+ boolean configurable = visible
+ && mSelectableDynamicRanges.size() > 1
+ && mRecordUi.getState() != RecordUi.State.RECORDING;
+
+ if (configurable) {
+ mDynamicRangeUi.setState(DynamicRangeUi.State.CONFIGURABLE);
+ } else if (visible) {
+ mDynamicRangeUi.setState(DynamicRangeUi.State.VISIBLE);
+ } else {
+ mDynamicRangeUi.setState(DynamicRangeUi.State.HIDDEN);
+ }
+ }
+
@SuppressLint({"NullAnnotationGroup", "RestrictedApiAndroidX"})
@OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
private void updateButtonsUi() {
mRecordUi.setEnabled(mVideoToggle.isChecked());
+ updateDynamicRangeUiState();
+
mTakePicture.setEnabled(mPhotoToggle.isChecked());
mCaptureQualityToggle.setEnabled(mPhotoToggle.isChecked());
mZslToggle.setVisibility(getCameraInfo() != null
@@ -1276,6 +1304,7 @@
mPreviewToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
setUpRecordButton();
+ setUpDynamicRangeButton();
setUpFlashButton();
setUpTakePictureButton();
setUpCameraDirectionButton();
@@ -1352,12 +1381,14 @@
setContentView(R.layout.activity_camera_xmain);
mImageCaptureExecutorService = Executors.newSingleThreadExecutor();
- Display display = null;
+ mDisplaySupportedHighDynamicRanges = Collections.emptySet();
if (Build.VERSION.SDK_INT >= 30) {
- display = OpenGLActivity.Api30Impl.getDisplay(this);
+ Display display = OpenGLActivity.Api30Impl.getDisplay(this);
+ mDisplaySupportedHighDynamicRanges =
+ OpenGLActivity.getHighDynamicRangesSupportedByDisplay(display);
}
OpenGLRenderer previewRenderer = mPreviewRenderer =
- new OpenGLRenderer(OpenGLActivity.getHdrEncodingsSupportedByDisplay(display));
+ new OpenGLRenderer(mDisplaySupportedHighDynamicRanges);
ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
updatePreviewRatioAndScaleTypeByIntent(viewFinderStub);
updateVideoMirrorModeByIntent(getIntent());
@@ -1391,13 +1422,14 @@
mZoomResetToggle = findViewById(R.id.zoom_reset_toggle);
mTextView = findViewById(R.id.textView);
+ mDynamicRangeUi = new DynamicRangeUi(findViewById(R.id.dynamic_range));
mRecordUi = new RecordUi(
findViewById(R.id.Video),
findViewById(R.id.video_pause),
findViewById(R.id.video_stats),
findViewById(R.id.video_quality),
findViewById(R.id.video_persistent),
- findViewById(R.id.video_dynamic_range)
+ (newState) -> updateDynamicRangeUiState()
);
setUpButtonEvents();
@@ -1631,8 +1663,7 @@
// Reset video quality to avoid always fail by quality too large.
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality = QUALITY_AUTO));
// Reset video dynamic range to avoid failure
- mRecordUi.getButtonDynamicRange().setText(
- getDynamicRangeIconName(mDynamicRange = DynamicRange.SDR));
+ setSelectedDynamicRange(DynamicRange.SDR);
reduceUseCaseToFindSupportedCombination();
@@ -1694,11 +1725,18 @@
@SuppressLint("RestrictedApiAndroidX")
private List<UseCase> buildUseCases() {
List<UseCase> useCases = new ArrayList<>();
+ if (mVideoToggle.isChecked() || mPreviewToggle.isChecked()) {
+ // Update possible dynamic ranges for current camera
+ updateDynamicRangeConfiguration();
+ }
+
if (mPreviewToggle.isChecked()) {
Preview preview = new Preview.Builder()
.setTargetName("Preview")
.setTargetAspectRatio(mTargetAspectRatio)
.setPreviewStabilizationEnabled(mIsPreviewStabilizationOn)
+ .setDynamicRange(
+ mVideoToggle.isChecked() ? DynamicRange.UNSPECIFIED : mDynamicRange)
.build();
resetViewIdlingResource();
// Use the listener of the future to make sure the Preview setup the new surface.
@@ -1732,9 +1770,6 @@
}
if (mVideoToggle.isChecked()) {
- // Update possible dynamic ranges for current camera
- updateDynamicRangeConfiguration();
-
// Recreate the Recorder except there's a running persistent recording, existing
// Recorder. We may later consider reuse the Recorder everytime if the quality didn't
// change.
@@ -1756,29 +1791,43 @@
return useCases;
}
+ @SuppressLint("RestrictedApiAndroidX")
private void updateDynamicRangeConfiguration() {
mSelectableDynamicRanges.clear();
- // Get the list of available dynamic ranges for the current quality
- VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(
- mCamera.getCameraInfo());
- Set<DynamicRange> supportedDynamicRanges =
- videoCapabilities.getSupportedDynamicRanges();
+ Set<DynamicRange> supportedDynamicRanges = Collections.singleton(DynamicRange.SDR);
+ // ImageCapture and ImageAnalysis currently only support SDR, so only update supported
+ // ranges if they're not enabled
+ if (!mAnalysisToggle.isChecked() && !mPhotoToggle.isChecked()) {
+ if (mVideoToggle.isChecked()) {
+ // Get the list of available dynamic ranges for the current quality
+ VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(
+ mCamera.getCameraInfo());
+ supportedDynamicRanges = videoCapabilities.getSupportedDynamicRanges();
+ } else if (mPreviewToggle.isChecked()) {
+ supportedDynamicRanges = new HashSet<>();
+ // Add SDR as its always available
+ supportedDynamicRanges.add(DynamicRange.SDR);
+
+ // Add all HDR dynamic ranges supported by the display
+ Set<DynamicRange> queryResult = mCamera.getCameraInfo()
+ .querySupportedDynamicRanges(
+ Collections.singleton(DynamicRange.UNSPECIFIED));
+ supportedDynamicRanges.addAll(queryResult);
+ }
+ }
if (supportedDynamicRanges.size() > 1) {
- mRecordUi.setDynamicRangeConfigurable(true);
if (hasTenBitDynamicRange(supportedDynamicRanges)) {
mSelectableDynamicRanges.add(DynamicRange.HDR_UNSPECIFIED_10_BIT);
}
- } else {
- mRecordUi.setDynamicRangeConfigurable(false);
}
mSelectableDynamicRanges.addAll(supportedDynamicRanges);
// In case the previous dynamic range held in mDynamicRange isn't supported, reset
// to SDR.
if (!mSelectableDynamicRanges.contains(mDynamicRange)) {
- mDynamicRange = DynamicRange.SDR;
+ setSelectedDynamicRange(DynamicRange.SDR);
}
}
@@ -2064,6 +2113,65 @@
}
}
+ private static class DynamicRangeUi {
+
+ enum State {
+ // Button can be selected to choose dynamic range
+ CONFIGURABLE,
+ // Button is visible, but cannot be selected
+ VISIBLE,
+ // Button is not visible, cannot be selected
+ HIDDEN
+ }
+
+ private State mState = State.HIDDEN;
+
+ private final Button mButtonDynamicRange;
+
+ DynamicRangeUi(@NonNull Button buttonDynamicRange) {
+ mButtonDynamicRange = buttonDynamicRange;
+ }
+
+ void setState(@NonNull State newState) {
+ if (newState != mState) {
+ mState = newState;
+ switch (newState) {
+ case HIDDEN: {
+ mButtonDynamicRange.setEnabled(false);
+ mButtonDynamicRange.setVisibility(View.INVISIBLE);
+ break;
+ }
+ case VISIBLE: {
+ mButtonDynamicRange.setEnabled(false);
+ mButtonDynamicRange.setVisibility(View.VISIBLE);
+ break;
+ }
+ case CONFIGURABLE: {
+ mButtonDynamicRange.setEnabled(true);
+ mButtonDynamicRange.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+ }
+ }
+
+ @NonNull
+ Button getButton() {
+ return mButtonDynamicRange;
+ }
+
+ void setDisplayedDynamicRange(@NonNull DynamicRange dynamicRange) {
+ int resId = R.string.toggle_video_dyn_rng_unknown;
+ for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) {
+ if (Objects.equals(dynamicRange, uiData.mDynamicRange)) {
+ resId = uiData.mToggleLabelRes;
+ break;
+ }
+ }
+ mButtonDynamicRange.setText(resId);
+ }
+ }
+
@UiThread
private static class RecordUi {
@@ -2076,22 +2184,20 @@
private final TextView mTextStats;
private final Button mButtonQuality;
private final ToggleButton mButtonPersistent;
- private final Button mButtonDynamicRange;
- private boolean mDynamicRangeConfigurable = false;
-
private boolean mEnabled = false;
private State mState = State.IDLE;
+ private final Consumer<State> mNewStateConsumer;
RecordUi(@NonNull Button buttonRecord, @NonNull Button buttonPause,
@NonNull TextView textStats, @NonNull Button buttonQuality,
@NonNull ToggleButton buttonPersistent,
- @NonNull Button buttonDynamicRange) {
+ @NonNull Consumer<State> onNewState) {
mButtonRecord = buttonRecord;
mButtonPause = buttonPause;
mTextStats = textStats;
mButtonQuality = buttonQuality;
mButtonPersistent = buttonPersistent;
- mButtonDynamicRange = buttonDynamicRange;
+ mNewStateConsumer = onNewState;
}
void setEnabled(boolean enabled) {
@@ -2101,22 +2207,23 @@
mTextStats.setVisibility(View.VISIBLE);
mButtonQuality.setVisibility(View.VISIBLE);
mButtonPersistent.setVisibility(View.VISIBLE);
- mButtonDynamicRange.setVisibility(View.VISIBLE);
updateUi();
} else {
mButtonRecord.setText("Record");
mButtonRecord.setEnabled(false);
mButtonPause.setVisibility(View.INVISIBLE);
mButtonQuality.setVisibility(View.INVISIBLE);
- mButtonDynamicRange.setVisibility(View.INVISIBLE);
mTextStats.setVisibility(View.GONE);
mButtonPersistent.setVisibility(View.INVISIBLE);
}
}
void setState(@NonNull State state) {
- mState = state;
- updateUi();
+ if (state != mState) {
+ mState = state;
+ updateUi();
+ mNewStateConsumer.accept(state);
+ }
}
@NonNull
@@ -2131,14 +2238,6 @@
mButtonPersistent.setVisibility(View.GONE);
}
- private void setDynamicRangeConfigurable(boolean configurable) {
- if (configurable != mDynamicRangeConfigurable) {
- mDynamicRangeConfigurable = configurable;
- boolean buttonEnabled = mButtonDynamicRange.isEnabled();
- mButtonDynamicRange.setEnabled(buttonEnabled && mDynamicRangeConfigurable);
- }
- }
-
private void updateUi() {
if (!mEnabled) {
return;
@@ -2151,7 +2250,6 @@
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(true);
mButtonQuality.setEnabled(true);
- mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
case RECORDING:
mButtonRecord.setText("Stop");
@@ -2160,7 +2258,6 @@
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(false);
- mButtonDynamicRange.setEnabled(false);
break;
case STOPPING:
mButtonRecord.setText("Saving");
@@ -2169,7 +2266,6 @@
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(true);
- mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
case PAUSED:
mButtonRecord.setText("Stop");
@@ -2178,7 +2274,6 @@
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(true);
- mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
}
}
@@ -2203,10 +2298,6 @@
ToggleButton getButtonPersistent() {
return mButtonPersistent;
}
- @NonNull
- Button getButtonDynamicRange() {
- return mButtonDynamicRange;
- }
}
Preview getPreview() {
@@ -2351,19 +2442,6 @@
}
@NonNull
- private String getDynamicRangeIconName(@NonNull DynamicRange dynamicRange) {
- int resId = R.string.toggle_video_dyn_rng_unknown;
- for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) {
- if (Objects.equals(dynamicRange, uiData.mDynamicRange)) {
- resId = uiData.mToggleLabelRes;
- break;
- }
- }
-
- return getString(resId);
- }
-
- @NonNull
private static String getDynamicRangeMenuItemName(@NonNull DynamicRange dynamicRange) {
String menuItemName = dynamicRange.toString();
for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
index 4faa7a9..00a0313 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
@@ -59,10 +59,11 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
+import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
/** Activity which runs the camera preview with opengl processing */
@@ -136,7 +137,7 @@
display = Api30Impl.getDisplay(this);
}
OpenGLRenderer renderer = mRenderer = new OpenGLRenderer(
- getHdrEncodingsSupportedByDisplay(display));
+ getHighDynamicRangesSupportedByDisplay(display));
ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
View viewFinder = OpenGLActivity.chooseViewFinder(getIntent().getExtras(), viewFinderStub,
renderer);
@@ -267,20 +268,20 @@
}
/**
- * Returns a list of HDR encodings supported by the display.
+ * Returns a list of HDR dynamic ranges supported by the display.
*
- * <p>The returned HDR encodings are the encodings from the {@link DynamicRange} class, such
- * as {@link DynamicRange#ENCODING_HLG}. The returned list will never contain
- * {@link DynamicRange#ENCODING_SDR}.
+ * <p>The returned HDR dynamic ranges are constants defined by the {@code DynamicRange} class.
+ * The returned list will never contain {@link DynamicRange#SDR}.
*
* <p>The list may be empty if the display does not support HDR, such as on pre-API 24 devices.
*/
@NonNull
- public static List<Integer> getHdrEncodingsSupportedByDisplay(@Nullable Display display) {
+ public static Set<DynamicRange> getHighDynamicRangesSupportedByDisplay(
+ @Nullable Display display) {
if (display != null && Build.VERSION.SDK_INT >= 24) {
- return Api24Impl.getHdrEncodingsSupportedByDisplay(display);
+ return Api24Impl.getHighDynamicRangesSupportedByDisplay(display);
} else {
- return Collections.emptyList();
+ return Collections.emptySet();
}
}
@@ -334,17 +335,19 @@
@RequiresApi(24)
static class Api24Impl {
- private static final Map<Integer, Integer> DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING =
+ private static final Map<Integer, Set<DynamicRange>> DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE =
new HashMap<>();
static {
- DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING.put(HDR_TYPE_HLG, DynamicRange.ENCODING_HLG);
- DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING.put(HDR_TYPE_HDR10,
- DynamicRange.ENCODING_HDR10);
- DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING.put(HDR_TYPE_HDR10_PLUS,
- DynamicRange.ENCODING_HDR10_PLUS);
- DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING.put(HDR_TYPE_DOLBY_VISION,
- DynamicRange.ENCODING_DOLBY_VISION);
+ DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE.put(HDR_TYPE_HLG,
+ Collections.singleton(DynamicRange.HLG_10_BIT));
+ DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE.put(HDR_TYPE_HDR10,
+ Collections.singleton(DynamicRange.HDR10_10_BIT));
+ DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE.put(HDR_TYPE_HDR10_PLUS,
+ Collections.singleton(DynamicRange.HDR10_PLUS_10_BIT));
+ DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE.put(HDR_TYPE_DOLBY_VISION,
+ new HashSet<>(Arrays.asList(
+ DynamicRange.DOLBY_VISION_8_BIT, DynamicRange.DOLBY_VISION_10_BIT)));
}
private Api24Impl() {
@@ -352,12 +355,14 @@
}
@DoNotInline
- static List<Integer> getHdrEncodingsSupportedByDisplay(@NonNull Display display) {
+ static Set<DynamicRange> getHighDynamicRangesSupportedByDisplay(
+ @NonNull Display display) {
return Arrays.stream(display.getHdrCapabilities().getSupportedHdrTypes())
.boxed()
- .map(DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE_ENCODING::get)
+ .map(DISPLAY_HDR_TYPE_TO_DYNAMIC_RANGE::get)
+ .flatMap(set -> Objects.requireNonNull(set).stream())
.filter(Objects::nonNull)
- .collect(Collectors.toList());
+ .collect(Collectors.toSet());
}
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
index 60dafae..fe1c9b8 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
@@ -42,9 +42,9 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
@@ -120,10 +120,10 @@
private Pair<Executor, Consumer<Long>> mFrameUpdateListener;
- private final List<Integer> mHdrEncodingsSupportedByOutput;
+ private final Set<DynamicRange> mHighDynamicRangesSupportedByOutput;
- OpenGLRenderer(@NonNull List<Integer> hdrEncodingsSupportedByOutput) {
- mHdrEncodingsSupportedByOutput = hdrEncodingsSupportedByOutput;
+ OpenGLRenderer(@NonNull Set<DynamicRange> highDynamicRangesSupportedByOutput) {
+ mHighDynamicRangesSupportedByOutput = highDynamicRangesSupportedByOutput;
// Initialize the GL context on the GL thread
mExecutor.execute(() -> mNativeContext = initContext());
}
@@ -218,7 +218,7 @@
private boolean outputSupportsDynamicRange(DynamicRange newDynamicRange) {
if (!Objects.equals(newDynamicRange, DynamicRange.SDR)) {
- return mHdrEncodingsSupportedByOutput.contains(newDynamicRange.getEncoding());
+ return mHighDynamicRangesSupportedByOutput.contains(newDynamicRange);
}
// SDR is always supported
return true;
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
index 81eb97a..27bbfa0 100644
--- a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
@@ -323,7 +323,7 @@
app:layout_constraintTop_toBottomOf="@id/direction_toggle" />
<Button
- android:id="@+id/video_quality"
+ android:id="@+id/dynamic_range"
android:layout_width="46dp"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
@@ -338,7 +338,7 @@
/>
<Button
- android:id="@+id/video_dynamic_range"
+ android:id="@+id/video_quality"
android:layout_width="46dp"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
@@ -348,7 +348,7 @@
android:textColor="#EEEEEE"
android:textSize="14dp"
android:visibility="invisible"
- app:layout_constraintTop_toBottomOf="@id/video_persistent"
+ app:layout_constraintTop_toBottomOf="@id/dynamic_range"
app:layout_constraintRight_toRightOf="parent"
/>
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/EffectsFragment.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/EffectsFragment.kt
index 1d811a0..ca25ba9 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/EffectsFragment.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/EffectsFragment.kt
@@ -217,6 +217,7 @@
private fun stopRecording() {
record.text = "Record"
recording?.stop()
+ recording = null
}
private fun getNewVideoOutputMediaStoreOptions(): MediaStoreOutputOptions {
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/effects_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/effects_view.xml
index cbf1b36..dbdca03 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/effects_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/effects_view.xml
@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2023 The Android Open Source Project
-
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
-
http://www.apache.org/licenses/LICENSE-2.0
-
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,13 +11,12 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_weight="1"
- android:layout_height="0dp">
+ android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
@@ -51,14 +47,15 @@
</LinearLayout>
</RelativeLayout>
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
android:layout_margin="20dp">
<LinearLayout
android:orientation="vertical"
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_weight="1"
- android:layout_height="match_parent">
+ android:layout_height="0dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -84,9 +81,9 @@
</LinearLayout>
<LinearLayout
android:orientation="vertical"
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_weight="1"
- android:layout_height="match_parent">
+ android:layout_height="0dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 3c11d14..31468ef 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -2376,4 +2376,37 @@
}
""".trimIndent()
)
+
+ @Test
+ fun testGroupsInLoops() = verifyGoldenComposeIrTransform(
+ """
+ import androidx.compose.runtime.*
+
+ @Composable
+ private fun KeyContent1(items: List<Int>) {
+ items.forEach { item ->
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun KeyContent2(items: List<Int>) {
+ for (item in items) {
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+ }
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
index 44c3e39..f4a354e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
@@ -1576,4 +1576,42 @@
"""
)
}
+
+ @Test
+ fun testErrorInAnonymousFunctionPropertyInitializer() {
+ assumeTrue(!useFir)
+ check(
+ """
+ import androidx.compose.runtime.Composable
+ @Composable fun ComposableFunction() {}
+ fun getMyClass(): Any {
+ class MyClass {
+ val property = <!COMPOSABLE_EXPECTED!>fun() {
+ <!COMPOSABLE_INVOCATION!>ComposableFunction<!>() // invocation
+ }<!>
+ }
+ return MyClass()
+ }
+ """
+ )
+ }
+
+ @Test
+ fun testErrorInAnonymousFunctionPropertyInitializerForK2() {
+ assumeTrue(useFir)
+ check(
+ """
+ import androidx.compose.runtime.Composable
+ @Composable fun ComposableFunction() {}
+ fun getMyClass(): Any {
+ class MyClass {
+ val property = <!COMPOSABLE_EXPECTED!>fun()<!> {
+ <!COMPOSABLE_INVOCATION!>ComposableFunction<!>() // invocation
+ }
+ }
+ return MyClass()
+ }
+ """
+ )
+ }
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/facade/K2CompilerFacade.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/facade/K2CompilerFacade.kt
index 6c095ec..ff32894 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/facade/K2CompilerFacade.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/facade/K2CompilerFacade.kt
@@ -216,6 +216,7 @@
configuration.getBoolean(JVMConfigurationKeys.LINK_VIA_SIGNATURES),
EvaluatedConstTracker.create(),
configuration[CommonConfigurationKeys.INLINE_CONST_TRACKER],
+ configuration[CommonConfigurationKeys.EXPECT_ACTUAL_TRACKER],
allowNonCachedDeclarations = false
),
IrGenerationExtension.getInstances(project),
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = false\135.txt"
index f8c822e..65f9106 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = false\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = false\135.txt"
@@ -22,7 +22,7 @@
@Composable
fun Test(foo: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
- sourceInformation(%composer, "C(Test)*<A()>,<B()>:Test.kt")
+ sourceInformation(%composer, "C(Test)*<A()>:Test.kt")
val %dirty = %changed
if (%changed and 0b1110 == 0) {
%dirty = %dirty or if (%composer.changed(foo)) 0b0100 else 0b0010
@@ -33,9 +33,12 @@
}
with(foo) {
A(%this%with, %composer, 0)
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "*<B()>")
with(Bar()) {
B(%this%with, %this%with, %composer, 0)
}
+ %composer.endReplaceableGroup()
}
if (isTraceInProgress()) {
traceEventEnd()
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = true\135.txt"
index f8c822e..65f9106 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = true\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ContextReceiversTransformTests/testContextReceiversNestedWith\133useFir = true\135.txt"
@@ -22,7 +22,7 @@
@Composable
fun Test(foo: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
- sourceInformation(%composer, "C(Test)*<A()>,<B()>:Test.kt")
+ sourceInformation(%composer, "C(Test)*<A()>:Test.kt")
val %dirty = %changed
if (%changed and 0b1110 == 0) {
%dirty = %dirty or if (%composer.changed(foo)) 0b0100 else 0b0010
@@ -33,9 +33,12 @@
}
with(foo) {
A(%this%with, %composer, 0)
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "*<B()>")
with(Bar()) {
B(%this%with, %this%with, %composer, 0)
}
+ %composer.endReplaceableGroup()
}
if (isTraceInProgress()) {
traceEventEnd()
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = false\135.txt"
new file mode 100644
index 0000000..c4bbba2
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = false\135.txt"
@@ -0,0 +1,122 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.*
+
+@Composable
+private fun KeyContent1(items: List<Int>) {
+ items.forEach { item ->
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun KeyContent2(items: List<Int>) {
+ for (item in items) {
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+}
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@Composable
+private fun KeyContent1(items: List<Int>, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(KeyContent1):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b0110 == 0) {
+ %dirty = %dirty or if (%composer.changedInstance(items)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b0011 != 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %dirty, -1, <>)
+ }
+ items.forEach { item: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "")
+ if (item > -1) {
+ %composer.startMovableGroup(<>, item)
+ sourceInformation(%composer, "<rememb...>")
+ val tmp0 = <block>{
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(false) {
+ item
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }
+ %composer.endMovableGroup()
+ tmp0
+ }
+ %composer.endReplaceableGroup()
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ KeyContent1(items, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+}
+@Composable
+private fun KeyContent2(items: List<Int>, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(KeyContent2):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b0110 == 0) {
+ %dirty = %dirty or if (%composer.changedInstance(items)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b0011 != 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %dirty, -1, <>)
+ }
+ val <iterator> = items.iterator()
+ while (<iterator>.hasNext()) {
+ val item = <iterator>.next()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "")
+ if (item > -1) {
+ %composer.startMovableGroup(<>, item)
+ sourceInformation(%composer, "<rememb...>")
+ val tmp0 = <block>{
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(false) {
+ item
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }
+ %composer.endMovableGroup()
+ tmp0
+ }
+ %composer.endReplaceableGroup()
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ KeyContent2(items, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = true\135.txt"
new file mode 100644
index 0000000..b19edaf
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testGroupsInLoops\133useFir = true\135.txt"
@@ -0,0 +1,118 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.*
+
+@Composable
+private fun KeyContent1(items: List<Int>) {
+ items.forEach { item ->
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun KeyContent2(items: List<Int>) {
+ for (item in items) {
+ if (item > -1) {
+ key(item) {
+ remember {
+ item
+ }
+ }
+ }
+ }
+}
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@Composable
+private fun KeyContent1(items: List<Int>, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(KeyContent1):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b0110 == 0) {
+ %dirty = %dirty or if (%composer.changedInstance(items)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b0011 != 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %dirty, -1, <>)
+ }
+ items.forEach { item: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "")
+ if (item > -1) {
+ %composer.startMovableGroup(<>, item)
+ sourceInformation(%composer, "<rememb...>")
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ %composer.cache(false) {
+ item
+ }
+ %composer.endReplaceableGroup()
+ %composer.endMovableGroup()
+ }
+ %composer.endReplaceableGroup()
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ KeyContent1(items, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+}
+@Composable
+private fun KeyContent2(items: List<Int>, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(KeyContent2):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b0110 == 0) {
+ %dirty = %dirty or if (%composer.changedInstance(items)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b0011 != 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %dirty, -1, <>)
+ }
+ val <iterator> = items.iterator()
+ while (<iterator>.hasNext()) {
+ val item = <iterator>.next()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "")
+ if (item > -1) {
+ %composer.startMovableGroup(<>, item)
+ sourceInformation(%composer, "<rememb...>")
+ val tmp0 = <block>{
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(false) {
+ item
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }
+ %composer.endMovableGroup()
+ tmp0
+ }
+ %composer.endReplaceableGroup()
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ KeyContent2(items, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = false\135.txt"
index 212e96e..f603a80 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = false\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = false\135.txt"
@@ -26,11 +26,14 @@
traceEventStart(<>, %changed, -1, <>)
}
%composer.startReplaceableGroup(<>)
- sourceInformation(%composer, "*<Test("...>")
+ sourceInformation(%composer, "")
InlineNonComposable {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "*<Test("...>")
repeat(10) { it: Int ->
Test("InsideInline", %composer, 0b0110)
}
+ %composer.endReplaceableGroup()
}
%composer.endReplaceableGroup()
val tmp0 = Test("AfterInline", %composer, 0b0110)
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = true\135.txt"
index 212e96e..f603a80 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = true\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ControlFlowTransformTests/testInlineLambdaBeforeACall\133useFir = true\135.txt"
@@ -26,11 +26,14 @@
traceEventStart(<>, %changed, -1, <>)
}
%composer.startReplaceableGroup(<>)
- sourceInformation(%composer, "*<Test("...>")
+ sourceInformation(%composer, "")
InlineNonComposable {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "*<Test("...>")
repeat(10) { it: Int ->
Test("InsideInline", %composer, 0b0110)
}
+ %composer.endReplaceableGroup()
}
%composer.endReplaceableGroup()
val tmp0 = Test("AfterInline", %composer, 0b0110)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index df1ebf9..db3c96d 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -266,7 +266,7 @@
companion object {
fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
try {
- val KOTLIN_VERSION_EXPECTATION = "1.9.20"
+ val KOTLIN_VERSION_EXPECTATION = "1.9.21"
KotlinCompilerVersion.getVersion()?.let { version ->
val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
val suppressKotlinVersionCheck = configuration.get(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposableCallChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposableCallChecker.kt
index 46a822b..4465723 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposableCallChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/k2/ComposableCallChecker.kt
@@ -16,6 +16,7 @@
package androidx.compose.compiler.plugins.kotlin.k2
+import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirElement
@@ -41,12 +42,16 @@
import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression
import org.jetbrains.kotlin.fir.expressions.FirTryExpression
import org.jetbrains.kotlin.fir.expressions.impl.FirResolvedArgumentList
+import org.jetbrains.kotlin.fir.psi
import org.jetbrains.kotlin.fir.references.toResolvedCallableSymbol
import org.jetbrains.kotlin.fir.references.toResolvedValueParameterSymbol
import org.jetbrains.kotlin.fir.resolve.isInvoke
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.functionTypeKind
+import org.jetbrains.kotlin.psi.KtFunction
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
+import org.jetbrains.kotlin.psi.KtLambdaExpression
object ComposablePropertyAccessExpressionChecker : FirPropertyAccessExpressionChecker() {
override fun check(
@@ -119,49 +124,44 @@
visitAnonymousFunction = { function ->
if (function.typeRef.coneType.functionTypeKind(context.session) === ComposableFunction)
return
+ val functionPsi = function.psi
+ if (functionPsi is KtFunctionLiteral || functionPsi is KtLambdaExpression ||
+ functionPsi !is KtFunction
+ ) {
+ return@visitCurrentScope
+ }
+ val nonReadOnlyCalleeReference =
+ if (!calleeFunction.isReadOnlyComposable(context.session)) {
+ expression.calleeReference.source
+ } else {
+ null
+ }
+ if (checkComposableFunction(
+ function,
+ nonReadOnlyCalleeReference,
+ context,
+ reporter,
+ ) == ComposableCheckForScopeStatus.STOP
+ ) {
+ return
+ }
},
visitFunction = { function ->
- if (function.hasComposableAnnotation(context.session)) {
- if (
- function.hasReadOnlyComposableAnnotation(context.session) &&
- !calleeFunction.isReadOnlyComposable(context.session)
- ) {
- reporter.reportOn(
- expression.calleeReference.source,
- ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE,
- context
- )
+ val nonReadOnlyCalleeReference =
+ if (!calleeFunction.isReadOnlyComposable(context.session)) {
+ expression.calleeReference.source
+ } else {
+ null
}
+ if (checkComposableFunction(
+ function,
+ nonReadOnlyCalleeReference,
+ context,
+ reporter,
+ ) == ComposableCheckForScopeStatus.STOP
+ ) {
return
}
- // We allow composable calls in local delegated properties.
- // The only call this could be is a getValue/setValue in the synthesized getter/setter.
- if (function is FirPropertyAccessor && function.propertySymbol.hasDelegate) {
- if (function.propertySymbol.isVar) {
- reporter.reportOn(
- function.source,
- ComposeErrors.COMPOSE_INVALID_DELEGATE,
- context
- )
- }
- // Only local variables can be implicitly composable, for top-level or class-level
- // declarations we require an explicit annotation.
- if (!function.propertySymbol.isLocal) {
- reporter.reportOn(
- function.propertySymbol.source,
- ComposeErrors.COMPOSABLE_EXPECTED,
- context
- )
- }
- return
- }
- // We've found a non-composable function which contains a composable call.
- val source = if (function is FirPropertyAccessor) {
- function.propertySymbol.source
- } else {
- function.source
- }
- reporter.reportOn(source, ComposeErrors.COMPOSABLE_EXPECTED, context)
},
visitTryExpression = { tryExpression, container ->
// Only report an error if the composable call happens inside of the `try`
@@ -182,6 +182,69 @@
)
}
+private enum class ComposableCheckForScopeStatus {
+ STOP,
+ CONTINUE,
+}
+
+/**
+ * This function will be called by [visitCurrentScope], and this function determines
+ * whether it will continue the composable element check for the scope or not
+ * by returning [ComposableCheckForScopeStatus].
+ */
+private fun checkComposableFunction(
+ function: FirFunction,
+ nonReadOnlyCallInsideFunction: KtSourceElement?,
+ context: CheckerContext,
+ reporter: DiagnosticReporter,
+): ComposableCheckForScopeStatus {
+ // [function] is a function with "read-only" composable annotation, but it has a call
+ // without "read-only" composable annotation.
+ // -> report NONREADONLY_CALL_IN_READONLY_COMPOSABLE.
+ if (function.hasComposableAnnotation(context.session)) {
+ if (
+ function.hasReadOnlyComposableAnnotation(context.session) &&
+ nonReadOnlyCallInsideFunction != null
+ ) {
+ reporter.reportOn(
+ nonReadOnlyCallInsideFunction,
+ ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE,
+ context
+ )
+ }
+ return ComposableCheckForScopeStatus.STOP
+ }
+ // We allow composable calls in local delegated properties.
+ // The only call this could be is a getValue/setValue in the synthesized getter/setter.
+ if (function is FirPropertyAccessor && function.propertySymbol.hasDelegate) {
+ if (function.propertySymbol.isVar) {
+ reporter.reportOn(
+ function.source,
+ ComposeErrors.COMPOSE_INVALID_DELEGATE,
+ context
+ )
+ }
+ // Only local variables can be implicitly composable, for top-level or class-level
+ // declarations we require an explicit annotation.
+ if (!function.propertySymbol.isLocal) {
+ reporter.reportOn(
+ function.propertySymbol.source,
+ ComposeErrors.COMPOSABLE_EXPECTED,
+ context
+ )
+ }
+ return ComposableCheckForScopeStatus.STOP
+ }
+ // We've found a non-composable function which contains a composable call.
+ val source = if (function is FirPropertyAccessor) {
+ function.propertySymbol.source
+ } else {
+ function.source
+ }
+ reporter.reportOn(source, ComposeErrors.COMPOSABLE_EXPECTED, context)
+ return ComposableCheckForScopeStatus.CONTINUE
+}
+
/**
* Reports an error if we are invoking a lambda parameter of an inline function in a context
* where composable calls are not allowed, unless the lambda parameter is itself annotated
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 409e4ac..9f7db48 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -2781,6 +2781,9 @@
expression.transformChildrenVoid()
}
return if (captureScope.hasCapturedComposableCall) {
+ // if the inlined lambda has composable calls, realize its coalescable groups
+ // in the body to ensure that repeated invocations are not colliding.
+ captureScope.realizeAllDirectChildren()
expression.asCoalescableGroup(captureScope)
} else {
expression
@@ -2809,7 +2812,7 @@
visitNormalComposableCall(expression)
}
}
- ComposeFqNames.key -> visitKeyCall(expression)
+ ComposeFqNames.key,
DecoyFqNames.key -> visitKeyCall(expression)
else -> visitNormalComposableCall(expression)
}
@@ -3586,7 +3589,7 @@
// If a loop contains composable calls but not a otherwise need a group per iteration
// group, none of the children can be coalesced and must be realized as the second
// iteration as composable calls at the end might end of overlapping slots with the
- // start of the loop. See b/205590513 for details.
+ // start of the loop. See b/232007227 for details.
loopScope.realizeAllDirectChildren()
loop.asCoalescableGroup(loopScope)
} else {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index fc52c37..7a9fa96 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -19,7 +19,9 @@
import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
@@ -68,11 +70,18 @@
}
}
- private fun testScroll(spacingPx: Int = 0, assertBlock: suspend () -> Unit) {
+ private fun testScroll(
+ spacingPx: Int = 0,
+ containerSizePx: Int = itemSizePx * 3,
+ afterContentPaddingPx: Int = 0,
+ assertBlock: suspend () -> Unit
+ ) {
rule.setContent {
state = rememberLazyGridState()
scope = rememberCoroutineScope()
- TestContent(with(rule.density) { spacingPx.toDp() })
+ with(rule.density) {
+ TestContent(spacingPx.toDp(), containerSizePx.toDp(), afterContentPaddingPx.toDp())
+ }
}
runBlocking {
assertBlock()
@@ -297,6 +306,85 @@
}
@Test
+ fun canScrollForwardAndBackward_afterSmallScrollFromStart() = testScroll(
+ containerSizePx = (itemSizePx * 1.5f).roundToInt()
+ ) {
+ val delta = (itemSizePx / 3f).roundToInt()
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ // small enough scroll to not cause any new items to be composed or old ones disposed.
+ state.scrollBy(delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(delta)
+ assertThat(state.canScrollForward).isTrue()
+ assertThat(state.canScrollBackward).isTrue()
+ }
+ // and scroll back to start
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollBy(-delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.canScrollForward).isTrue()
+ assertThat(state.canScrollBackward).isFalse()
+ }
+ }
+
+ @Test
+ fun canScrollForwardAndBackward_afterSmallScrollFromEnd() = testScroll(
+ containerSizePx = (itemSizePx * 1.5f).roundToInt()
+ ) {
+ val delta = -(itemSizePx / 3f).roundToInt()
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ // scroll to the end of the list.
+ state.scrollToItem(itemsCount)
+ // small enough scroll to not cause any new items to be composed or old ones disposed.
+ state.scrollBy(delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.canScrollForward).isTrue()
+ assertThat(state.canScrollBackward).isTrue()
+ }
+ // and scroll back to the end
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollBy(-delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.canScrollForward).isFalse()
+ assertThat(state.canScrollBackward).isTrue()
+ }
+ }
+
+ @Test
+ fun canScrollForwardAndBackward_afterSmallScrollFromEnd_withContentPadding() = testScroll(
+ containerSizePx = (itemSizePx * 1.5f).roundToInt(),
+ afterContentPaddingPx = 2,
+ ) {
+ val delta = -(itemSizePx / 3f).roundToInt()
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ // scroll to the end of the list.
+ state.scrollToItem(itemsCount)
+
+ assertThat(state.canScrollForward).isFalse()
+ assertThat(state.canScrollBackward).isTrue()
+
+ // small enough scroll to not cause any new items to be composed or old ones disposed.
+ state.scrollBy(delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.canScrollForward).isTrue()
+ assertThat(state.canScrollBackward).isTrue()
+ }
+ // and scroll back to the end
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollBy(-delta.toFloat())
+ }
+ rule.runOnIdle {
+ assertThat(state.canScrollForward).isFalse()
+ assertThat(state.canScrollBackward).isTrue()
+ }
+ }
+
+ @Test
fun animatePerFrameForwardWithSpacing() = testScroll(spacingPx = 10) {
assertSpringAnimation(toIndex = 16, spacingPx = 10)
}
@@ -375,12 +463,13 @@
}
@Composable
- private fun TestContent(spacingDp: Dp) {
+ private fun TestContent(spacingDp: Dp, containerSizeDp: Dp, afterContentPaddingDp: Dp) {
if (vertical) {
LazyVerticalGrid(
GridCells.Fixed(2),
Modifier.height(containerSizeDp),
state,
+ contentPadding = PaddingValues(bottom = afterContentPaddingDp),
verticalArrangement = Arrangement.spacedBy(spacingDp)
) {
items(itemsCount) {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
index 766c37e..f83f030 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/KeyboardOptionsTest.kt
@@ -16,11 +16,11 @@
package androidx.compose.foundation.text
-import androidx.compose.ui.text.input.AndroidImeOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PlatformImeOptions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,7 +31,7 @@
@Test
fun test_toImeOption() {
- val platformImeOptions = AndroidImeOptions("privateImeOptions")
+ val platformImeOptions = PlatformImeOptions("privateImeOptions")
val keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 26f0514..daf2c45 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -359,6 +359,7 @@
spanLayoutProvider = spanLayoutProvider,
pinnedItems = pinnedItems,
coroutineScope = coroutineScope,
+ placementScopeInvalidator = state.placementScopeInvalidator,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 19d1f00..0d50108 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy.grid
+import androidx.collection.mutableScatterMapOf
import androidx.compose.foundation.lazy.layout.LazyLayoutAnimation
import androidx.compose.foundation.lazy.layout.LazyLayoutAnimationSpecsNode
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
@@ -33,7 +34,7 @@
*/
internal class LazyGridItemPlacementAnimator {
// state containing relevant info for active items.
- private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+ private val keyToItemInfoMap = mutableScatterMapOf<Any, ItemInfo>()
// snapshot of the key to index map used for the last measuring.
private var keyIndexMap: LazyLayoutKeyIndexMap = LazyLayoutKeyIndexMap.Empty
@@ -84,7 +85,7 @@
}
// first add all items we had in the previous run
- movingAwayKeys.addAll(keyToItemInfoMap.keys)
+ keyToItemInfoMap.forEachKey { movingAwayKeys.add(it) }
// iterate through the items which are visible (without animated offsets)
positionedItems.fastForEach { item ->
// remove items we have in the current one as they are still visible.
@@ -167,7 +168,7 @@
movingAwayKeys.forEach { key ->
// found an item which was in our map previously but is not a part of the
// positionedItems now
- val itemInfo = keyToItemInfoMap.getValue(key)
+ val itemInfo = keyToItemInfoMap[key]!!
val newIndex = keyIndexMap.getIndex(key)
if (newIndex == -1) {
@@ -181,6 +182,7 @@
Constraints.fixedHeight(itemInfo.crossAxisSize)
}
)
+ item.nonScrollableItem = true
// check if we have any active placement animation on the item
val inProgress =
itemInfo.animations.any { it?.isPlacementAnimationInProgress == true }
@@ -211,7 +213,7 @@
}
val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
- val itemInfo = keyToItemInfoMap.getValue(item.key)
+ val itemInfo = keyToItemInfoMap[item.key]!!
item.position(
mainAxisOffset = mainAxisOffset,
@@ -237,7 +239,7 @@
}
val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
- val itemInfo = keyToItemInfoMap.getValue(item.key)
+ val itemInfo = keyToItemInfoMap[item.key]!!
item.position(
mainAxisOffset = mainAxisOffset,
crossAxisOffset = itemInfo.crossAxisOffset,
@@ -269,7 +271,7 @@
private fun initializeAnimation(
item: LazyGridMeasuredItem,
mainAxisOffset: Int,
- itemInfo: ItemInfo = keyToItemInfoMap.getValue(item.key)
+ itemInfo: ItemInfo = keyToItemInfoMap[item.key]!!
) {
val firstPlaceableOffset = item.offset
@@ -290,7 +292,7 @@
}
private fun startAnimationsIfNeeded(item: LazyGridMeasuredItem) {
- val itemInfo = keyToItemInfoMap.getValue(item.key)
+ val itemInfo = keyToItemInfoMap[item.key]!!
itemInfo.animations.forEach { animation ->
if (animation != null) {
val newTarget = item.offset
@@ -305,8 +307,13 @@
}
}
- fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutAnimation? =
- keyToItemInfoMap[key]?.animations?.get(placeableIndex)
+ fun getAnimation(key: Any, placeableIndex: Int): LazyLayoutAnimation? {
+ return if (keyToItemInfoMap.isEmpty()) {
+ null
+ } else {
+ keyToItemInfoMap[key]?.animations?.get(placeableIndex)
+ }
+ }
private val LazyGridMeasuredItem.hasAnimations: Boolean
get() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 2130384..9e08bf66 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
@@ -62,6 +63,7 @@
spanLayoutProvider: LazyGridSpanLayoutProvider,
pinnedItems: List<Int>,
coroutineScope: CoroutineScope,
+ placementScopeInvalidator: ObservableScopeInvalidator,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyGridMeasureResult {
require(beforeContentPadding >= 0) { "negative beforeContentPadding" }
@@ -81,7 +83,8 @@
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenLines
+ mainAxisItemSpacing = spaceBetweenLines,
+ remeasureNeeded = false
)
} else {
var currentFirstLineIndex = firstVisibleLineIndex
@@ -137,10 +140,23 @@
val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
var currentMainAxisOffset = -currentFirstLineScrollOffset
+ // will be set to true if we composed some items only to know their size and apply scroll,
+ // while in the end this item will not end up in the visible viewport. we will need an
+ // extra remeasure in order to dispose such items.
+ var remeasureNeeded = false
+
// first we need to skip lines we already composed while composing backward
- visibleLines.fastForEach {
- index++
- currentMainAxisOffset += it.mainAxisSizeWithSpacings
+ var indexInVisibleLines = 0
+ while (indexInVisibleLines < visibleLines.size) {
+ if (currentMainAxisOffset >= maxMainAxis) {
+ // this item is out of the bounds and will not be visible.
+ visibleLines.removeAt(indexInVisibleLines)
+ remeasureNeeded = true
+ } else {
+ index++
+ currentMainAxisOffset += visibleLines[indexInVisibleLines].mainAxisSizeWithSpacings
+ indexInVisibleLines++
+ }
}
// then composing visible lines forward until we fill the whole viewport.
@@ -159,9 +175,10 @@
currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
if (currentMainAxisOffset <= minOffset &&
measuredLine.items.last().index != itemsCount - 1) {
- // this line is offscreen and will not be placed. advance firstVisibleLineIndex
+ // this line is offscreen and will not be visible. advance firstVisibleLineIndex
currentFirstLineIndex = index + 1
currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
+ remeasureNeeded = true
} else {
visibleLines.add(measuredLine)
}
@@ -285,6 +302,8 @@
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
+ // we attach it during the placement so LazyGridState can trigger re-placement
+ placementScopeInvalidator.attachToScope()
},
viewportStartOffset = -beforeContentPadding,
viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
@@ -299,7 +318,8 @@
reverseLayout = reverseLayout,
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
afterContentPadding = afterContentPadding,
- mainAxisItemSpacing = spaceBetweenLines
+ mainAxisItemSpacing = spaceBetweenLines,
+ remeasureNeeded = remeasureNeeded
)
}
}
@@ -390,7 +410,7 @@
} else {
absoluteOffset
}
- positionedItems.addAll(
+ positionedItems.addAllFromArray(
line.position(relativeOffset, layoutWidth, layoutHeight)
)
}
@@ -405,7 +425,7 @@
currentMainAxis = firstLineScrollOffset
lines.fastForEach {
- positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ positionedItems.addAllFromArray(it.position(currentMainAxis, layoutWidth, layoutHeight))
currentMainAxis += it.mainAxisSizeWithSpacings
}
@@ -417,3 +437,10 @@
}
return positionedItems
}
+
+// Faster version of addAll that does not create a list for each array
+private fun <T> MutableList<T>.addAllFromArray(arr: Array<T>) {
+ for (item in arr) {
+ add(item)
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
index 00dfd71..49a5139 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
@@ -16,30 +16,32 @@
package androidx.compose.foundation.lazy.grid
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.offsetOnMainAxis
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastForEach
/**
* The result of the measure pass for lazy list layout.
*/
-@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridMeasureResult(
// properties defining the scroll position:
/** The new first visible line of items.*/
val firstVisibleLine: LazyGridMeasuredLine?,
/** The new value for [LazyGridState.firstVisibleItemScrollOffset].*/
- val firstVisibleLineScrollOffset: Int,
+ var firstVisibleLineScrollOffset: Int,
/** True if there is some space available to continue scrolling in the forward direction.*/
- val canScrollForward: Boolean,
+ var canScrollForward: Boolean,
/** The amount of scroll consumed during the measure pass.*/
- val consumedScroll: Float,
+ var consumedScroll: Float,
/** MeasureResult defining the layout.*/
measureResult: MeasureResult,
+ /** True when extra remeasure is required. */
+ val remeasureNeeded: Boolean,
// properties representing the info needed for LazyListLayoutInfo:
/** see [LazyGridLayoutInfo.visibleItemsInfo] */
- override val visibleItemsInfo: List<LazyGridItemInfo>,
+ override val visibleItemsInfo: List<LazyGridMeasuredItem>,
/** see [LazyGridLayoutInfo.viewportStartOffset] */
override val viewportStartOffset: Int,
/** see [LazyGridLayoutInfo.viewportEndOffset] */
@@ -55,7 +57,67 @@
/** see [LazyGridLayoutInfo.mainAxisItemSpacing] */
override val mainAxisItemSpacing: Int
) : LazyGridLayoutInfo, MeasureResult by measureResult {
+
+ val canScrollBackward
+ get() = (firstVisibleLine?.index ?: 0) != 0 || firstVisibleLineScrollOffset != 0
+
override val viewportSize: IntSize
get() = IntSize(width, height)
override val beforeContentPadding: Int get() = -viewportStartOffset
+
+ /**
+ * Tries to apply a scroll [delta] for this layout info. In some cases we can apply small
+ * scroll deltas by just changing the offsets for each [visibleItemsInfo].
+ * But we can only do so if after applying the delta we would not need to compose a new item
+ * or dispose an item which is currently visible. In this case this function will not apply
+ * the [delta] and return false.
+ *
+ * @return true if we can safely apply a passed scroll [delta] to this layout info.
+ * If true is returned, only the placement phase is needed to apply new offsets.
+ * If false is returned, it means we have to rerun the full measure phase to apply the [delta].
+ */
+ fun tryToApplyScrollWithoutRemeasure(delta: Int): Boolean {
+ if (remeasureNeeded || visibleItemsInfo.isEmpty() || firstVisibleLine == null ||
+ // applying this delta will change firstVisibleLineScrollOffset
+ (firstVisibleLineScrollOffset - delta) !in
+ 0 until firstVisibleLine.mainAxisSizeWithSpacings
+ ) {
+ return false
+ }
+ val first = visibleItemsInfo.first()
+ val last = visibleItemsInfo.last()
+ if (first.nonScrollableItem || last.nonScrollableItem) {
+ // non scrollable items require special handling.
+ return false
+ }
+ val canApply = if (delta < 0) {
+ // scrolling forward
+ val deltaToFirstItemChange = first.offsetOnMainAxis(orientation) +
+ first.mainAxisSizeWithSpacings - viewportStartOffset
+ val deltaToLastItemChange = last.offsetOnMainAxis(orientation) +
+ last.mainAxisSizeWithSpacings - viewportEndOffset
+ minOf(deltaToFirstItemChange, deltaToLastItemChange) > -delta
+ } else {
+ // scrolling backward
+ val deltaToFirstItemChange =
+ viewportStartOffset - first.offsetOnMainAxis(orientation)
+ val deltaToLastItemChange =
+ viewportEndOffset - last.offsetOnMainAxis(orientation)
+ minOf(deltaToFirstItemChange, deltaToLastItemChange) > delta
+ }
+ return if (canApply) {
+ firstVisibleLineScrollOffset -= delta
+ visibleItemsInfo.fastForEach {
+ it.applyScrollDelta(delta)
+ }
+ consumedScroll = delta.toFloat()
+ if (!canScrollForward && delta > 0) {
+ // we scrolled backward, so now we can scroll forward
+ canScrollForward = true
+ }
+ true
+ } else {
+ false
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
index 961341b..d7f4dd5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -90,6 +90,12 @@
private set
/**
+ * True when this item is not supposed to react on scroll delta. for example items being
+ * animated away out of the bounds are non scrollable.
+ */
+ var nonScrollableItem: Boolean = false
+
+ /**
* Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
* [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
* up outside of the viewport (for example one item consist of 2 placeables, and the first one
@@ -123,6 +129,19 @@
maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
}
+ fun applyScrollDelta(delta: Int) {
+ if (nonScrollableItem) {
+ return
+ }
+ offset = offset.copy { it + delta }
+ repeat(placeablesCount) { index ->
+ val animation = animator.getAnimation(key, index)
+ if (animation != null) {
+ animation.rawOffset = animation.rawOffset.copy { mainAxis -> mainAxis + delta }
+ }
+ }
+ }
+
fun place(
scope: Placeable.PlacementScope,
) = with(scope) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
index 7ec7f70..30b61cc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -67,6 +67,11 @@
}
}
+ fun updateScrollOffset(scrollOffset: Int) {
+ check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+ this.scrollOffset = scrollOffset
+ }
+
/**
* Updates the scroll position - the passed values will be used as a start position for
* composing the items during the next measure pass and will be updated by the real
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index e76d3de..913d241 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -28,6 +28,7 @@
import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator
import androidx.compose.foundation.lazy.layout.animateScrollToItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -35,17 +36,20 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
+import kotlin.math.roundToInt
/**
* Creates a [LazyGridState] that is remembered across compositions.
@@ -115,7 +119,10 @@
val firstVisibleItemScrollOffset: Int get() = scrollPosition.scrollOffset
/** Backing state for [layoutInfo] */
- private val layoutInfoState = mutableStateOf<LazyGridLayoutInfo>(EmptyLazyGridLayoutInfo)
+ private val layoutInfoState = mutableStateOf(
+ EmptyLazyGridLayoutInfo,
+ neverEqualPolicy()
+ )
/**
* The object of [LazyGridLayoutInfo] calculated during the last layout pass. For example,
@@ -239,6 +246,8 @@
internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+ internal val placementScopeInvalidator = ObservableScopeInvalidator()
+
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
@@ -308,9 +317,21 @@
// inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
// we have less than 0.5 pixels
if (abs(scrollToBeConsumed) > 0.5f) {
+ val layoutInfo = layoutInfoState.value
val preScrollToBeConsumed = scrollToBeConsumed
- remeasurement?.forceRemeasure()
- if (prefetchingEnabled) {
+ val intDelta = scrollToBeConsumed.roundToInt()
+ if (layoutInfo.tryToApplyScrollWithoutRemeasure(intDelta)) {
+ applyMeasureResult(
+ result = layoutInfo,
+ visibleItemsStayedTheSame = true
+ )
+ // we don't need to remeasure, so we only trigger re-placement:
+ placementScopeInvalidator.invalidateScope()
+
+ notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed, layoutInfo)
+ } else {
+ remeasurement?.forceRemeasure()
+
notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
}
}
@@ -329,7 +350,10 @@
}
}
- private fun notifyPrefetch(delta: Float) {
+ private fun notifyPrefetch(
+ delta: Float,
+ layoutInfo: LazyGridLayoutInfo = layoutInfoState.value
+ ) {
val prefetchState = prefetchState
if (!prefetchingEnabled) {
return
@@ -414,18 +438,23 @@
/**
* Updates the state with the new calculated scroll position and consumed scroll.
*/
- internal fun applyMeasureResult(result: LazyGridMeasureResult) {
- scrollPosition.updateFromMeasureResult(result)
+ internal fun applyMeasureResult(
+ result: LazyGridMeasureResult,
+ visibleItemsStayedTheSame: Boolean = false
+ ) {
scrollToBeConsumed -= result.consumedScroll
layoutInfoState.value = result
+ if (visibleItemsStayedTheSame) {
+ scrollPosition.updateScrollOffset(result.firstVisibleLineScrollOffset)
+ } else {
+ scrollPosition.updateFromMeasureResult(result)
+ cancelPrefetchIfVisibleItemsChanged(result)
+ }
+ canScrollBackward = result.canScrollBackward
canScrollForward = result.canScrollForward
- canScrollBackward = (result.firstVisibleLine?.index ?: 0) != 0 ||
- result.firstVisibleLineScrollOffset != 0
numMeasurePasses++
-
- cancelPrefetchIfVisibleItemsChanged(result)
}
/**
@@ -454,16 +483,25 @@
}
}
-@OptIn(ExperimentalFoundationApi::class)
-private object EmptyLazyGridLayoutInfo : LazyGridLayoutInfo {
- override val visibleItemsInfo = emptyList<LazyGridItemInfo>()
- override val viewportStartOffset = 0
- override val viewportEndOffset = 0
- override val totalItemsCount = 0
- override val viewportSize = IntSize.Zero
- override val orientation = Orientation.Vertical
- override val reverseLayout = false
- override val beforeContentPadding: Int = 0
- override val afterContentPadding: Int = 0
- override val mainAxisItemSpacing = 0
-}
+private val EmptyLazyGridLayoutInfo = LazyGridMeasureResult(
+ firstVisibleLine = null,
+ firstVisibleLineScrollOffset = 0,
+ canScrollForward = false,
+ consumedScroll = 0f,
+ measureResult = object : MeasureResult {
+ override val width: Int = 0
+ override val height: Int = 0
+ @Suppress("PrimitiveInCollection")
+ override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
+ override fun placeChildren() {}
+ },
+ visibleItemsInfo = emptyList(),
+ viewportStartOffset = 0,
+ viewportEndOffset = 0,
+ totalItemsCount = 0,
+ reverseLayout = false,
+ orientation = Orientation.Vertical,
+ afterContentPadding = 0,
+ mainAxisItemSpacing = 0,
+ remeasureNeeded = false
+)
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupPerfettoSdkBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupPerfettoSdkBenchmark.kt
index eb9ab43..32e2f34 100644
--- a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupPerfettoSdkBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/TrivialStartupPerfettoSdkBenchmark.kt
@@ -23,6 +23,7 @@
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.testutils.measureStartup
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
@@ -33,6 +34,7 @@
@OptIn(ExperimentalMetricApi::class)
@LargeTest
+@SdkSuppress(minSdkVersion = 30) // required for perfetto sdk capture
@RunWith(Parameterized::class)
class TrivialStartupPerfettoSdkBenchmark(
private val startupMode: StartupMode,
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 4b75b67..cce19c1 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -62,6 +62,7 @@
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -3975,6 +3976,46 @@
assertEquals(1, consumer.invokeCount)
}
+ // regression test from b/232007227 with forEach
+ @Test
+ fun slotsAreUsedCorrectly_forEach() = compositionTest {
+ class Car(val model: String)
+ class Person(val name: String, val car: MutableStateFlow<Car>)
+
+ val people = mutableListOf<MutableStateFlow<Person?>>(
+ MutableStateFlow(Person("Ford", MutableStateFlow(Car("Model T")))),
+ MutableStateFlow(Person("Musk", MutableStateFlow(Car("Model 3"))))
+ )
+ compose {
+ people.forEach {
+ val person = it.collectAsState().value
+ Text(person?.name ?: "No person")
+ if (person != null) {
+ val car = person.car.collectAsState().value
+ Text(" ${car.model}")
+ }
+ }
+ }
+
+ validate {
+ people.forEach {
+ val person = it.value
+ Text(person?.name ?: "No person")
+ if (person != null) {
+ val car = person.car.value
+ Text(" ${car.model}")
+ }
+ }
+ }
+
+ advanceTimeBy(16_000L)
+ people[0].value = null
+ advanceTimeBy(16_000L)
+
+ expectChanges()
+ revalidate()
+ }
+
private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
val channel = Channel<Unit>(Channel.CONFLATED)
val job = launch {
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index c4ca8f3..8595a42 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -440,6 +440,7 @@
method @androidx.compose.runtime.Stable public static float luminance(long);
method public static inline long takeOrElse(long, kotlin.jvm.functions.Function0<androidx.compose.ui.graphics.Color> block);
method @ColorInt @androidx.compose.runtime.Stable public static int toArgb(long);
+ field @kotlin.PublishedApi internal static final long UnspecifiedColor = 16L; // 0x10L
}
@kotlin.jvm.JvmInline public final value class ColorMatrix {
diff --git a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/ColorBenchmark.kt b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/ColorBenchmark.kt
index 0dad9cd..84a8a27 100644
--- a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/ColorBenchmark.kt
+++ b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/ColorBenchmark.kt
@@ -19,6 +19,7 @@
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.graphics.lerp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -28,15 +29,26 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-open class ColorBenchmark {
+class ColorBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun colorLerp() {
benchmarkRule.measureRepeated {
- for (i in 0..1_000) {
- lerp(Color.Red, Color.Green, i / 1_000.0f)
+ for (i in 0..500) {
+ lerp(Color.Red, Color.Green, i / 500.0f)
+ }
+ }
+ }
+
+ @Test
+ fun wideColorLerp() {
+ val start = Color(1.0f, 0.0f, 0.0f, 1.0f, ColorSpaces.DisplayP3)
+ val end = Color(0.0f, 1.0f, 0.0f, 1.0f, ColorSpaces.DisplayP3)
+ benchmarkRule.measureRepeated {
+ for (i in 0..500) {
+ lerp(start, end, i / 500.0f)
}
}
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 7b97f58..d65109d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.graphics.colorspace.Rgb
import androidx.compose.ui.graphics.colorspace.connect
+import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.lerp
import kotlin.math.max
import kotlin.math.min
@@ -390,6 +391,10 @@
}
}
+// Same as Color.Unspecified.packedValue, but avoids a getstatic
+@PublishedApi
+internal const val UnspecifiedColor = 0x10UL
+
/**
* Create a [Color] by passing individual [red], [green], [blue], [alpha], and [colorSpace]
* components. The default [color space][ColorSpace] is [sRGB][ColorSpaces.Srgb] and
@@ -420,7 +425,7 @@
((green * 255.0f + 0.5f).toInt() shl 8) or
(blue * 255.0f + 0.5f).toInt()
)
- return Color(value = (argb.toULong() and 0xffffffffUL) shl 32)
+ return Color(argb.toULong() shl 32)
}
requirePrecondition(colorSpace.componentCount == 3) {
@@ -438,15 +443,57 @@
val a = (max(0.0f, min(alpha, 1.0f)) * 1023.0f + 0.5f).toInt()
- // Suppress sign extension
return Color(
- value = (
- ((r.toULong() and 0xffffUL) shl 48) or
- ((g.toULong() and 0xffffUL) shl 32) or
- ((b.toULong() and 0xffffUL) shl 16) or
- ((a.toULong() and 0x03ffUL) shl 6) or
- (id.toULong() and 0x003fUL)
+ (
+ ((r.toLong() and 0xffffL) shl 48) or
+ ((g.toLong() and 0xffffL) shl 32) or
+ ((b.toLong() and 0xffffL) shl 16) or
+ ((a.toLong() and 0x03ffL) shl 6) or
+ (id.toLong() and 0x003fL)
+ ).toULong()
+ )
+}
+
+/**
+ * Create a [Color] by passing individual [red], [green], [blue], [alpha], and [colorSpace]
+ * components. This function is equivalent to [Color] but doesn't perform any check/validation
+ * of the parameters. It is meant to be used when the color space and values are known to
+ * be valid by construction, for instance when lerping colors.
+ */
+@Stable
+internal fun UncheckedColor(
+ red: Float,
+ green: Float,
+ blue: Float,
+ alpha: Float = 1f,
+ colorSpace: ColorSpace = ColorSpaces.Srgb
+): Color {
+ if (colorSpace.isSrgb) {
+ val argb = (
+ ((alpha * 255.0f + 0.5f).toInt() shl 24) or
+ ((red * 255.0f + 0.5f).toInt() shl 16) or
+ ((green * 255.0f + 0.5f).toInt() shl 8) or
+ (blue * 255.0f + 0.5f).toInt()
)
+ return Color(argb.toULong() shl 32)
+ }
+
+ val r = floatToHalf(red)
+ val g = floatToHalf(green)
+ val b = floatToHalf(blue)
+
+ val a = (max(0.0f, min(alpha, 1.0f)) * 1023.0f + 0.5f).toInt()
+
+ val id = colorSpace.id
+
+ return Color(
+ (
+ ((r.toLong() and 0xffffL) shl 48) or
+ ((g.toLong() and 0xffffL) shl 32) or
+ ((b.toLong() and 0xffffL) shl 16) or
+ ((a.toLong() and 0x03ffL) shl 6) or
+ (id.toLong() and 0x003fL)
+ ).toULong()
)
}
@@ -460,7 +507,7 @@
*/
@Stable
fun Color(@ColorInt color: Int): Color {
- return Color(value = color.toULong() shl 32)
+ return Color(color.toULong() shl 32)
}
/**
@@ -477,7 +524,7 @@
*/
@Stable
fun Color(color: Long): Color {
- return Color(value = (color.toULong() and 0xffffffffUL) shl 32)
+ return Color((color shl 32).toULong())
}
/**
@@ -499,7 +546,8 @@
@IntRange(from = 0, to = 0xFF) blue: Int,
@IntRange(from = 0, to = 0xFF) alpha: Int = 0xFF
): Color {
- val color = ((alpha and 0xFF) shl 24) or
+ val color =
+ ((alpha and 0xFF) shl 24) or
((red and 0xFF) shl 16) or
((green and 0xFF) shl 8) or
(blue and 0xFF)
@@ -528,12 +576,12 @@
val endA = endColor.green
val endB = endColor.blue
- val interpolated = Color(
- alpha = lerp(startAlpha, endAlpha, fraction),
- red = lerp(startL, endL, fraction),
- green = lerp(startA, endA, fraction),
- blue = lerp(startB, endB, fraction),
- colorSpace = colorSpace
+ val interpolated = UncheckedColor(
+ lerp(startL, endL, fraction),
+ lerp(startA, endA, fraction),
+ lerp(startB, endB, fraction),
+ lerp(startAlpha, endAlpha, fraction),
+ colorSpace
)
return interpolated.convert(stop.colorSpace)
}
@@ -561,7 +609,7 @@
val g = compositeComponent(fg.green, background.green, fgA, bgA, a)
val b = compositeComponent(fg.blue, background.blue, fgA, bgA, a)
- return Color(r, g, b, a, background.colorSpace)
+ return UncheckedColor(r, g, b, a, background.colorSpace)
}
/**
@@ -602,7 +650,7 @@
@Stable
fun Color.luminance(): Float {
val colorSpace = colorSpace
- require(colorSpace.model == ColorModel.Rgb) {
+ requirePrecondition(colorSpace.model == ColorModel.Rgb) {
"The specified color must be encoded in an RGB color space. " +
"The supplied color space is ${colorSpace.model}"
}
@@ -612,11 +660,7 @@
val g = eotf(green.toDouble())
val b = eotf(blue.toDouble())
- return saturate(((0.2126 * r) + (0.7152 * g) + (0.0722 * b)).toFloat())
-}
-
-private fun saturate(v: Float): Float {
- return if (v <= 0.0f) 0.0f else (if (v >= 1.0f) 1.0f else v)
+ return (((0.2126 * r) + (0.7152 * g) + (0.0722 * b)).toFloat()).fastCoerceIn(0.0f, 1.0f)
}
/**
@@ -636,13 +680,13 @@
* `false` when this is [Color.Unspecified].
*/
@Stable
-inline val Color.isSpecified: Boolean get() = value != Color.Unspecified.value
+inline val Color.isSpecified: Boolean get() = value != UnspecifiedColor
/**
* `true` when this is [Color.Unspecified].
*/
@Stable
-inline val Color.isUnspecified: Boolean get() = value == Color.Unspecified.value
+inline val Color.isUnspecified: Boolean get() = value == UnspecifiedColor
/**
* If this color [isSpecified] then this is returned, otherwise [block] is executed and its result
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
index 4adb50b..cd9b92d 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
+++ b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/ColorTest.kt
@@ -568,6 +568,11 @@
assertEquals(50f / 255f, srgbGreen.blue, 0.01f)
}
+ @Test fun unspecifiedConstantValue() {
+ // See comments in Color.kt, we want to make sure Color.Unspecified doesn't change encoding
+ assertEquals(0x10UL, Color.Unspecified.value)
+ }
+
companion object {
fun Int.toHexString() = "0x${toUInt().toString(16).padStart(8, '0')}"
}
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
new file mode 100644
index 0000000..8e2ec26
--- /dev/null
+++ b/compose/ui/ui-text/api/current.ignore
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+ChangedClass: androidx.compose.ui.text.input.PlatformImeOptions:
+ Class androidx.compose.ui.text.input.PlatformImeOptions changed class/interface declaration
+
+
+RemovedClass: androidx.compose.ui.text.input.AndroidImeOptions:
+ Removed class androidx.compose.ui.text.input.AndroidImeOptions
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 9f723e7..7abd7ad 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -941,12 +941,6 @@
package androidx.compose.ui.text.input {
- public final class AndroidImeOptions implements androidx.compose.ui.text.input.PlatformImeOptions {
- ctor public AndroidImeOptions(optional String? privateImeOptions);
- method public String? getPrivateImeOptions();
- property public final String? privateImeOptions;
- }
-
public final class BackspaceCommand implements androidx.compose.ui.text.input.EditCommand {
ctor public BackspaceCommand();
method public void applyTo(androidx.compose.ui.text.input.EditingBuffer buffer);
@@ -1139,7 +1133,10 @@
property public final char mask;
}
- public sealed interface PlatformImeOptions {
+ @androidx.compose.runtime.Immutable public final class PlatformImeOptions {
+ ctor public PlatformImeOptions(optional String? privateImeOptions);
+ method public String? getPrivateImeOptions();
+ property public final String? privateImeOptions;
}
public interface PlatformTextInputService {
diff --git a/compose/ui/ui-text/api/restricted_current.ignore b/compose/ui/ui-text/api/restricted_current.ignore
index 2c9640b..8e2ec26 100644
--- a/compose/ui/ui-text/api/restricted_current.ignore
+++ b/compose/ui/ui-text/api/restricted_current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
-AddedClass: androidx.compose.ui.text.platform.Synchronization_jvmKt:
- Added class androidx.compose.ui.text.platform.Synchronization_jvmKt
+ChangedClass: androidx.compose.ui.text.input.PlatformImeOptions:
+ Class androidx.compose.ui.text.input.PlatformImeOptions changed class/interface declaration
+
+
+RemovedClass: androidx.compose.ui.text.input.AndroidImeOptions:
+ Removed class androidx.compose.ui.text.input.AndroidImeOptions
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 707c653..36f0b32 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -941,12 +941,6 @@
package androidx.compose.ui.text.input {
- public final class AndroidImeOptions implements androidx.compose.ui.text.input.PlatformImeOptions {
- ctor public AndroidImeOptions(optional String? privateImeOptions);
- method public String? getPrivateImeOptions();
- property public final String? privateImeOptions;
- }
-
public final class BackspaceCommand implements androidx.compose.ui.text.input.EditCommand {
ctor public BackspaceCommand();
method public void applyTo(androidx.compose.ui.text.input.EditingBuffer buffer);
@@ -1139,7 +1133,10 @@
property public final char mask;
}
- public sealed interface PlatformImeOptions {
+ @androidx.compose.runtime.Immutable public final class PlatformImeOptions {
+ ctor public PlatformImeOptions(optional String? privateImeOptions);
+ method public String? getPrivateImeOptions();
+ property public final String? privateImeOptions;
}
public interface PlatformTextInputService {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt
index e274e69..68ef3fb 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.android.kt
@@ -16,10 +16,7 @@
package androidx.compose.ui.text.input
-/**
- * Used to configure the platform specific IME options.
- */
-actual sealed interface PlatformImeOptions
+import androidx.compose.runtime.Immutable
/**
* Used to configure Android platform IME options.
@@ -27,10 +24,13 @@
* @param privateImeOptions defines a [String] for supplying additional information options that
* are private to a particular IME implementation.
*/
-class AndroidImeOptions(val privateImeOptions: String? = null) : PlatformImeOptions {
+@Immutable
+actual class PlatformImeOptions(
+ val privateImeOptions: String? = null,
+) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is AndroidImeOptions) return false
+ if (other !is PlatformImeOptions) return false
if (privateImeOptions != other.privateImeOptions) return false
@@ -42,6 +42,6 @@
}
override fun toString(): String {
- return "AndroidImeOptions(privateImeOptions=$privateImeOptions)"
+ return "PlatformImeOptions(privateImeOptions=$privateImeOptions)"
}
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt
index a9ade23..c7be9a2 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.text.input
+import androidx.compose.runtime.Immutable
+
/**
* Used to configure the platform specific IME options.
*/
-expect sealed interface PlatformImeOptions
+@Immutable
+expect class PlatformImeOptions
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.desktop.kt
index 60607ca..a5bc839 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/input/PlatformImeOptions.desktop.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.text.input
+import androidx.compose.runtime.Immutable
+
/**
* Used to configure the platform specific IME options.
*/
-actual sealed interface PlatformImeOptions
+@Immutable
+actual class PlatformImeOptions
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
index a7086c8..a77df1b 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
@@ -127,10 +127,11 @@
// given LookaheadScope, whenever the relative position changes.
fun Modifier.animatePlacementInScope(lookaheadScope: LookaheadScope) = composed {
// Creates an offset animation
- var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by mutableStateOf(
- null
- )
- var targetOffset: IntOffset? by mutableStateOf(null)
+ var offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by remember {
+ mutableStateOf(
+ null
+ )
+ }
this.intermediateLayout { measurable, constraints ->
val placeable = measurable.measure(constraints)
@@ -142,7 +143,7 @@
val target = with(lookaheadScope) {
lookaheadScopeCoordinates
.localLookaheadPositionOf(coordinates)
- .round().also { targetOffset = it }
+ .round()
}
// Uses the target offset to start an offset animation
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 5b4c70b..015999c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -23,43 +23,25 @@
import android.os.Build.VERSION_CODES.P
import android.os.Build.VERSION_CODES.R
import android.text.SpannableString
-import android.util.LongSparseArray
import android.view.View
-import android.view.ViewStructure
import android.view.accessibility.AccessibilityEvent
-import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED
-import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED
import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT
-import android.view.translation.TranslationRequestValue
-import android.view.translation.TranslationResponseValue
-import android.view.translation.ViewTranslationRequest
-import android.view.translation.ViewTranslationRequest.ID_TEXT
-import android.view.translation.ViewTranslationResponse
-import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
-import androidx.compose.ui.platform.coreshims.ViewStructureCompat
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role
@@ -67,11 +49,9 @@
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.clearTextSubstitution
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.copyText
-import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.cutText
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.dismiss
@@ -84,7 +64,6 @@
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.horizontalScrollAxisRange
import androidx.compose.ui.semantics.invisibleToUser
-import androidx.compose.ui.semantics.isShowingTextSubstitution
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
@@ -96,55 +75,37 @@
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.semantics.setSelection
import androidx.compose.ui.semantics.setText
-import androidx.compose.ui.semantics.setTextSubstitution
-import androidx.compose.ui.semantics.showTextSubstitution
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.semantics.text
import androidx.compose.ui.semantics.textSelectionRange
-import androidx.compose.ui.semantics.textSubstitution
-import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION
-import androidx.core.view.accessibility.AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLICK
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_COLLAPSE
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_DISMISS
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_EXPAND
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_PASTE
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
-import androidx.core.view.doOnDetach
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.clearInvocations
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoMoreInteractions
-import org.mockito.kotlin.whenever
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -154,11 +115,8 @@
private val tag = "tag"
private lateinit var androidComposeView: AndroidComposeView
- private lateinit var contentCaptureSessionCompat: ContentCaptureSessionCompat
- private lateinit var viewStructureCompat: ViewStructureCompat
private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
private val accessibilityEventLoopIntervalMs = 100L
- private val contentCaptureEventLoopIntervalMs = 100L
@Test
@OptIn(ExperimentalComposeUiApi::class)
@@ -1058,358 +1016,6 @@
}
@Test
- fun sendScrollEvent_byStateObservation_horizontal() {
- // Arrange.
- var scrollValue by mutableStateOf(0f, structuralEqualityPolicy())
- val scrollMaxValue = 100f
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Row(
- Modifier
- .size(20.toDp(), 10.toDp())
- .semantics(mergeDescendants = false) {
- horizontalScrollAxisRange = ScrollAxisRange(
- { scrollValue },
- { scrollMaxValue }
- )
- }
- ) {
- Text("foo", Modifier.size(10.toDp()))
- Text("bar",
- Modifier
- .size(10.toDp())
- .testTag(tag))
- }
- }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
- val virtualViewId = rule.onNodeWithTag(tag).semanticsId
- rule.runOnIdle { dispatchedAccessibilityEvents.clear() }
-
- // Act.
- try {
- androidComposeView.snapshotObserver.startObserving()
- rule.runOnIdle {
- androidComposeView.accessibilityNodeProvider
- .performAction(virtualViewId, ACTION_ACCESSIBILITY_FOCUS, null)
- Snapshot.notifyObjectsInitialized()
- scrollValue = 2f
- Snapshot.sendApplyNotifications()
- }
- } finally {
- androidComposeView.snapshotObserver.stopObserving()
- }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- val focusedANI = androidComposeView.accessibilityNodeProvider
- .findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
- assertThat(Rect().also { focusedANI?.getBoundsInScreen(it) })
- .isEqualTo(Rect(10, 0, 20, 10))
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_VIEW_ACCESSIBILITY_FOCUSED
- },
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_SUBTREE
- },
- AccessibilityEvent().apply {
- eventType = TYPE_VIEW_SCROLLED
- scrollX = 2
- maxScrollX = 100
- },
- )
- }
- }
-
- @Test
- fun sendScrollEvent_byStateObservation_vertical() {
- // Arrange.
- var scrollValue by mutableStateOf(0f, structuralEqualityPolicy())
- val scrollMaxValue = 100f
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- verticalScrollAxisRange = ScrollAxisRange(
- { scrollValue },
- { scrollMaxValue }
- )
- }
- )
- }
-
- // TODO(b/272068594): We receive an extra TYPE_WINDOW_CONTENT_CHANGED event 100ms after
- // setup. So we wait an extra 100ms here so that this test is not affected by that extra
- // event.
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
- dispatchedAccessibilityEvents.clear()
-
- // Act.
- try {
- androidComposeView.snapshotObserver.startObserving()
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
- rule.runOnIdle {
- Snapshot.notifyObjectsInitialized()
- scrollValue = 2f
- Snapshot.sendApplyNotifications()
- }
- } finally {
- androidComposeView.snapshotObserver.stopObserving()
- }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_SUBTREE
- },
- AccessibilityEvent().apply {
- eventType = TYPE_VIEW_SCROLLED
- scrollY = 2
- maxScrollY = 100
- },
- )
- }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_whenPropertyAdded() {
- // Arrange.
- var addProperty by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- if (addProperty) disabled()
- }
- )
- }
-
- // Act.
- rule.runOnIdle { addProperty = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
- }
- )
- }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_whenPropertyRemoved() {
- // Arrange.
- var removeProperty by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- if (!removeProperty) disabled()
- }
- )
- }
-
- // Act.
- rule.runOnIdle { removeProperty = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
- }
- )
- }
- }
-
- @Test
- @Ignore("b/307823561")
- fun sendWindowContentChangeUndefinedEventByDefault_onlyOnce_whenMultiplePropertiesChange() {
- // Arrange.
- var propertiesChanged by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- if (!propertiesChanged) {
- disabled()
- } else {
- onClick { true }
- }
- }
- )
- }
-
- // Act.
- rule.runOnIdle { propertiesChanged = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
- }
- )
- }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_standardActionWithTheSameLabel() {
- // Arrange.
- var newAction by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- if (!newAction) {
- onClick(label = "action") { true }
- } else {
- onClick(label = "action") { true }
- }
- }
- )
- }
-
- // Act.
- rule.runOnIdle { newAction = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle { assertThat(dispatchedAccessibilityEvents).isEmpty() }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_standardActionWithDifferentLabels() {
- // Arrange.
- var newAction by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- if (!newAction) {
- onClick(label = "action1") { true }
- } else {
- onClick(label = "action2") { true }
- }
- }
- )
- }
-
- // Act.
- rule.runOnIdle { newAction = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
- }
- )
- }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_customActionWithTheSameLabel() {
- // Arrange.
- var newAction by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- customActions = if (!newAction) {
- listOf(CustomAccessibilityAction("action") { true })
- } else {
- listOf(CustomAccessibilityAction("action") { false })
- }
- }
- )
- }
-
- // Act.
- rule.runOnIdle { newAction = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle { assertThat(dispatchedAccessibilityEvents).isEmpty() }
- }
-
- @Test
- fun sendWindowContentChangeUndefinedEventByDefault_customActionWithDifferentLabels() {
- // Arrange.
- var newAction by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics(mergeDescendants = false) {
- customActions = if (!newAction) {
- listOf(CustomAccessibilityAction("action1") { true })
- } else {
- listOf(CustomAccessibilityAction("action2") { true })
- }
- }
- )
- }
-
- // Act.
- rule.runOnIdle { newAction = true }
- rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle {
- assertThat(dispatchedAccessibilityEvents)
- .comparingElementsUsing(AccessibilityEventComparator)
- .containsExactly(
- AccessibilityEvent().apply {
- eventType = TYPE_WINDOW_CONTENT_CHANGED
- contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
- }
- )
- }
- }
-
- @Test
fun testUncoveredNodes_notPlacedNodes_notIncluded() {
// Arrange.
rule.setContentWithAccessibilityEnabled {
@@ -1489,294 +1095,6 @@
}
}
- @Test
- fun canScroll_returnsFalse_whenPositionInvalid() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.dp)
- .semantics(mergeDescendants = true) {
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
- }
- }
-
- @Test
- fun canScroll_returnsTrue_whenHorizontalScrollableNotAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0.5f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- // Should be scrollable in both directions.
- assertThat(androidComposeView.canScrollHorizontally(1)).isTrue()
- assertThat(androidComposeView.canScrollHorizontally(0)).isTrue()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
- }
- }
-
- @Test
- fun canScroll_returnsTrue_whenVerticalScrollableNotAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- verticalScrollAxisRange = ScrollAxisRange(
- value = { 0.5f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- // Should be scrollable in both directions.
- assertThat(androidComposeView.canScrollVertically(1)).isTrue()
- assertThat(androidComposeView.canScrollVertically(0)).isTrue()
- assertThat(androidComposeView.canScrollVertically(-1)).isTrue()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_whenHorizontalScrollable_whenScrolledRightAndAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 1f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_whenHorizontalScrollable_whenScrolledLeftAndAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollHorizontally(1)).isTrue()
- assertThat(androidComposeView.canScrollHorizontally(0)).isTrue()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_whenVerticalScrollable_whenScrolledDownAndAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- verticalScrollAxisRange = ScrollAxisRange(
- value = { 1f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollVertically(1)).isFalse()
- assertThat(androidComposeView.canScrollVertically(0)).isFalse()
- assertThat(androidComposeView.canScrollVertically(-1)).isTrue()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_whenVerticalScrollable_whenScrolledUpAndAtLimit() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- verticalScrollAxisRange = ScrollAxisRange(
- value = { 0f },
- maxValue = { 1f },
- reverseScrolling = false
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollVertically(1)).isTrue()
- assertThat(androidComposeView.canScrollVertically(0)).isTrue()
- assertThat(androidComposeView.canScrollVertically(-1)).isFalse()
- }
- }
-
- @Test
- fun canScroll_respectsReverseDirection() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0f },
- maxValue = { 1f },
- reverseScrolling = true
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_forVertical_whenScrollableIsHorizontal() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(100.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0.5f },
- maxValue = { 1f },
- reverseScrolling = true
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollVertically(1)).isFalse()
- assertThat(androidComposeView.canScrollVertically(0)).isFalse()
- assertThat(androidComposeView.canScrollVertically(-1)).isFalse()
- }
- }
-
- @Test
- fun canScroll_returnsFalse_whenTouchIsOutsideBounds() {
- // Arrange.
- rule.setContentWithAccessibilityEnabled {
- Box(
- Modifier
- .size(50.toDp())
- .semantics(mergeDescendants = true) {
- testTag = tag
- horizontalScrollAxisRange = ScrollAxisRange(
- value = { 0.5f },
- maxValue = { 1f },
- reverseScrolling = true
- )
- }
- )
- }
-
- // Act.
- rule.onNodeWithTag(tag).performTouchInput { down(Offset(100f, 100f)) }
-
- // Assert.
- rule.runOnIdle {
- assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
- assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
- }
- }
-
// TODO(b/272068594): Asserting that a list does not contain an element can be an incorrect test
// because this would pass even if the event was present, (For example when isEnabled = false).
// Keeping this here to show parity for code review. This can be removed because the test
@@ -2031,525 +1349,6 @@
}
}
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testInitContentCaptureSemanticsStructureChangeEvents_onStart() {
- // Arrange.
- rule.setContentWithContentCaptureEnabled(retainInteractionsDuringInitialization = true) {}
-
- // Act - Wait for initialization that is triggered by onStart().
-
- // Assert = verify the root node appeared.
- rule.runOnIdle {
- verify(contentCaptureSessionCompat).newVirtualViewStructure(any(), any())
- verify(contentCaptureSessionCompat).notifyViewsAppeared(any())
- verify(viewStructureCompat).setDimens(any(), any(), any(), any(), any(), any())
- verify(viewStructureCompat).toViewStructure()
- verifyNoMoreInteractions(contentCaptureSessionCompat)
- verifyNoMoreInteractions(viewStructureCompat)
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testInitContentCaptureSemanticsStructureChangeEvents_onStop() {
- // Arrange.
- rule.setContentWithContentCaptureEnabled {}
-
- // Act.
- rule.runOnIdle {
- androidComposeView.doOnDetach {
-
- // Assert.
- verify(contentCaptureSessionCompat).notifyViewsDisappeared(any())
- verifyNoMoreInteractions(contentCaptureSessionCompat)
- verifyNoMoreInteractions(viewStructureCompat)
- }
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testSendContentCaptureSemanticsStructureChangeEvents_appeared() {
- // Arrange.
- var appeared by mutableStateOf(false)
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Row(
- Modifier
- .size(100.dp)
- .semantics {}) {
- if (appeared) {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("foo") })
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") })
- }
- }
- }
-
- // Act.
- rule.runOnIdle { appeared = true }
- // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
- // invocations of boundsUpdatesEventLoop.
- repeat(2) {
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.waitForIdle()
- }
-
- // Assert.
- rule.runOnIdle {
- with(argumentCaptor<CharSequence>()) {
- verify(viewStructureCompat, times(2)).setText(capture())
- assertThat(firstValue).isEqualTo("foo")
- assertThat(secondValue).isEqualTo("bar")
- }
- verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
- with(argumentCaptor<List<ViewStructure>>()) {
- verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
- assertThat(firstValue.count()).isEqualTo(2)
- }
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testSendContentCaptureSemanticsStructureChangeEvents_disappeared() {
- // Arrange.
- var disappeared by mutableStateOf(false)
-
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- if (!disappeared) {
- Row(
- Modifier
- .size(100.dp)
- .semantics { }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics { })
- Box(
- Modifier
- .size(10.dp)
- .semantics { })
- }
- }
- }
-
- // Act.
- rule.runOnIdle { disappeared = true }
-
- // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
- // invocations of boundsUpdatesEventLoop.
- repeat(2) {
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.waitForIdle()
- }
-
- // Assert.
- rule.runOnIdle {
- with(argumentCaptor<LongArray>()) {
- verify(contentCaptureSessionCompat, times(1)).notifyViewsDisappeared(capture())
- assertThat(firstValue.count()).isEqualTo(3)
- }
- verify(contentCaptureSessionCompat, times(0)).notifyViewsAppeared(any())
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testSendContentCaptureSemanticsStructureChangeEvents_appearedAndDisappeared() {
- // Arrange.
- var appeared by mutableStateOf(false)
-
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- if (appeared) {
- Row(
- Modifier
- .size(100.dp)
- .semantics { }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics { })
- Box(
- Modifier
- .size(10.dp)
- .semantics { })
- }
- }
- }
-
- // Act.
- rule.runOnIdle { appeared = true }
- // TODO(b/272068594): This test was written to ensure that if the items appeared and
- // disappeared before the 100ms, it would still report the items that were added and the
- // items that were removed The items were (As long as the items had different IDs). However
- // it is not possible for a items with different IDs to disappear as they are not existing.
- // The mocks also limit us to write this test since we can't mock AutofillIDs since
- // AutofillId is a final class, and these tests just use the autofill id of the parent
- // view.
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.runOnIdle { appeared = false }
-
- // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for
- // two invocations of boundsUpdatesEventLoop.
- repeat(2) {
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.waitForIdle()
- }
-
- // Assert.
- rule.runOnIdle {
- with(argumentCaptor<LongArray>()) {
- verify(contentCaptureSessionCompat, times(1)).notifyViewsDisappeared(capture())
- assertThat(firstValue.count()).isEqualTo(3)
- }
- with(argumentCaptor<List<ViewStructure>>()) {
- verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
- assertThat(firstValue.count()).isEqualTo(3)
- }
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 29)
- fun testSendContentCaptureSemanticsStructureChangeEvents_sameNodeAppearedThenDisappeared() {
- // Arrange.
- var appeared by mutableStateOf(false)
-
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { }) {
- if (appeared) {
- Box(
- Modifier
- .size(10.dp)
- .semantics { })
- }
- }
- }
-
- // Act.
- rule.runOnIdle { appeared = true }
-
- // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
- // invocations of boundsUpdatesEventLoop.
- repeat(2) {
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.waitForIdle()
- }
-
- // Assert.
- rule.runOnIdle {
- verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
- with(argumentCaptor<List<ViewStructure>>()) {
- verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
- assertThat(firstValue.count()).isEqualTo(1)
- }
- clearInvocations(contentCaptureSessionCompat)
- }
-
- rule.runOnIdle { appeared = false }
-
- // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
- // invocations of boundsUpdatesEventLoop.
- repeat(2) {
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
- rule.waitForIdle()
- }
-
- // Assert.
- rule.runOnIdle {
- verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
- verify(contentCaptureSessionCompat, times(0)).notifyViewsAppeared(any())
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testUpdateTranslationOnAppeared_showOriginal() {
- // Arrange.
- var appeared by mutableStateOf(false)
- var result = true
-
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { }) {
- if (appeared) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- text = AnnotatedString("foo")
- isShowingTextSubstitution = true
- showTextSubstitution {
- result = it
- true
- }
- }
- )
- }
- }
- }
- rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onHideTranslation() }
-
- // Act.
- rule.runOnIdle { appeared = true }
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle { assertThat(result).isFalse() }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testUpdateTranslationOnAppeared_showTranslated() {
- // Arrange.
- var appeared by mutableStateOf(false)
- var result = false
-
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { }) {
- if (appeared) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- text = AnnotatedString("foo")
- isShowingTextSubstitution = false
- showTextSubstitution {
- result = it
- true
- }
- }
- )
- }
- }
- }
- rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onShowTranslation() }
-
- // Act.
- rule.runOnIdle { appeared = true }
- rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
-
- // Assert.
- rule.runOnIdle { assertThat(result).isTrue() }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testOnCreateVirtualViewTranslationRequests() {
- // Arrange.
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- testTag = tag
- text = AnnotatedString("foo")
- }
- )
- }
- }
- val virtualViewId = rule.onNodeWithTag(tag).semanticsId
-
- val ids = LongArray(1).apply { this[0] = virtualViewId.toLong() }
- val requestsCollector: Consumer<ViewTranslationRequest?> = mock()
-
- // Act.
- rule.runOnIdle {
- androidComposeView.onCreateVirtualViewTranslationRequests(
- ids,
- IntArray(0),
- requestsCollector
- )
- }
-
- // Assert.
- rule.runOnIdle {
- with(argumentCaptor<ViewTranslationRequest>()) {
- verify(requestsCollector).accept(capture())
- assertThat(firstValue).isEqualTo(
- ViewTranslationRequest
- .Builder(androidComposeView.autofillId, virtualViewId.toLong())
- .setValue(ID_TEXT, TranslationRequestValue.forText(AnnotatedString("foo")))
- .build()
- )
- }
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testOnVirtualViewTranslationResponses() {
- // Arrange.
- var result: AnnotatedString? = null
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- testTag = tag
- text = AnnotatedString("foo")
- setTextSubstitution {
- result = it
- true
- }
- }
- )
- }
- }
- val virtualViewId = rule.onNodeWithTag(tag).semanticsId
-
- // Act.
- rule.runOnIdle {
- androidComposeView.onVirtualViewTranslationResponses(
- LongSparseArray<ViewTranslationResponse?>().apply {
- append(
- virtualViewId.toLong(),
- ViewTranslationResponse
- .Builder(androidComposeView.autofillId)
- .setValue(
- ID_TEXT,
- TranslationResponseValue.Builder(0).setText("bar").build()
- )
- .build()
- )
- }
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(result).isEqualTo(AnnotatedString("bar")) }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testOnShowTranslation() {
- // Arrange.
- var result = false
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- textSubstitution = AnnotatedString("foo")
- isShowingTextSubstitution = false
- showTextSubstitution {
- result = it
- true
- }
- }
- )
- }
- }
-
- // Act.
- rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onShowTranslation() }
-
- // Assert.
- rule.runOnIdle { assertThat(result).isTrue() }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testOnHideTranslation() {
- // Arrange.
- var result = true
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- text = AnnotatedString("bar")
- textSubstitution = AnnotatedString("foo")
- isShowingTextSubstitution = true
- showTextSubstitution {
- result = it
- true
- }
- }
- )
- }
- }
-
- // Act.
- rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onHideTranslation() }
-
- // Assert.
- rule.runOnIdle { assertThat(result).isFalse() }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- fun testOnClearTranslation() {
- // Arrange.
- var result = false
- rule.mainClock.autoAdvance = false
- rule.setContentWithContentCaptureEnabled {
- Box(
- Modifier
- .size(10.dp)
- .semantics { text = AnnotatedString("bar") }) {
- Box(
- Modifier
- .size(10.dp)
- .semantics {
- text = AnnotatedString("bar")
- isShowingTextSubstitution = true
- clearTextSubstitution {
- result = true
- true
- }
- }
- )
- }
- }
-
- // Act.
- rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onClearTranslation() }
-
- // Assert.
- rule.runOnIdle { assertThat(result).isTrue() }
- }
-
private fun Int.toDp(): Dp = with(rule.density) { [email protected]() }
private fun ComposeContentTestRule.setContentWithAccessibilityEnabled(
@@ -2570,47 +1369,6 @@
runOnIdle { dispatchedAccessibilityEvents.clear() }
}
- @RequiresApi(Build.VERSION_CODES.O)
- private fun ComposeContentTestRule.setContentWithContentCaptureEnabled(
- retainInteractionsDuringInitialization: Boolean = false,
- content: @Composable () -> Unit
- ) {
- contentCaptureSessionCompat = mock()
- viewStructureCompat = mock()
- val viewStructure: ViewStructure = mock()
-
- whenever(contentCaptureSessionCompat.newVirtualViewStructure(any(), any()))
- .thenReturn(viewStructureCompat)
- whenever(viewStructureCompat.toViewStructure())
- .thenReturn(viewStructure)
-
- setContent {
- androidComposeView = LocalView.current as AndroidComposeView
- with(androidComposeView.composeAccessibilityDelegate) {
- accessibilityForceEnabledForTesting = true
- contentCaptureForceEnabledForTesting = true
- contentCaptureSession = contentCaptureSessionCompat
- onSendAccessibilityEvent = { dispatchedAccessibilityEvents += it; false }
- }
-
- whenever(contentCaptureSessionCompat.newAutofillId(any())).thenAnswer {
- androidComposeView.autofillId
- }
-
- content()
- }
-
- // Advance the clock past the first accessibility event loop, and clear the initial
- // as we are want the assertions to check the events that were generated later.
- runOnIdle { mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs) }
-
- runOnIdle {
- if (!retainInteractionsDuringInitialization) {
- clearInvocations(contentCaptureSessionCompat, viewStructureCompat)
- }
- }
- }
-
private fun AndroidComposeView.createAccessibilityNodeInfo(
semanticsId: Int
): AccessibilityNodeInfoCompat {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ContentCaptureTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ContentCaptureTest.kt
new file mode 100644
index 0000000..60c607a
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/ContentCaptureTest.kt
@@ -0,0 +1,674 @@
+
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui
+
+import android.os.Build
+import android.util.LongSparseArray
+import android.view.View
+import android.view.ViewStructure
+import android.view.accessibility.AccessibilityEvent
+import android.view.translation.TranslationRequestValue
+import android.view.translation.TranslationResponseValue
+import android.view.translation.ViewTranslationRequest
+import android.view.translation.ViewTranslationRequest.ID_TEXT
+import android.view.translation.ViewTranslationResponse
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
+import androidx.compose.ui.platform.coreshims.ViewStructureCompat
+import androidx.compose.ui.semantics.clearTextSubstitution
+import androidx.compose.ui.semantics.isShowingTextSubstitution
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setTextSubstitution
+import androidx.compose.ui.semantics.showTextSubstitution
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.semantics.textSubstitution
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.doOnDetach
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ContentCaptureTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<TestActivity>()
+
+ private val tag = "tag"
+ private lateinit var androidComposeView: AndroidComposeView
+ private lateinit var contentCaptureSessionCompat: ContentCaptureSessionCompat
+ private lateinit var viewStructureCompat: ViewStructureCompat
+ private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
+ private val contentCaptureEventLoopIntervalMs = 100L
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testInitContentCaptureSemanticsStructureChangeEvents_onStart() {
+ // Arrange.
+ rule.setContentWithContentCaptureEnabled(retainInteractionsDuringInitialization = true) {}
+
+ // Act - Wait for initialization that is triggered by onStart().
+
+ // Assert = verify the root node appeared.
+ rule.runOnIdle {
+ verify(contentCaptureSessionCompat).newVirtualViewStructure(any(), any())
+ verify(contentCaptureSessionCompat).notifyViewsAppeared(any())
+ verify(viewStructureCompat).setDimens(any(), any(), any(), any(), any(), any())
+ verify(viewStructureCompat).toViewStructure()
+ verifyNoMoreInteractions(contentCaptureSessionCompat)
+ verifyNoMoreInteractions(viewStructureCompat)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testInitContentCaptureSemanticsStructureChangeEvents_onStop() {
+ // Arrange.
+ rule.setContentWithContentCaptureEnabled {}
+
+ // Act.
+ rule.runOnIdle {
+ androidComposeView.doOnDetach {
+
+ // Assert.
+ verify(contentCaptureSessionCompat).notifyViewsDisappeared(any())
+ verifyNoMoreInteractions(contentCaptureSessionCompat)
+ verifyNoMoreInteractions(viewStructureCompat)
+ }
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testSendContentCaptureSemanticsStructureChangeEvents_appeared() {
+ // Arrange.
+ var appeared by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Row(
+ Modifier
+ .size(100.dp)
+ .semantics {}
+ ) {
+ if (appeared) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("foo") }
+ )
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { appeared = true }
+ // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
+ // invocations of boundsUpdatesEventLoop.
+ repeat(2) {
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.waitForIdle()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ with(argumentCaptor<CharSequence>()) {
+ verify(viewStructureCompat, times(2)).setText(capture())
+ assertThat(firstValue).isEqualTo("foo")
+ assertThat(secondValue).isEqualTo("bar")
+ }
+ verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
+ with(argumentCaptor<List<ViewStructure>>()) {
+ verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
+ assertThat(firstValue.count()).isEqualTo(2)
+ }
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testSendContentCaptureSemanticsStructureChangeEvents_disappeared() {
+ // Arrange.
+ var disappeared by mutableStateOf(false)
+
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ if (!disappeared) {
+ Row(
+ Modifier
+ .size(100.dp)
+ .semantics { }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ )
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { disappeared = true }
+
+ // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
+ // invocations of boundsUpdatesEventLoop.
+ repeat(2) {
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.waitForIdle()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ with(argumentCaptor<LongArray>()) {
+ verify(contentCaptureSessionCompat, times(1)).notifyViewsDisappeared(capture())
+ assertThat(firstValue.count()).isEqualTo(3)
+ }
+ verify(contentCaptureSessionCompat, times(0)).notifyViewsAppeared(any())
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testSendContentCaptureSemanticsStructureChangeEvents_appearedAndDisappeared() {
+ // Arrange.
+ var appeared by mutableStateOf(false)
+
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ if (appeared) {
+ Row(
+ Modifier
+ .size(100.dp)
+ .semantics { }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ )
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { appeared = true }
+ // TODO(b/272068594): This test was written to ensure that if the items appeared and
+ // disappeared before the 100ms, it would still report the items that were added and the
+ // items that were removed The items were (As long as the items had different IDs). However
+ // it is not possible for a items with different IDs to disappear as they are not existing.
+ // The mocks also limit us to write this test since we can't mock AutofillIDs since
+ // AutofillId is a final class, and these tests just use the autofill id of the parent
+ // view.
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.runOnIdle { appeared = false }
+
+ // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for
+ // two invocations of boundsUpdatesEventLoop.
+ repeat(2) {
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.waitForIdle()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ with(argumentCaptor<LongArray>()) {
+ verify(contentCaptureSessionCompat, times(1)).notifyViewsDisappeared(capture())
+ assertThat(firstValue.count()).isEqualTo(3)
+ }
+ with(argumentCaptor<List<ViewStructure>>()) {
+ verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
+ assertThat(firstValue.count()).isEqualTo(3)
+ }
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 29)
+ fun testSendContentCaptureSemanticsStructureChangeEvents_sameNodeAppearedThenDisappeared() {
+ // Arrange.
+ var appeared by mutableStateOf(false)
+
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ ) {
+ if (appeared) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { appeared = true }
+
+ // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
+ // invocations of boundsUpdatesEventLoop.
+ repeat(2) {
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.waitForIdle()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
+ with(argumentCaptor<List<ViewStructure>>()) {
+ verify(contentCaptureSessionCompat, times(1)).notifyViewsAppeared(capture())
+ assertThat(firstValue.count()).isEqualTo(1)
+ }
+ clearInvocations(contentCaptureSessionCompat)
+ }
+
+ rule.runOnIdle { appeared = false }
+
+ // TODO(b/272068594): After refactoring this code, ensure that we don't need to wait for two
+ // invocations of boundsUpdatesEventLoop.
+ repeat(2) {
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+ rule.waitForIdle()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ verify(contentCaptureSessionCompat, times(0)).notifyViewsDisappeared(any())
+ verify(contentCaptureSessionCompat, times(0)).notifyViewsAppeared(any())
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testUpdateTranslationOnAppeared_showOriginal() {
+ // Arrange.
+ var appeared by mutableStateOf(false)
+ var result = true
+
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ ) {
+ if (appeared) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ text = AnnotatedString("foo")
+ isShowingTextSubstitution = true
+ showTextSubstitution {
+ result = it
+ true
+ }
+ }
+ )
+ }
+ }
+ }
+ rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onHideTranslation() }
+
+ // Act.
+ rule.runOnIdle { appeared = true }
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isFalse() }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testUpdateTranslationOnAppeared_showTranslated() {
+ // Arrange.
+ var appeared by mutableStateOf(false)
+ var result = false
+
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { }
+ ) {
+ if (appeared) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ text = AnnotatedString("foo")
+ isShowingTextSubstitution = false
+ showTextSubstitution {
+ result = it
+ true
+ }
+ }
+ )
+ }
+ }
+ }
+ rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onShowTranslation() }
+
+ // Act.
+ rule.runOnIdle { appeared = true }
+ rule.mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isTrue() }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testOnCreateVirtualViewTranslationRequests() {
+ // Arrange.
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ testTag = tag
+ text = AnnotatedString("foo")
+ }
+ )
+ }
+ }
+ val virtualViewId = rule.onNodeWithTag(tag).semanticsId
+
+ val ids = LongArray(1).apply { this[0] = virtualViewId.toLong() }
+ val requestsCollector: Consumer<ViewTranslationRequest?> = mock()
+
+ // Act.
+ rule.runOnIdle {
+ androidComposeView.onCreateVirtualViewTranslationRequests(
+ ids,
+ IntArray(0),
+ requestsCollector
+ )
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ with(argumentCaptor<ViewTranslationRequest>()) {
+ verify(requestsCollector).accept(capture())
+ assertThat(firstValue).isEqualTo(
+ ViewTranslationRequest
+ .Builder(androidComposeView.autofillId, virtualViewId.toLong())
+ .setValue(ID_TEXT, TranslationRequestValue.forText(AnnotatedString("foo")))
+ .build()
+ )
+ }
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testOnVirtualViewTranslationResponses() {
+ // Arrange.
+ var result: AnnotatedString? = null
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ testTag = tag
+ text = AnnotatedString("foo")
+ setTextSubstitution {
+ result = it
+ true
+ }
+ }
+ )
+ }
+ }
+ val virtualViewId = rule.onNodeWithTag(tag).semanticsId
+
+ // Act.
+ rule.runOnIdle {
+ androidComposeView.onVirtualViewTranslationResponses(
+ LongSparseArray<ViewTranslationResponse?>().apply {
+ append(
+ virtualViewId.toLong(),
+ ViewTranslationResponse
+ .Builder(androidComposeView.autofillId)
+ .setValue(
+ ID_TEXT,
+ TranslationResponseValue.Builder(0).setText("bar").build()
+ )
+ .build()
+ )
+ }
+ )
+ }
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isEqualTo(AnnotatedString("bar")) }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testOnShowTranslation() {
+ // Arrange.
+ var result = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ textSubstitution = AnnotatedString("foo")
+ isShowingTextSubstitution = false
+ showTextSubstitution {
+ result = it
+ true
+ }
+ }
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onShowTranslation() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isTrue() }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testOnHideTranslation() {
+ // Arrange.
+ var result = true
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ text = AnnotatedString("bar")
+ textSubstitution = AnnotatedString("foo")
+ isShowingTextSubstitution = true
+ showTextSubstitution {
+ result = it
+ true
+ }
+ }
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onHideTranslation() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isFalse() }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ fun testOnClearTranslation() {
+ // Arrange.
+ var result = false
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithContentCaptureEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics { text = AnnotatedString("bar") }
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics {
+ text = AnnotatedString("bar")
+ isShowingTextSubstitution = true
+ clearTextSubstitution {
+ result = true
+ true
+ }
+ }
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { androidComposeView.composeAccessibilityDelegate.onClearTranslation() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(result).isTrue() }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun ComposeContentTestRule.setContentWithContentCaptureEnabled(
+ retainInteractionsDuringInitialization: Boolean = false,
+ content: @Composable () -> Unit
+ ) {
+ contentCaptureSessionCompat = mock()
+ viewStructureCompat = mock()
+ val viewStructure: ViewStructure = mock()
+
+ whenever(contentCaptureSessionCompat.newVirtualViewStructure(any(), any()))
+ .thenReturn(viewStructureCompat)
+ whenever(viewStructureCompat.toViewStructure())
+ .thenReturn(viewStructure)
+
+ setContent {
+ androidComposeView = LocalView.current as AndroidComposeView
+ with(androidComposeView.composeAccessibilityDelegate) {
+ accessibilityForceEnabledForTesting = true
+ contentCaptureForceEnabledForTesting = true
+ contentCaptureSession = contentCaptureSessionCompat
+ onSendAccessibilityEvent = { dispatchedAccessibilityEvents += it; false }
+ }
+
+ whenever(contentCaptureSessionCompat.newAutofillId(any())).thenAnswer {
+ androidComposeView.autofillId
+ }
+
+ content()
+ }
+
+ // Advance the clock past the first accessibility event loop, and clear the initial
+ // as we are want the assertions to check the events that were generated later.
+ runOnIdle { mainClock.advanceTimeBy(contentCaptureEventLoopIntervalMs) }
+
+ runOnIdle {
+ if (!retainInteractionsDuringInitialization) {
+ clearInvocations(contentCaptureSessionCompat, viewStructureCompat)
+ }
+ }
+ }
+
+ private val View.composeAccessibilityDelegate: AndroidComposeViewAccessibilityDelegateCompat
+ get() = ViewCompat.getAccessibilityDelegate(this)
+ as AndroidComposeViewAccessibilityDelegateCompat
+
+ // TODO(b/272068594): Add api to fetch the semantics id from SemanticsNodeInteraction directly.
+ private val SemanticsNodeInteraction.semanticsId: Int get() = fetchSemanticsNode().id
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
new file mode 100644
index 0000000..04494ed
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
@@ -0,0 +1,573 @@
+
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.accessibility
+
+import android.graphics.Rect
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import android.os.Build.VERSION_CODES.R
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
+import android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED
+import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ScrollingTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<TestActivity>()
+
+ private val tag = "tag"
+ private lateinit var androidComposeView: AndroidComposeView
+ private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
+ private val accessibilityEventLoopIntervalMs = 100L
+
+ @Test
+ fun sendScrollEvent_byStateObservation_horizontal() {
+ // Arrange.
+ var scrollValue by mutableStateOf(0f, structuralEqualityPolicy())
+ val scrollMaxValue = 100f
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Row(
+ Modifier
+ .size(20.toDp(), 10.toDp())
+ .semantics(mergeDescendants = false) {
+ horizontalScrollAxisRange = ScrollAxisRange(
+ { scrollValue },
+ { scrollMaxValue }
+ )
+ }
+ ) {
+ Text("foo", Modifier.size(10.toDp()))
+ Text("bar",
+ Modifier
+ .size(10.toDp())
+ .testTag(tag))
+ }
+ }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+ val virtualViewId = rule.onNodeWithTag(tag).semanticsId
+ rule.runOnIdle { dispatchedAccessibilityEvents.clear() }
+
+ // Act.
+ try {
+ androidComposeView.snapshotObserver.startObserving()
+ rule.runOnIdle {
+ androidComposeView.accessibilityNodeProvider
+ .performAction(virtualViewId, ACTION_ACCESSIBILITY_FOCUS, null)
+ Snapshot.notifyObjectsInitialized()
+ scrollValue = 2f
+ Snapshot.sendApplyNotifications()
+ }
+ } finally {
+ androidComposeView.snapshotObserver.stopObserving()
+ }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ val focusedANI = androidComposeView.accessibilityNodeProvider
+ .findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
+ assertThat(Rect().also { focusedANI?.getBoundsInScreen(it) })
+ .isEqualTo(Rect(10, 0, 20, 10))
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ },
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_SUBTREE
+ },
+ AccessibilityEvent().apply {
+ eventType = TYPE_VIEW_SCROLLED
+ scrollX = 2
+ maxScrollX = 100
+ },
+ )
+ }
+ }
+
+ @Test
+ fun sendScrollEvent_byStateObservation_vertical() {
+ // Arrange.
+ var scrollValue by mutableStateOf(0f, structuralEqualityPolicy())
+ val scrollMaxValue = 100f
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ verticalScrollAxisRange = ScrollAxisRange(
+ { scrollValue },
+ { scrollMaxValue }
+ )
+ }
+ )
+ }
+
+ // TODO(b/272068594): We receive an extra TYPE_WINDOW_CONTENT_CHANGED event 100ms after
+ // setup. So we wait an extra 100ms here so that this test is not affected by that extra
+ // event.
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+ dispatchedAccessibilityEvents.clear()
+
+ // Act.
+ try {
+ androidComposeView.snapshotObserver.startObserving()
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+ rule.runOnIdle {
+ Snapshot.notifyObjectsInitialized()
+ scrollValue = 2f
+ Snapshot.sendApplyNotifications()
+ }
+ } finally {
+ androidComposeView.snapshotObserver.stopObserving()
+ }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_SUBTREE
+ },
+ AccessibilityEvent().apply {
+ eventType = TYPE_VIEW_SCROLLED
+ scrollY = 2
+ maxScrollY = 100
+ },
+ )
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenPositionInvalid() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.dp)
+ .semantics(mergeDescendants = true) {
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsTrue_whenHorizontalScrollableNotAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0.5f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ // Should be scrollable in both directions.
+ assertThat(androidComposeView.canScrollHorizontally(1)).isTrue()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isTrue()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsTrue_whenVerticalScrollableNotAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ verticalScrollAxisRange = ScrollAxisRange(
+ value = { 0.5f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ // Should be scrollable in both directions.
+ assertThat(androidComposeView.canScrollVertically(1)).isTrue()
+ assertThat(androidComposeView.canScrollVertically(0)).isTrue()
+ assertThat(androidComposeView.canScrollVertically(-1)).isTrue()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenHorizontalScrollable_whenScrolledRightAndAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 1f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenHorizontalScrollable_whenScrolledLeftAndAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollHorizontally(1)).isTrue()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isTrue()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenVerticalScrollable_whenScrolledDownAndAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ verticalScrollAxisRange = ScrollAxisRange(
+ value = { 1f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollVertically(1)).isFalse()
+ assertThat(androidComposeView.canScrollVertically(0)).isFalse()
+ assertThat(androidComposeView.canScrollVertically(-1)).isTrue()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenVerticalScrollable_whenScrolledUpAndAtLimit() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ verticalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ reverseScrolling = false
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollVertically(1)).isTrue()
+ assertThat(androidComposeView.canScrollVertically(0)).isTrue()
+ assertThat(androidComposeView.canScrollVertically(-1)).isFalse()
+ }
+ }
+
+ @Test
+ fun canScroll_respectsReverseDirection() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0f },
+ maxValue = { 1f },
+ reverseScrolling = true
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isTrue()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_forVertical_whenScrollableIsHorizontal() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(100.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0.5f },
+ maxValue = { 1f },
+ reverseScrolling = true
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(50f, 50f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollVertically(1)).isFalse()
+ assertThat(androidComposeView.canScrollVertically(0)).isFalse()
+ assertThat(androidComposeView.canScrollVertically(-1)).isFalse()
+ }
+ }
+
+ @Test
+ fun canScroll_returnsFalse_whenTouchIsOutsideBounds() {
+ // Arrange.
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(50.toDp())
+ .semantics(mergeDescendants = true) {
+ testTag = tag
+ horizontalScrollAxisRange = ScrollAxisRange(
+ value = { 0.5f },
+ maxValue = { 1f },
+ reverseScrolling = true
+ )
+ }
+ )
+ }
+
+ // Act.
+ rule.onNodeWithTag(tag).performTouchInput { down(Offset(100f, 100f)) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(androidComposeView.canScrollHorizontally(1)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(0)).isFalse()
+ assertThat(androidComposeView.canScrollHorizontally(-1)).isFalse()
+ }
+ }
+
+ private fun Int.toDp(): Dp = with(rule.density) { [email protected]() }
+
+ private fun ComposeContentTestRule.setContentWithAccessibilityEnabled(
+ content: @Composable () -> Unit
+ ) {
+ setContent {
+ androidComposeView = LocalView.current as AndroidComposeView
+ with(androidComposeView.composeAccessibilityDelegate) {
+ accessibilityForceEnabledForTesting = true
+ onSendAccessibilityEvent = { dispatchedAccessibilityEvents += it; false }
+ }
+ content()
+ }
+
+ // Advance the clock past the first accessibility event loop, and clear the initial
+ // events as we are want the assertions to check the events that were generated later.
+ runOnIdle { mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs) }
+ runOnIdle { dispatchedAccessibilityEvents.clear() }
+ }
+
+ companion object {
+
+ internal val AccessibilityEventComparator = Correspondence
+ .from<AccessibilityEvent, AccessibilityEvent>(
+ { actual, expected ->
+ actual != null && expected != null &&
+ actual.eventType == expected.eventType &&
+ actual.eventTime == expected.eventTime &&
+ actual.packageName == expected.packageName &&
+ actual.movementGranularity == expected.movementGranularity &&
+ actual.action == expected.action &&
+ actual.contentChangeTypes == expected.contentChangeTypes &&
+ (SDK_INT < P || actual.windowChanges == expected.windowChanges) &&
+ actual.className.contentEquals(expected.className) &&
+ actual.text.toString() == expected.text.toString() &&
+ actual.contentDescription.contentEquals(expected.contentDescription) &&
+ actual.itemCount == expected.itemCount &&
+ actual.currentItemIndex == expected.currentItemIndex &&
+ actual.isEnabled == expected.isEnabled &&
+ actual.isPassword == expected.isPassword &&
+ actual.isChecked == expected.isChecked &&
+ actual.isFullScreen == expected.isFullScreen &&
+ actual.isScrollable == expected.isScrollable &&
+ actual.beforeText.contentEquals(expected.beforeText) &&
+ actual.fromIndex == expected.fromIndex &&
+ actual.toIndex == expected.toIndex &&
+ actual.scrollX == expected.scrollX &&
+ actual.scrollY == expected.scrollY &&
+ actual.maxScrollX == expected.maxScrollX &&
+ actual.maxScrollY == expected.maxScrollY &&
+ (SDK_INT < P || actual.scrollDeltaX == expected.scrollDeltaX) &&
+ (SDK_INT < P || actual.scrollDeltaY == expected.scrollDeltaY) &&
+ actual.addedCount == expected.addedCount &&
+ actual.removedCount == expected.removedCount &&
+ actual.parcelableData == expected.parcelableData &&
+ actual.recordCount == expected.recordCount
+ },
+ "has same properties as"
+ )
+ }
+
+ private val View.composeAccessibilityDelegate: AndroidComposeViewAccessibilityDelegateCompat
+ get() = ViewCompat.getAccessibilityDelegate(this)
+ as AndroidComposeViewAccessibilityDelegateCompat
+
+ // TODO(b/272068594): Add api to fetch the semantics id from SemanticsNodeInteraction directly.
+ private val SemanticsNodeInteraction.semanticsId: Int get() = fetchSemanticsNode().id
+
+ // TODO(b/304359126): Move this to AccessibilityEventCompat and use it wherever we use obtain().
+ private fun AccessibilityEvent(): AccessibilityEvent = if (SDK_INT >= R) {
+ android.view.accessibility.AccessibilityEvent()
+ } else {
+ @Suppress("DEPRECATION")
+ AccessibilityEvent.obtain()
+ }.apply {
+ packageName = "androidx.compose.ui.test"
+ className = "android.view.View"
+ isEnabled = true
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/WindowContentChangeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/WindowContentChangeTest.kt
new file mode 100644
index 0000000..b8f22d5
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/WindowContentChangeTest.kt
@@ -0,0 +1,366 @@
+
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.accessibility
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import android.os.Build.VERSION_CODES.R
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED
+import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class WindowContentChangeTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<TestActivity>()
+
+ private lateinit var androidComposeView: AndroidComposeView
+ private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
+ private val accessibilityEventLoopIntervalMs = 100L
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_whenPropertyAdded() {
+ // Arrange.
+ var addProperty by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ if (addProperty) disabled()
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { addProperty = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
+ }
+ )
+ }
+ }
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_whenPropertyRemoved() {
+ // Arrange.
+ var removeProperty by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ if (!removeProperty) disabled()
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { removeProperty = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
+ }
+ )
+ }
+ }
+
+ @Test
+ @Ignore("b/307823561")
+ fun sendWindowContentChangeUndefinedEventByDefault_onlyOnce_whenMultiplePropertiesChange() {
+ // Arrange.
+ var propertiesChanged by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ if (!propertiesChanged) {
+ disabled()
+ } else {
+ onClick { true }
+ }
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { propertiesChanged = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
+ }
+ )
+ }
+ }
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_standardActionWithTheSameLabel() {
+ // Arrange.
+ var newAction by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ if (!newAction) {
+ onClick(label = "action") { true }
+ } else {
+ onClick(label = "action") { true }
+ }
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { newAction = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle { assertThat(dispatchedAccessibilityEvents).isEmpty() }
+ }
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_standardActionWithDifferentLabels() {
+ // Arrange.
+ var newAction by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ if (!newAction) {
+ onClick(label = "action1") { true }
+ } else {
+ onClick(label = "action2") { true }
+ }
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { newAction = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
+ }
+ )
+ }
+ }
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_customActionWithTheSameLabel() {
+ // Arrange.
+ var newAction by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ customActions = if (!newAction) {
+ listOf(CustomAccessibilityAction("action") { true })
+ } else {
+ listOf(CustomAccessibilityAction("action") { false })
+ }
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { newAction = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle { assertThat(dispatchedAccessibilityEvents).isEmpty() }
+ }
+
+ @Test
+ fun sendWindowContentChangeUndefinedEventByDefault_customActionWithDifferentLabels() {
+ // Arrange.
+ var newAction by mutableStateOf(false)
+ rule.mainClock.autoAdvance = false
+ rule.setContentWithAccessibilityEnabled {
+ Box(
+ Modifier
+ .size(10.dp)
+ .semantics(mergeDescendants = false) {
+ customActions = if (!newAction) {
+ listOf(CustomAccessibilityAction("action1") { true })
+ } else {
+ listOf(CustomAccessibilityAction("action2") { true })
+ }
+ }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { newAction = true }
+ rule.mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(dispatchedAccessibilityEvents)
+ .comparingElementsUsing(AccessibilityEventComparator)
+ .containsExactly(
+ AccessibilityEvent().apply {
+ eventType = TYPE_WINDOW_CONTENT_CHANGED
+ contentChangeTypes = CONTENT_CHANGE_TYPE_UNDEFINED
+ }
+ )
+ }
+ }
+
+ private fun ComposeContentTestRule.setContentWithAccessibilityEnabled(
+ content: @Composable () -> Unit
+ ) {
+ setContent {
+ androidComposeView = LocalView.current as AndroidComposeView
+ with(androidComposeView.composeAccessibilityDelegate) {
+ accessibilityForceEnabledForTesting = true
+ onSendAccessibilityEvent = { dispatchedAccessibilityEvents += it; false }
+ }
+ content()
+ }
+
+ // Advance the clock past the first accessibility event loop, and clear the initial
+ // events as we are want the assertions to check the events that were generated later.
+ runOnIdle { mainClock.advanceTimeBy(accessibilityEventLoopIntervalMs) }
+ runOnIdle { dispatchedAccessibilityEvents.clear() }
+ }
+
+ companion object {
+ internal val AccessibilityEventComparator = Correspondence
+ .from<AccessibilityEvent, AccessibilityEvent>(
+ { actual, expected ->
+ actual != null && expected != null &&
+ actual.eventType == expected.eventType &&
+ actual.eventTime == expected.eventTime &&
+ actual.packageName == expected.packageName &&
+ actual.movementGranularity == expected.movementGranularity &&
+ actual.action == expected.action &&
+ actual.contentChangeTypes == expected.contentChangeTypes &&
+ (SDK_INT < P || actual.windowChanges == expected.windowChanges) &&
+ actual.className.contentEquals(expected.className) &&
+ actual.text.toString() == expected.text.toString() &&
+ actual.contentDescription.contentEquals(expected.contentDescription) &&
+ actual.itemCount == expected.itemCount &&
+ actual.currentItemIndex == expected.currentItemIndex &&
+ actual.isEnabled == expected.isEnabled &&
+ actual.isPassword == expected.isPassword &&
+ actual.isChecked == expected.isChecked &&
+ actual.isFullScreen == expected.isFullScreen &&
+ actual.isScrollable == expected.isScrollable &&
+ actual.beforeText.contentEquals(expected.beforeText) &&
+ actual.fromIndex == expected.fromIndex &&
+ actual.toIndex == expected.toIndex &&
+ actual.scrollX == expected.scrollX &&
+ actual.scrollY == expected.scrollY &&
+ actual.maxScrollX == expected.maxScrollX &&
+ actual.maxScrollY == expected.maxScrollY &&
+ (SDK_INT < P || actual.scrollDeltaX == expected.scrollDeltaX) &&
+ (SDK_INT < P || actual.scrollDeltaY == expected.scrollDeltaY) &&
+ actual.addedCount == expected.addedCount &&
+ actual.removedCount == expected.removedCount &&
+ actual.parcelableData == expected.parcelableData &&
+ actual.recordCount == expected.recordCount
+ },
+ "has same properties as"
+ )
+ }
+
+ private val View.composeAccessibilityDelegate: AndroidComposeViewAccessibilityDelegateCompat
+ get() = ViewCompat.getAccessibilityDelegate(this)
+ as AndroidComposeViewAccessibilityDelegateCompat
+
+ // TODO(b/304359126): Move this to AccessibilityEventCompat and use it wherever we use obtain().
+ private fun AccessibilityEvent(): AccessibilityEvent = if (SDK_INT >= R) {
+ android.view.accessibility.AccessibilityEvent()
+ } else {
+ @Suppress("DEPRECATION")
+ AccessibilityEvent.obtain()
+ }.apply {
+ packageName = "androidx.compose.ui.test"
+ className = "android.view.View"
+ isEnabled = true
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
index ddd1083..5c9a77e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/EditorInfoTest.kt
@@ -19,11 +19,11 @@
import android.text.InputType
import android.view.inputmethod.EditorInfo
import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.AndroidImeOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.update
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -544,7 +544,7 @@
val privateImeOptions = "testOptions"
info.update(
ImeOptions(
- platformImeOptions = AndroidImeOptions(privateImeOptions)
+ platformImeOptions = PlatformImeOptions(privateImeOptions)
)
)
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 7cfd71b..bce1f6e 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
@@ -49,9 +49,6 @@
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.R
import androidx.compose.ui.geometry.Offset
@@ -314,7 +311,7 @@
// traversal with granularity switches to the next node
private var previousTraversedNode: Int? = null
private val subtreeChangedLayoutNodes = ArraySet<LayoutNode>()
- private val boundsUpdateChannel = Channel<Unit>(Channel.CONFLATED)
+ private val boundsUpdateChannel = Channel<Unit>(1)
private var currentSemanticsNodesInvalidated = true
@VisibleForTesting
internal var contentCaptureForceEnabledForTesting = false
@@ -3798,4 +3795,4 @@
@get:ExperimentalComposeUiApi
@set:ExperimentalComposeUiApi
@ExperimentalComposeUiApi
-var DisableContentCapture: Boolean by mutableStateOf(false)
+var DisableContentCapture: Boolean = false
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index b3f0880..4ef0d8d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -504,7 +504,7 @@
ImeAction.Done -> EditorInfo.IME_ACTION_DONE
else -> error("invalid ImeAction")
}
- (imeOptions.platformImeOptions as? AndroidImeOptions)?.privateImeOptions?.let {
+ imeOptions.platformImeOptions?.privateImeOptions?.let {
privateImeOptions = it
}
when (imeOptions.keyboardType) {
diff --git a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
index 9bda3bb..601fbf8 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -91,8 +91,8 @@
is Serializable -> putSerializable(key, value)
else -> {
- if (Build.VERSION.SDK_INT >= 18 && value is IBinder) {
- BundleApi18ImplKt.putBinder(this, key, value)
+ if (value is IBinder) {
+ this.putBinder(key, value)
} else if (Build.VERSION.SDK_INT >= 21 && value is Size) {
BundleApi21ImplKt.putSize(this, key, value)
} else if (Build.VERSION.SDK_INT >= 21 && value is SizeF) {
@@ -111,13 +111,6 @@
*/
public fun bundleOf(): Bundle = Bundle(0)
-@RequiresApi(18)
-private object BundleApi18ImplKt {
- @DoNotInline
- @JvmStatic
- fun putBinder(bundle: Bundle, key: String, value: IBinder?) = bundle.putBinder(key, value)
-}
-
@RequiresApi(21)
private object BundleApi21ImplKt {
@DoNotInline
diff --git a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
index 9f4cd4e..d1dc409 100644
--- a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
@@ -280,14 +280,7 @@
*/
@SuppressWarnings("deprecation")
public static int getCapabilities(@NonNull AccessibilityServiceInfo info) {
- if (Build.VERSION.SDK_INT >= 18) {
- return info.getCapabilities();
- } else {
- if (info.getCanRetrieveWindowContent()) {
- return CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
- }
- return 0;
- }
+ return info.getCapabilities();
}
/**
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 61c464a..8464b94 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -907,12 +907,12 @@
// The Android framework supports per-app locales on API 33, so we assume the
// configuration has been updated after API 32.
- if (Build.VERSION.SDK_INT <= 32 && Build.VERSION.SDK_INT >= 17) {
+ if (Build.VERSION.SDK_INT <= 32) {
if (!locales.isEmpty()) {
Configuration newConfig = new Configuration(
context.getResources().getConfiguration());
ConfigurationCompat.setLocales(newConfig, locales);
- return Api17Impl.createConfigurationContext(context, newConfig);
+ return context.createConfigurationContext(newConfig);
}
}
return context;
@@ -1006,24 +1006,18 @@
SERVICES.put(TelecomManager.class, TELECOM_SERVICE);
SERVICES.put(TvInputManager.class, TV_INPUT_SERVICE);
}
- if (Build.VERSION.SDK_INT >= 19) {
- SERVICES.put(AppOpsManager.class, APP_OPS_SERVICE);
- SERVICES.put(CaptioningManager.class, CAPTIONING_SERVICE);
- SERVICES.put(ConsumerIrManager.class, CONSUMER_IR_SERVICE);
- SERVICES.put(PrintManager.class, PRINT_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 18) {
- SERVICES.put(BluetoothManager.class, BLUETOOTH_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 17) {
- SERVICES.put(DisplayManager.class, DISPLAY_SERVICE);
- SERVICES.put(UserManager.class, USER_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 16) {
- SERVICES.put(InputManager.class, INPUT_SERVICE);
- SERVICES.put(MediaRouter.class, MEDIA_ROUTER_SERVICE);
- SERVICES.put(NsdManager.class, NSD_SERVICE);
- }
+
+ SERVICES.put(AppOpsManager.class, APP_OPS_SERVICE);
+ SERVICES.put(CaptioningManager.class, CAPTIONING_SERVICE);
+ SERVICES.put(ConsumerIrManager.class, CONSUMER_IR_SERVICE);
+ SERVICES.put(PrintManager.class, PRINT_SERVICE);
+ SERVICES.put(BluetoothManager.class, BLUETOOTH_SERVICE);
+ SERVICES.put(DisplayManager.class, DISPLAY_SERVICE);
+ SERVICES.put(UserManager.class, USER_SERVICE);
+
+ SERVICES.put(InputManager.class, INPUT_SERVICE);
+ SERVICES.put(MediaRouter.class, MEDIA_ROUTER_SERVICE);
+ SERVICES.put(NsdManager.class, NSD_SERVICE);
SERVICES.put(AccessibilityManager.class, ACCESSIBILITY_SERVICE);
SERVICES.put(AccountManager.class, ACCOUNT_SERVICE);
SERVICES.put(ActivityManager.class, ACTIVITY_SERVICE);
@@ -1056,18 +1050,6 @@
}
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static Context createConfigurationContext(Context obj, Configuration config) {
- return obj.createConfigurationContext(config);
- }
- }
-
@RequiresApi(19)
static class Api19Impl {
private Api19Impl() {
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index a1ab899..883ff8a 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -497,16 +497,7 @@
* @see android.location.LocationManager#addTestProvider
*/
public static boolean isMock(@NonNull Location location) {
- if (VERSION.SDK_INT >= 18) {
- return Api18Impl.isMock(location);
- } else {
- Bundle extras = location.getExtras();
- if (extras == null) {
- return false;
- }
-
- return extras.getBoolean(EXTRA_IS_MOCK, false);
- }
+ return location.isFromMockProvider();
}
/**
@@ -517,9 +508,9 @@
* boolean extra with the key {@link #EXTRA_IS_MOCK} to mark the location as mock. Be aware that
* this will overwrite any prior extra value under the same key.
*/
+ @SuppressLint("BanUncheckedReflection")
public static void setMock(@NonNull Location location, boolean mock) {
- if (VERSION.SDK_INT >= 18) {
- try {
+ try {
getSetIsFromMockProviderMethod().invoke(location, mock);
} catch (NoSuchMethodException e) {
Error error = new NoSuchMethodError();
@@ -532,25 +523,6 @@
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
- } else {
- Bundle extras = location.getExtras();
- if (extras == null) {
- if (mock) {
- extras = new Bundle();
- extras.putBoolean(EXTRA_IS_MOCK, true);
- location.setExtras(extras);
- }
- } else {
- if (mock) {
- extras.putBoolean(EXTRA_IS_MOCK, true);
- } else {
- extras.remove(EXTRA_IS_MOCK);
- if (extras.isEmpty()) {
- location.setExtras(null);
- }
- }
- }
- }
}
@RequiresApi(34)
@@ -940,18 +912,6 @@
}
}
- @RequiresApi(18)
- private static class Api18Impl {
-
- private Api18Impl() {
- }
-
- @DoNotInline
- static boolean isMock(Location location) {
- return location.isFromMockProvider();
- }
- }
-
private static Method getSetIsFromMockProviderMethod() throws NoSuchMethodException {
if (sSetIsFromMockProviderMethod == null) {
sSetIsFromMockProviderMethod = Location.class.getDeclaredMethod("setIsFromMockProvider",
diff --git a/core/core/src/main/java/androidx/core/os/BundleCompat.java b/core/core/src/main/java/androidx/core/os/BundleCompat.java
index 94555cf..3aea8a8 100644
--- a/core/core/src/main/java/androidx/core/os/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BundleCompat.java
@@ -21,7 +21,6 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
-import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.DoNotInline;
@@ -29,8 +28,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.ArrayList;
/**
@@ -192,11 +189,7 @@
*/
@Nullable
public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api18Impl.getBinder(bundle, key);
- } else {
- return BeforeApi18Impl.getBinder(bundle, key);
- }
+ return bundle.getBinder(key);
}
/**
@@ -209,11 +202,7 @@
*/
public static void putBinder(@NonNull Bundle bundle, @Nullable String key,
@Nullable IBinder binder) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.putBinder(bundle, key, binder);
- } else {
- BeforeApi18Impl.putBinder(bundle, key, binder);
- }
+ bundle.putBinder(key, binder);
}
@RequiresApi(33)
@@ -246,84 +235,4 @@
return in.getSparseParcelableArray(key, clazz);
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static IBinder getBinder(Bundle bundle, String key) {
- return bundle.getBinder(key);
- }
-
- @DoNotInline
- static void putBinder(Bundle bundle, String key, IBinder value) {
- bundle.putBinder(key, value);
- }
- }
-
- @SuppressLint("BanUncheckedReflection") // Only called prior to API 18
- static class BeforeApi18Impl {
- private static final String TAG = "BundleCompat";
-
- private static Method sGetIBinderMethod;
- private static boolean sGetIBinderMethodFetched;
-
- private static Method sPutIBinderMethod;
- private static boolean sPutIBinderMethodFetched;
-
- private BeforeApi18Impl() {
- // This class is not instantiable.
- }
-
- @SuppressWarnings("JavaReflectionMemberAccess")
- public static IBinder getBinder(Bundle bundle, String key) {
- if (!sGetIBinderMethodFetched) {
- try {
- sGetIBinderMethod = Bundle.class.getMethod("getIBinder", String.class);
- sGetIBinderMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to retrieve getIBinder method", e);
- }
- sGetIBinderMethodFetched = true;
- }
-
- if (sGetIBinderMethod != null) {
- try {
- return (IBinder) sGetIBinderMethod.invoke(bundle, key);
- } catch (InvocationTargetException | IllegalAccessException
- | IllegalArgumentException e) {
- Log.i(TAG, "Failed to invoke getIBinder via reflection", e);
- sGetIBinderMethod = null;
- }
- }
- return null;
- }
-
- @SuppressWarnings("JavaReflectionMemberAccess")
- public static void putBinder(Bundle bundle, String key, IBinder binder) {
- if (!sPutIBinderMethodFetched) {
- try {
- sPutIBinderMethod =
- Bundle.class.getMethod("putIBinder", String.class, IBinder.class);
- sPutIBinderMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to retrieve putIBinder method", e);
- }
- sPutIBinderMethodFetched = true;
- }
-
- if (sPutIBinderMethod != null) {
- try {
- sPutIBinderMethod.invoke(bundle, key, binder);
- } catch (InvocationTargetException | IllegalAccessException
- | IllegalArgumentException e) {
- Log.i(TAG, "Failed to invoke putIBinder via reflection", e);
- sPutIBinderMethod = null;
- }
- }
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/os/TraceCompat.java b/core/core/src/main/java/androidx/core/os/TraceCompat.java
index f5022c6..7689534 100644
--- a/core/core/src/main/java/androidx/core/os/TraceCompat.java
+++ b/core/core/src/main/java/androidx/core/os/TraceCompat.java
@@ -81,7 +81,7 @@
public static boolean isEnabled() {
if (Build.VERSION.SDK_INT >= 29) {
return Api29Impl.isEnabled();
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
return (boolean) sIsTagEnabledMethod.invoke(null, sTraceTagApp);
} catch (Exception e) {
@@ -105,9 +105,7 @@
* most 127 Unicode code units long.
*/
public static void beginSection(@NonNull String sectionName) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.beginSection(sectionName);
- }
+ Trace.beginSection(sectionName);
}
/**
@@ -118,9 +116,7 @@
* thread.
*/
public static void endSection() {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.endSection();
- }
+ Trace.endSection();
}
/**
@@ -136,7 +132,7 @@
public static void beginAsyncSection(@NonNull String methodName, int cookie) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.beginAsyncSection(methodName, cookie);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sAsyncTraceBeginMethod.invoke(null, sTraceTagApp, methodName, cookie);
} catch (Exception e) {
@@ -156,7 +152,7 @@
public static void endAsyncSection(@NonNull String methodName, int cookie) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.endAsyncSection(methodName, cookie);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sAsyncTraceEndMethod.invoke(null, sTraceTagApp, methodName, cookie);
} catch (Exception e) {
@@ -175,7 +171,7 @@
public static void setCounter(@NonNull String counterName, int counterValue) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.setCounter(counterName, counterValue);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sTraceCounterMethod.invoke(null, sTraceTagApp, counterName, counterValue);
} catch (Exception e) {
@@ -213,21 +209,4 @@
Trace.setCounter(counterName, counterValue);
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void beginSection(String sectionName) {
- Trace.beginSection(sectionName);
- }
-
- @DoNotInline
- static void endSection() {
- Trace.endSection();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
index 207e55f..3034440 100644
--- a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
@@ -113,10 +113,7 @@
* @see #setLayoutMode(ViewGroup, int)
*/
public static int getLayoutMode(@NonNull ViewGroup group) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api18Impl.getLayoutMode(group);
- }
- return LAYOUT_MODE_CLIP_BOUNDS;
+ return group.getLayoutMode();
}
/**
@@ -130,9 +127,7 @@
* @see #getLayoutMode(ViewGroup)
*/
public static void setLayoutMode(@NonNull ViewGroup group, int mode) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.setLayoutMode(group, mode);
- }
+ group.setLayoutMode(mode);
}
/**
@@ -191,23 +186,6 @@
return ViewCompat.SCROLL_AXIS_NONE;
}
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getLayoutMode(ViewGroup viewGroup) {
- return viewGroup.getLayoutMode();
- }
-
- @DoNotInline
- static void setLayoutMode(ViewGroup viewGroup, int layoutMode) {
- viewGroup.setLayoutMode(layoutMode);
- }
- }
-
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index fec6650..26c0417 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -18,6 +18,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
@@ -450,6 +451,7 @@
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
*/
+ @SuppressLint("WrongConstant")
@ContentChangeType
public static int getContentChangeTypes(@NonNull AccessibilityEvent event) {
if (Build.VERSION.SDK_INT >= 19) {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 48f9d84..f980b7b 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -3789,9 +3789,7 @@
* @param viewId The id resource name.
*/
public void setViewIdResourceName(String viewId) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setViewIdResourceName(viewId);
- }
+ mInfo.setViewIdResourceName(viewId);
}
/**
@@ -3807,11 +3805,7 @@
* @return The id resource name.
*/
public String getViewIdResourceName() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getViewIdResourceName();
- } else {
- return null;
- }
+ return mInfo.getViewIdResourceName();
}
/**
@@ -4433,9 +4427,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setTextSelection(int start, int end) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setTextSelection(start, end);
- }
+ mInfo.setTextSelection(start, end);
}
/**
@@ -4444,11 +4436,7 @@
* @return The text selection start if there is selection or -1.
*/
public int getTextSelectionStart() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getTextSelectionStart();
- } else {
- return -1;
- }
+ return mInfo.getTextSelectionStart();
}
/**
@@ -4457,11 +4445,7 @@
* @return The text selection end if there is selection or -1.
*/
public int getTextSelectionEnd() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getTextSelectionEnd();
- } else {
- return -1;
- }
+ return mInfo.getTextSelectionEnd();
}
/**
@@ -4642,11 +4626,7 @@
* @return True if the node is editable, false otherwise.
*/
public boolean isEditable() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.isEditable();
- } else {
- return false;
- }
+ return mInfo.isEditable();
}
/**
@@ -4662,9 +4642,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setEditable(boolean editable) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setEditable(editable);
- }
+ mInfo.setEditable(editable);
}
/**
@@ -4958,11 +4936,7 @@
* @return Whether the refresh succeeded.
*/
public boolean refresh() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.refresh();
- } else {
- return false;
- }
+ return mInfo.refresh();
}
/**
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index b63d1df..df34fd0 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -120,14 +120,7 @@
public static void setCompoundDrawablesRelative(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelative(textView, start, top, end, bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawables(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelative(start, top, end, bottom);
}
/**
@@ -152,16 +145,7 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end,
- bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
}
/**
@@ -185,16 +169,7 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
@DrawableRes int bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end,
- bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
}
/**
@@ -235,22 +210,7 @@
*/
@NonNull
public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api17Impl.getCompoundDrawablesRelative(textView);
- }
- if (Build.VERSION.SDK_INT >= 17) {
- final boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- final Drawable[] compounds = textView.getCompoundDrawables();
- if (rtl) {
- // If we're on RTL, we need to invert the horizontal result like above
- final Drawable start = compounds[2];
- final Drawable end = compounds[0];
- compounds[0] = start;
- compounds[2] = end;
- }
- return compounds;
- }
- return textView.getCompoundDrawables();
+ return textView.getCompoundDrawablesRelative();
}
/**
@@ -815,9 +775,7 @@
builder.setBreakStrategy(Api23Impl.getBreakStrategy(textView));
builder.setHyphenationFrequency(Api23Impl.getHyphenationFrequency(textView));
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- builder.setTextDirection(getTextDirectionHeuristic(textView));
- }
+ builder.setTextDirection(getTextDirectionHeuristic(textView));
return builder.build();
}
}
@@ -833,9 +791,7 @@
// There is no way of setting text direction heuristics to TextView.
// Convert to the View's text direction int values.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- Api17Impl.setTextDirection(textView, getTextDirection(params.getTextDirection()));
- }
+ textView.setTextDirection(getTextDirection(params.getTextDirection()));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
float paintTextScaleX = params.getTextPaint().getTextScaleX();
@@ -910,7 +866,7 @@
// have LTR digits, but some locales, such as those written in the Adlam or N'Ko
// scripts, have RTL digits.
final DecimalFormatSymbols symbols =
- Api24Impl.getInstance(Api17Impl.getTextLocale(textView));
+ Api24Impl.getInstance(textView.getTextLocale());
final String zero = Api28Impl.getDigitStrings(symbols)[0];
// In case the zero digit is multi-codepoint, just use the first codepoint to
// determine direction.
@@ -927,10 +883,10 @@
// Always need to resolve layout direction first
final boolean defaultIsRtl =
- (Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL);
+ (textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
// Now, we can select the heuristic
- switch (Api17Impl.getTextDirection(textView)) {
+ switch (textView.getTextDirection()) {
default:
case TEXT_DIRECTION_FIRST_STRONG:
return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
@@ -953,7 +909,6 @@
/**
* Convert TextDirectionHeuristic to TextDirection int values
*/
- @RequiresApi(18)
private static int getTextDirection(@NonNull TextDirectionHeuristic heuristic) {
if (heuristic == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
return TEXT_DIRECTION_FIRST_STRONG;
@@ -1045,56 +1000,6 @@
return null;
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelative(TextView textView, Drawable start, Drawable top,
- Drawable end, Drawable bottom) {
- textView.setCompoundDrawablesRelative(start, top, end, bottom);
- }
-
- @DoNotInline
- static int getLayoutDirection(View view) {
- return view.getLayoutDirection();
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView,
- Drawable start, Drawable top, Drawable end, Drawable bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, int start,
- int top, int end, int bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @DoNotInline
- static Drawable[] getCompoundDrawablesRelative(TextView textView) {
- return textView.getCompoundDrawablesRelative();
- }
-
- @DoNotInline
- static void setTextDirection(View view, int textDirection) {
- view.setTextDirection(textDirection);
- }
-
- @DoNotInline
- static Locale getTextLocale(TextView textView) {
- return textView.getTextLocale();
- }
-
- @DoNotInline
- static int getTextDirection(View view) {
- return view.getTextDirection();
- }
- }
-
@RequiresApi(26)
static class Api26Impl {
private Api26Impl() {
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 491d65e..9fecfb4 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -30,6 +30,7 @@
Configuration cache entry reused\.
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ from cache
Configuration cache entry stored\.
+Calculating task graph as no cached configuration is available for tasks.*
See the profiling report at\: file\:\/\/\$OUT_DIR\/androidx\/build\/reports\/profile\/profile\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\.html
# > Task :lifecycle:lifecycle-common:compileJava
Note: \$[^ ]+ uses or overrides a deprecated API\.
diff --git a/development/update-verification-metadata.sh b/development/update-verification-metadata.sh
index e588ec0..70b1472 100755
--- a/development/update-verification-metadata.sh
+++ b/development/update-verification-metadata.sh
@@ -73,10 +73,6 @@
# rename keyring
mv gradle/verification-keyring-dryrun.keys gradle/verification-keyring.keys 2>/dev/null || true
-
- # remove temporary files
- rm -f gradle/verification-keyring-dryrun.gpg
- rm -f gradle/verification-keyring.gpg
}
regenerateVerificationMetadata
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 985e0cd..251972d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -21,7 +21,7 @@
<string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONES I EMOCIONS"</string>
<string name="emoji_category_people" msgid="7968173366822927025">"PERSONES"</string>
<string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS I NATURALESA"</string>
- <string name="emoji_category_food_drink" msgid="1189971856721244395">"MENJAR I BEGUDES"</string>
+ <string name="emoji_category_food_drink" msgid="1189971856721244395">"MENJAR I BEGUDA"</string>
<string name="emoji_category_travel_places" msgid="8105712773237012672">"VIATGES I LLOCS"</string>
<string name="emoji_category_activity" msgid="4381135114947330911">"ACTIVITATS I ESDEVENIMENTS"</string>
<string name="emoji_category_objects" msgid="6106115586332708067">"OBJECTES"</string>
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
index 43f6429..e9dd1a8 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
@@ -18,7 +18,6 @@
import android.animation.LayoutTransition
import android.content.Context
import android.graphics.Canvas
-import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
@@ -178,13 +177,6 @@
* @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
*/
public override fun setLayoutTransition(transition: LayoutTransition?) {
- if (Build.VERSION.SDK_INT < 18) {
- // Transitions on APIs below 18 are using an empty LayoutTransition as a replacement
- // for suppressLayout(true) and null LayoutTransition to then unsuppress it. If the
- // API is below 18, we should allow FrameLayout to handle this call.
- super.setLayoutTransition(transition)
- return
- }
throw UnsupportedOperationException(
"FragmentContainerView does not support Layout Transitions or " +
"animateLayoutChanges=\"true\"."
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d7c926c..6fcece3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -39,10 +39,10 @@
jcodec = "0.2.5"
kotlin17 = "1.7.10"
kotlin18 = "1.8.22"
-kotlin19 = "1.9.20"
-kotlin = "1.9.20"
+kotlin19 = "1.9.21"
+kotlin = "1.9.21"
kotlinBenchmark = "0.4.8"
-kotlinNative = "1.9.20"
+kotlinNative = "1.9.21"
kotlinCompileTesting = "1.4.9"
kotlinCoroutines = "1.7.3"
kotlinSerialization = "1.3.3"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index e0c828e..e5f9139 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -1,9 +1,10 @@
<?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">
+<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.3.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>true</verify-signatures>
+ <keyring-format>armored</keyring-format>
<key-servers enabled="false">
<key-server uri="https://keyserver.ubuntu.com"/>
<key-server uri="https://keys.openpgp.org"/>
@@ -477,19 +478,19 @@
</trusted-keys>
</configuration>
<components>
- <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.20">
- <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.20.tar.gz">
- <sha256 value="21899334495a5340c5504b1f7698db425b94ffeb633d809668c308e1f15bf585" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.20.tar.gz" reason="Artifact is not signed"/>
+ <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.21">
+ <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.21.tar.gz">
+ <sha256 value="8fbabb93092de6047345f1a9134e41e778828d51e70a44a7a50044b8471ce900" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.21.tar.gz" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.20">
- <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.20.tar.gz">
- <sha256 value="1c6a149c98d7c2f557b638465b415152d30b80746a3582235b1cd0484d320da8" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.20.tar.gz"/>
+ <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.21">
+ <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.21.tar.gz">
+ <sha256 value="8a05fb4645f5252143e6262e9207f60902ab4641ae08bc7cb40f93b47771358f" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.21.tar.gz"/>
</artifact>
</component>
- <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.20">
- <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.20.tar.gz">
- <sha256 value="3e39493d86f8175a34698a5c884e127dde04390629942f3c52eeca5bdc1ccf56" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.20.tar.gz"/>
+ <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.21">
+ <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.21.tar.gz">
+ <sha256 value="11c8e5764f05541d23c09db75d1f975930cfe38962bb264ad0719e45fb0e6f9c" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.21.tar.gz"/>
</artifact>
</component>
<component group="aopalliance" name="aopalliance" version="1.0">
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 48a5558..570dfce 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-8.4-bin.zip
-distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
+distributionUrl=../../../../tools/external/gradle/gradle-8.5-bin.zip
+distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 45c5536..11901b7e 100755
--- a/gradlew
+++ b/gradlew
@@ -43,12 +43,6 @@
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-if [[ " ${@} " =~ " -PupdateLintBaseline " ]]; then
- # remove when b/188666845 is complete
- # Inform lint to not fail even when creating a baseline file
- JAVA_OPTS="$JAVA_OPTS -Dlint.baselines.continue=true"
-fi
-
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
diff --git a/graphics/graphics-core/api/1.0.0-beta01.txt b/graphics/graphics-core/api/1.0.0-beta01.txt
index e1b45be..0019e2b 100644
--- a/graphics/graphics-core/api/1.0.0-beta01.txt
+++ b/graphics/graphics-core/api/1.0.0-beta01.txt
@@ -13,6 +13,7 @@
method public void setLightSourceAlpha(float ambientShadowAlpha, float spotShadowAlpha);
method public void setLightSourceGeometry(float lightX, float lightY, float lightZ, float lightRadius);
property public final int bufferFormat;
+ property public final boolean isClosed;
property public final int maxBuffers;
property public final long usageFlags;
}
@@ -26,7 +27,8 @@
}
public final class CanvasBufferedRenderer.RenderRequest {
- method public void draw(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
+ method public suspend Object? draw(optional boolean waitForFence, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>);
+ method public void drawAsync(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest preserveContents(boolean preserve);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setBufferTransform(int bufferTransform);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setColorSpace(android.graphics.ColorSpace? colorSpace);
diff --git a/graphics/graphics-core/api/current.ignore b/graphics/graphics-core/api/current.ignore
new file mode 100644
index 0000000..af77f0d
--- /dev/null
+++ b/graphics/graphics-core/api/current.ignore
@@ -0,0 +1,11 @@
+// Baseline format: 1.0
+AddedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#draw(boolean, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Added method androidx.graphics.CanvasBufferedRenderer.RenderRequest.draw(boolean,kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+AddedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#drawAsync(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Added method androidx.graphics.CanvasBufferedRenderer.RenderRequest.drawAsync(java.util.concurrent.Executor,androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+
+
+RemovedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#draw(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Removed method androidx.graphics.CanvasBufferedRenderer.RenderRequest.draw(java.util.concurrent.Executor,androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+RemovedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#drawSync(boolean):
+ Removed method androidx.graphics.CanvasBufferedRenderer.RenderRequest.drawSync(boolean)
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index e1b45be..0019e2b 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -13,6 +13,7 @@
method public void setLightSourceAlpha(float ambientShadowAlpha, float spotShadowAlpha);
method public void setLightSourceGeometry(float lightX, float lightY, float lightZ, float lightRadius);
property public final int bufferFormat;
+ property public final boolean isClosed;
property public final int maxBuffers;
property public final long usageFlags;
}
@@ -26,7 +27,8 @@
}
public final class CanvasBufferedRenderer.RenderRequest {
- method public void draw(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
+ method public suspend Object? draw(optional boolean waitForFence, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>);
+ method public void drawAsync(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest preserveContents(boolean preserve);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setBufferTransform(int bufferTransform);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setColorSpace(android.graphics.ColorSpace? colorSpace);
diff --git a/graphics/graphics-core/api/restricted_1.0.0-beta01.txt b/graphics/graphics-core/api/restricted_1.0.0-beta01.txt
index 74ac194..3119011d 100644
--- a/graphics/graphics-core/api/restricted_1.0.0-beta01.txt
+++ b/graphics/graphics-core/api/restricted_1.0.0-beta01.txt
@@ -13,6 +13,7 @@
method public void setLightSourceAlpha(float ambientShadowAlpha, float spotShadowAlpha);
method public void setLightSourceGeometry(float lightX, float lightY, float lightZ, float lightRadius);
property public final int bufferFormat;
+ property public final boolean isClosed;
property public final int maxBuffers;
property public final long usageFlags;
}
@@ -26,7 +27,8 @@
}
public final class CanvasBufferedRenderer.RenderRequest {
- method public void draw(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
+ method public suspend Object? draw(optional boolean waitForFence, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>);
+ method public void drawAsync(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest preserveContents(boolean preserve);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setBufferTransform(int bufferTransform);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setColorSpace(android.graphics.ColorSpace? colorSpace);
diff --git a/graphics/graphics-core/api/restricted_current.ignore b/graphics/graphics-core/api/restricted_current.ignore
new file mode 100644
index 0000000..af77f0d
--- /dev/null
+++ b/graphics/graphics-core/api/restricted_current.ignore
@@ -0,0 +1,11 @@
+// Baseline format: 1.0
+AddedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#draw(boolean, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Added method androidx.graphics.CanvasBufferedRenderer.RenderRequest.draw(boolean,kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+AddedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#drawAsync(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Added method androidx.graphics.CanvasBufferedRenderer.RenderRequest.drawAsync(java.util.concurrent.Executor,androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+
+
+RemovedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#draw(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>):
+ Removed method androidx.graphics.CanvasBufferedRenderer.RenderRequest.draw(java.util.concurrent.Executor,androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult>)
+RemovedMethod: androidx.graphics.CanvasBufferedRenderer.RenderRequest#drawSync(boolean):
+ Removed method androidx.graphics.CanvasBufferedRenderer.RenderRequest.drawSync(boolean)
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 74ac194..3119011d 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -13,6 +13,7 @@
method public void setLightSourceAlpha(float ambientShadowAlpha, float spotShadowAlpha);
method public void setLightSourceGeometry(float lightX, float lightY, float lightZ, float lightRadius);
property public final int bufferFormat;
+ property public final boolean isClosed;
property public final int maxBuffers;
property public final long usageFlags;
}
@@ -26,7 +27,8 @@
}
public final class CanvasBufferedRenderer.RenderRequest {
- method public void draw(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
+ method public suspend Object? draw(optional boolean waitForFence, kotlin.coroutines.Continuation<? super androidx.graphics.CanvasBufferedRenderer.RenderResult>);
+ method public void drawAsync(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.graphics.CanvasBufferedRenderer.RenderResult> callback);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest preserveContents(boolean preserve);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setBufferTransform(int bufferTransform);
method public androidx.graphics.CanvasBufferedRenderer.RenderRequest setColorSpace(android.graphics.ColorSpace? colorSpace);
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
index f5a4f87..9dfe084 100644
--- a/graphics/graphics-core/build.gradle
+++ b/graphics/graphics-core/build.gradle
@@ -25,6 +25,7 @@
dependencies {
api(libs.kotlinStdlib)
+ implementation(libs.kotlinCoroutinesAndroid)
implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
implementation("androidx.core:core:1.8.0")
androidTestImplementation(libs.testExtJunit)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
index 3a91718..9420dab 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
@@ -53,6 +53,7 @@
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.abs
+import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -72,16 +73,16 @@
fun testRenderAfterCloseReturnsError() = hardwareBufferRendererTest { renderer ->
renderer.close()
assertThrows(IllegalStateException::class.java) {
- renderer.obtainRenderRequest().draw(mExecutor) { _ -> /* NO-OP */ }
+ renderer.obtainRenderRequest().drawAsync(mExecutor) { _ -> /* NO-OP */ }
}
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testIsClosed() = hardwareBufferRendererTest { renderer ->
- assertFalse(renderer.isClosed())
+ assertFalse(renderer.isClosed)
renderer.close()
- assertTrue(renderer.isClosed())
+ assertTrue(renderer.isClosed)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@@ -106,7 +107,7 @@
var bitmap: Bitmap? = null
renderer.obtainRenderRequest()
.preserveContents(true)
- .draw(mExecutor) { result ->
+ .drawAsync(mExecutor) { result ->
assertEquals(SUCCESS, result.status)
result.fence?.awaitForever()
bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null)
@@ -122,7 +123,7 @@
latch = CountDownLatch(1)
renderer.obtainRenderRequest()
.preserveContents(false)
- .draw(mExecutor) { result ->
+ .drawAsync(mExecutor) { result ->
assertEquals(SUCCESS, result.status)
result.fence?.awaitForever()
bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null)
@@ -212,7 +213,7 @@
var bitmap: Bitmap? = null
renderer.obtainRenderRequest()
.preserveContents(true)
- .draw(executor) { result ->
+ .drawAsync(executor) { result ->
assertEquals(SUCCESS, result.status)
result.fence?.awaitForever()
bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null)
@@ -228,7 +229,7 @@
val secondRenderLatch = CountDownLatch(1)
renderer.obtainRenderRequest()
.preserveContents(true)
- .draw(executor) { result ->
+ .drawAsync(executor) { result ->
assertEquals(SUCCESS, result.status)
result.fence?.awaitForever()
bitmap = Bitmap.wrapHardwareBuffer(result.hardwareBuffer, null)
@@ -254,11 +255,13 @@
val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
val latch = CountDownLatch(1)
var hardwareBuffer: HardwareBuffer? = null
- renderer.obtainRenderRequest().setColorSpace(colorSpace).draw(mExecutor) { renderResult ->
- renderResult.fence?.awaitForever()
- hardwareBuffer = renderResult.hardwareBuffer
- latch.countDown()
- }
+ renderer.obtainRenderRequest()
+ .setColorSpace(colorSpace)
+ .drawAsync(mExecutor) { renderResult ->
+ renderResult.fence?.awaitForever()
+ hardwareBuffer = renderResult.hardwareBuffer
+ latch.countDown()
+ }
assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
@@ -272,6 +275,73 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
+ fun testDrawSync() = hardwareBufferRendererTest { renderer ->
+ val contentRoot = RenderNode("content").apply {
+ setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT)
+ record { canvas -> canvas.drawColor(Color.BLUE) }
+ }
+ renderer.setContentRoot(contentRoot)
+
+ val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
+
+ var renderResult: CanvasBufferedRenderer.RenderResult?
+ runBlocking {
+ renderResult = renderer.obtainRenderRequest().setColorSpace(colorSpace).draw()
+ }
+ assertNotNull(renderResult)
+ assertEquals(SUCCESS, renderResult!!.status)
+ val fence = renderResult?.fence
+ if (fence != null) {
+ // by default drawSync will automatically wait on the fence and close it leaving
+ // it in the invalid state
+ assertFalse(fence.isValid())
+ }
+
+ val hardwareBuffer = renderResult!!.hardwareBuffer
+
+ val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)!!
+ .copy(Bitmap.Config.ARGB_8888, false)
+
+ assertEquals(TEST_WIDTH, bitmap.width)
+ assertEquals(TEST_HEIGHT, bitmap.height)
+ assertEquals(0xFF0000FF.toInt(), bitmap.getPixel(0, 0))
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testDrawSyncWithoutBlockingFence() = hardwareBufferRendererTest { renderer ->
+ val contentRoot = RenderNode("content").apply {
+ setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT)
+ record { canvas -> canvas.drawColor(Color.BLUE) }
+ }
+ renderer.setContentRoot(contentRoot)
+
+ val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
+
+ var renderResult: CanvasBufferedRenderer.RenderResult?
+ runBlocking {
+ renderResult = renderer.obtainRenderRequest().setColorSpace(colorSpace).draw(false)
+ }
+ assertNotNull(renderResult)
+ assertEquals(SUCCESS, renderResult!!.status)
+ val fence = renderResult?.fence
+ assertNotNull(fence)
+ assertTrue(fence!!.isValid())
+ fence.awaitForever()
+ fence.close()
+
+ val hardwareBuffer = renderResult!!.hardwareBuffer
+
+ val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace)!!
+ .copy(Bitmap.Config.ARGB_8888, false)
+
+ assertEquals(TEST_WIDTH, bitmap.width)
+ assertEquals(TEST_HEIGHT, bitmap.height)
+ assertEquals(0xFF0000FF.toInt(), bitmap.getPixel(0, 0))
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
fun testContentsPreservedSRGB() = preservedContentsTest { bitmap ->
assertEquals(Color.RED, bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 4))
assertEquals(Color.BLUE, bitmap.getPixel(TEST_WIDTH / 2, TEST_HEIGHT / 2 + TEST_HEIGHT / 4))
@@ -346,7 +416,7 @@
renderer.obtainRenderRequest()
.setColorSpace(colorSpace)
.preserveContents(true)
- .draw(mExecutor) { renderResult ->
+ .drawAsync(mExecutor) { renderResult ->
renderResult.fence?.awaitForever()
hardwareBuffer = renderResult.hardwareBuffer
latch.countDown()
@@ -365,7 +435,7 @@
renderer.obtainRenderRequest()
.setColorSpace(colorSpace)
.preserveContents(true)
- .draw(mExecutor) { renderResult ->
+ .drawAsync(mExecutor) { renderResult ->
renderResult.fence?.awaitForever()
hardwareBuffer = renderResult.hardwareBuffer
latch2.countDown()
@@ -539,7 +609,7 @@
renderer.obtainRenderRequest()
.setColorSpace(colorSpace)
.setBufferTransform(42)
- .draw(mExecutor) { renderResult ->
+ .drawAsync(mExecutor) { renderResult ->
renderResult.fence?.awaitForever()
latch.countDown()
}
@@ -606,7 +676,7 @@
renderer.obtainRenderRequest()
.setColorSpace(colorSpace)
.setBufferTransform(transform)
- .draw(mExecutor) { renderResult ->
+ .drawAsync(mExecutor) { renderResult ->
renderStatus = renderResult.status
renderResult.fence?.awaitForever()
hardwareBuffer = renderResult.hardwareBuffer
@@ -708,7 +778,7 @@
val latch2 = CountDownLatch(1)
val latch3 = CountDownLatch(1)
var hardwareBuffer: HardwareBuffer? = null
- renderer.obtainRenderRequest().draw(executor) { result ->
+ renderer.obtainRenderRequest().drawAsync(executor) { result ->
result.fence?.awaitForever()
result.fence?.close()
hardwareBuffer = result.hardwareBuffer
@@ -721,7 +791,7 @@
canvas.drawColor(Color.BLUE)
renderNode.endRecording()
- renderer.obtainRenderRequest().draw(executor) { _ ->
+ renderer.obtainRenderRequest().drawAsync(executor) { _ ->
latch2.countDown()
}
@@ -731,7 +801,7 @@
canvas.drawColor(Color.GREEN)
renderNode.endRecording()
- renderer.obtainRenderRequest().draw(executor) { _ ->
+ renderer.obtainRenderRequest().drawAsync(executor) { _ ->
latch3.countDown()
}
@@ -849,7 +919,7 @@
renderNode.endRecording()
val latch = CountDownLatch(1)
- hbr.obtainRenderRequest().draw(executor) { result ->
+ hbr.obtainRenderRequest().drawAsync(executor) { result ->
hbr.releaseBuffer(result.hardwareBuffer, result.fence)
latch.countDown()
}
@@ -924,7 +994,7 @@
.setColorSpace(colorSpace)
.preserveContents(true)
.setBufferTransform(transform)
- .draw(executor) { renderResult ->
+ .drawAsync(executor) { renderResult ->
renderResult.fence?.awaitForever()
hardwareBuffer = renderResult.hardwareBuffer
latch.countDown()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRenderer.kt
index 742e437..7b58718 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRenderer.kt
@@ -31,13 +31,14 @@
import androidx.hardware.HardwareBufferFormat
import androidx.hardware.HardwareBufferUsage
import androidx.hardware.SyncFenceCompat
-import java.lang.IllegalStateException
import java.util.concurrent.Executor
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
/**
* Creates an instance of a hardware-accelerated renderer. This is used to render a scene built
* from [RenderNode]s to an output [HardwareBuffer]. There can be as many
- * HardwareBufferRenderer instances as desired.
+ * [CanvasBufferedRenderer] instances as desired.
*
* Resources & lifecycle
*
@@ -125,7 +126,8 @@
* Returns if the [CanvasBufferedRenderer] has already been closed. That is
* [CanvasBufferedRenderer.close] has been invoked.
*/
- fun isClosed(): Boolean = mImpl.isClosed()
+ val isClosed: Boolean
+ get() = mImpl.isClosed()
/**
* Returns a [RenderRequest] that can be used to render into the provided HardwareBuffer.
@@ -140,7 +142,7 @@
* Sets the content root to render. It is not necessary to call this whenever the content
* recording changes. Any mutations to the [RenderNode] content, or any of the [RenderNode]s
* contained within the content node, will be applied whenever a new [RenderRequest] is issued
- * via [obtainRenderRequest] and [RenderRequest.draw].
+ * via [obtainRenderRequest] and [RenderRequest.drawAsync].
*/
fun setContentRoot(renderNode: RenderNode) {
mImpl.setContentRoot(renderNode)
@@ -217,8 +219,8 @@
* [CanvasBufferedRenderer].
* If 1 is specified, then the created [CanvasBufferedRenderer] is running in
* "single buffer mode". In this case consumption of the buffer content would need to be
- * coordinated with the [SyncFenceCompat] returned by the callback of [RenderRequest.draw].
- * @see CanvasBufferedRenderer.RenderRequest.draw
+ * coordinated with the [SyncFenceCompat] returned by the callback of [RenderRequest.drawAsync].
+ * @see CanvasBufferedRenderer.RenderRequest.drawAsync
*
* @param numBuffers The number of buffers within the swap chain to be consumed by the
* created [CanvasBufferedRenderer]. This must be greater than zero. The default
@@ -312,14 +314,42 @@
* @throws IllegalStateException if this method is invoked after the
* [CanvasBufferedRenderer] has been closed.
*/
- fun draw(executor: Executor, callback: Consumer<RenderResult>) {
- if (isClosed()) {
+ fun drawAsync(executor: Executor, callback: Consumer<RenderResult>) {
+ if (isClosed) {
throw IllegalStateException("Attempt to draw after renderer has been closed")
}
mImpl.draw(this, executor, callback)
}
/**
+ * Syncs the [RenderNode] tree to the render thread and requests content to be drawn
+ * synchronously.
+ * This [RenderRequest] instance should no longer be used after calling this method.
+ * The system internally may reuse instances of [RenderRequest] to reduce allocation churn.
+ *
+ * @param waitForFence Optional flag to determine if the [SyncFenceCompat] is also waited
+ * upon before returning as a convenience in order to enable callers to consume the
+ * [HardwareBuffer] returned in the [RenderResult] immediately after this method returns.
+ * Passing `false` here on Android T and below is a no-op as the graphics rendering pipeline
+ * internally blocks on the fence before returning.
+ */
+ suspend fun draw(waitForFence: Boolean = true): RenderResult {
+ check(!isClosed) { "Attempt to draw after renderer has been closed" }
+
+ return suspendCancellableCoroutine { continuation ->
+ drawAsync(Runnable::run) { result ->
+ if (waitForFence) {
+ result.fence?.apply {
+ awaitForever()
+ close()
+ }
+ }
+ continuation.resume(result)
+ }
+ }
+ }
+
+ /**
* Specifies a transform to be applied before content is rendered. This is useful
* for pre-rotating content for the current display orientation to increase performance
* of displaying the associated buffer. This transformation will also adjust the light
@@ -367,11 +397,19 @@
/**
* Determines whether or not previous buffer contents will be persisted across render
* requests. If false then contents are cleared before issuing drawing instructions,
- * otherwise contents will remain. If contents are known in advance to be completely opaque
- * and cover all pixels within the buffer, setting this flag to false will slightly improve
- * performance as the clear operation will be skipped. Additionally for single buffered
- * rendering scenarios, persisting contents can be beneficial in order to draw the deltas of
- * content across frames. The default setting is false
+ * otherwise contents will remain.
+ *
+ * If contents are known in advance to be completely opaque and cover all pixels within the
+ * buffer, setting this flag to true will slightly improve performance as the clear
+ * operation will be skipped.
+ *
+ * For low latency use cases (ex applications that support drawing with a stylus), setting
+ * this value to true alongside single buffered rendering by configuring
+ * [CanvasBufferedRenderer.Builder.setMaxBuffers] to 1 allows for reduced latency by allowing
+ * consumers to only render the deltas across frames as the previous content would be
+ * persisted.
+ *
+ * The default setting is false.
*/
fun preserveContents(preserve: Boolean): RenderRequest {
mPreserveContents = preserve
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
index 3464fd6..062ef16 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
@@ -470,7 +470,7 @@
}
}
.setColorSpace(targetColorSpace)
- .draw(mHandlerThread) { result ->
+ .drawAsync(mHandlerThread) { result ->
setParentSurfaceControlBuffer(
frontBufferSurfaceControl,
parentSurfaceControl,
@@ -543,7 +543,7 @@
}
}
.setColorSpace(targetColorSpace)
- .draw(mHandlerThread) { result ->
+ .drawAsync(mHandlerThread) { result ->
setParentSurfaceControlBuffer(
frontBufferSurfaceControl,
parentSurfaceControl,
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/PreservedBufferContentsVerifier.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/PreservedBufferContentsVerifier.kt
index 2fe214f..3f15b61 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/PreservedBufferContentsVerifier.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/PreservedBufferContentsVerifier.kt
@@ -85,7 +85,7 @@
val firstRenderLatch = CountDownLatch(1)
multiBufferedRenderer.obtainRenderRequest()
.preserveContents(true)
- .draw(executor) { _ ->
+ .drawAsync(executor) { _ ->
firstRenderLatch.countDown()
}
@@ -107,7 +107,7 @@
val secondRenderLatch = CountDownLatch(1)
multiBufferedRenderer.obtainRenderRequest()
.preserveContents(true)
- .draw(executor) { result ->
+ .drawAsync(executor) { result ->
result.fence?.awaitForever()
bitmap = Bitmap.wrapHardwareBuffer(
result.hardwareBuffer,
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
index 3e4476e..699ed6d 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
@@ -103,7 +103,7 @@
}
preserveContents(true)
setColorSpace([email protected])
- draw(executor) { result ->
+ drawAsync(executor) { result ->
requestComplete.invoke(result.hardwareBuffer, result.fence)
}
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 3eaee64..9ffe39b 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -136,7 +136,7 @@
SWIPEREFRESHLAYOUT = "1.2.0-alpha01"
TESTEXT = "1.0.0-alpha02"
TESTSCREENSHOT = "1.0.0-alpha01"
-TEST_UIAUTOMATOR = "2.3.0-alpha05"
+TEST_UIAUTOMATOR = "2.3.0-beta01"
TEXT = "1.0.0-alpha01"
TRACING = "1.3.0-alpha02"
TRACING_PERFETTO = "1.0.0"
diff --git a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index a92d2198..cf9e20d 100644
--- a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -553,15 +553,9 @@
? Looper.myLooper() : Looper.getMainLooper());
setCallback(new Callback() {}, handler);
mImpl.setMediaButtonReceiver(mbrIntent);
- } else if (android.os.Build.VERSION.SDK_INT >= 19) {
+ } else {
mImpl = new MediaSessionImplApi19(context, tag, mbrComponent, mbrIntent,
session2Token, sessionInfo);
- } else if (android.os.Build.VERSION.SDK_INT >= 18) {
- mImpl = new MediaSessionImplApi18(context, tag, mbrComponent, mbrIntent,
- session2Token, sessionInfo);
- } else {
- mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent, session2Token,
- sessionInfo);
}
mController = new MediaControllerCompat(context, this);
@@ -3758,7 +3752,6 @@
}
}
- @RequiresApi(18)
static class MediaSessionImplApi18 extends MediaSessionImplBase {
private static boolean sIsMbrPendingIntentSupported = true;
@@ -3844,7 +3837,6 @@
}
}
- @RequiresApi(19)
static class MediaSessionImplApi19 extends MediaSessionImplApi18 {
MediaSessionImplApi19(Context context, String tag, ComponentName mbrComponent,
PendingIntent mbrIntent, VersionedParcelable session2Token, Bundle sessionInfo) {
diff --git a/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java b/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
index f6e962ee..5fc397b 100644
--- a/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
+++ b/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
@@ -51,25 +51,6 @@
private AudioManager() {}
}
- /** Helper class for {@link android.os.HandlerThread}. */
- public static final class HandlerThread {
-
- /** Helper methods for {@link android.os.HandlerThread} APIs added in API level 18. */
- @RequiresApi(18)
- public static final class Api18 {
-
- /** Helper method to call {@link android.os.HandlerThread#quitSafely()}. */
- @DoNotInline
- public static boolean quitSafely(@NonNull android.os.HandlerThread handlerThread) {
- return handlerThread.quitSafely();
- }
-
- private Api18() {}
- }
-
- private HandlerThread() {}
- }
-
/** Helper class for {@link android.app.PendingIntent}. */
public static final class PendingIntent {
diff --git a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
index 20d1c73..6636bb4 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
@@ -43,7 +43,6 @@
import android.content.Intent;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.os.Build;
import android.os.Build.VERSION;
import android.os.HandlerThread;
import android.os.Looper;
@@ -125,11 +124,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
index d39d457..2b482e0 100644
--- a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
+++ b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.session;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -84,11 +83,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
index dc21534..9a4987c 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
@@ -36,7 +36,6 @@
import android.app.PendingIntent;
import android.content.Context;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -56,7 +55,6 @@
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
-import androidx.media2.common.ClassVerificationHelper;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
@@ -205,11 +203,7 @@
}
mHandler.removeCallbacksAndMessages(null);
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(mHandlerThread);
- } else {
- mHandlerThread.quit();
- }
+ mHandlerThread.quitSafely();
mClosed = true;
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 52ae967..e4ea230 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -322,11 +322,7 @@
});
mHandler.removeCallbacksAndMessages(null);
if (mHandlerThread.isAlive()) {
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(mHandlerThread);
- } else {
- mHandlerThread.quit();
- }
+ mHandlerThread.quitSafely();
}
}
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
index b6c7f26..a0997be 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
@@ -23,7 +23,6 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -38,7 +37,6 @@
import androidx.annotation.RestrictTo;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager;
-import androidx.media2.common.ClassVerificationHelper;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
@@ -345,11 +343,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void quitHandlerThread(HandlerThread thread) {
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(thread);
- } else {
- thread.quit();
- }
+ thread.quitSafely();
}
@SuppressWarnings("deprecation")
diff --git a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
index 639fb5b..ea17c74 100644
--- a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.client.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.support.v4.media.session.MediaSessionCompat;
@@ -88,11 +87,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
index 78b5841..cb8b580 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
@@ -54,7 +54,6 @@
import android.app.Service;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.util.Log;
@@ -124,11 +123,7 @@
sAssertLibraryParams = false;
sExpectedParams = null;
}
- if (Build.VERSION.SDK_INT >= 18) {
- mHandler.getLooper().quitSafely();
- } else {
- mHandler.getLooper().quit();
- }
+ mHandler.getLooper().quitSafely();
mHandler = null;
TestServiceRegistry.getInstance().cleanUp();
}
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
index 6db0c22..40529f8 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.service.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -79,11 +78,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/previous/client/build.gradle b/media2/media2-session/version-compat-tests/previous/client/build.gradle
index 5ca0a2c..d16354e 100644
--- a/media2/media2-session/version-compat-tests/previous/client/build.gradle
+++ b/media2/media2-session/version-compat-tests/previous/client/build.gradle
@@ -20,7 +20,7 @@
}
dependencies {
- androidTestImplementation("androidx.media2:media2-session:1.2.0-rc01")
+ androidTestImplementation("androidx.media2:media2-session:1.2.0")
androidTestImplementation(project(":media2:media2-session:version-compat-tests:common"))
androidTestImplementation(libs.testExtJunit)
diff --git a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
index 639fb5b..ea17c74 100644
--- a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.client.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.support.v4.media.session.MediaSessionCompat;
@@ -88,11 +87,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/previous/service/build.gradle b/media2/media2-session/version-compat-tests/previous/service/build.gradle
index 0f6afc1..001262f 100644
--- a/media2/media2-session/version-compat-tests/previous/service/build.gradle
+++ b/media2/media2-session/version-compat-tests/previous/service/build.gradle
@@ -20,7 +20,7 @@
}
dependencies {
- androidTestImplementation("androidx.media2:media2-session:1.2.0-rc01")
+ androidTestImplementation("androidx.media2:media2-session:1.2.0")
androidTestImplementation(project(":media2:media2-session:version-compat-tests:common"))
androidTestImplementation(libs.testExtJunit)
diff --git a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
index 78b5841..cb8b580 100644
--- a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
+++ b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
@@ -54,7 +54,6 @@
import android.app.Service;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.util.Log;
@@ -124,11 +123,7 @@
sAssertLibraryParams = false;
sExpectedParams = null;
}
- if (Build.VERSION.SDK_INT >= 18) {
- mHandler.getLooper().quitSafely();
- } else {
- mHandler.getLooper().quit();
- }
+ mHandler.getLooper().quitSafely();
mHandler = null;
TestServiceRegistry.getInstance().cleanUp();
}
diff --git a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
index 6db0c22..40529f8 100644
--- a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.service.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -79,11 +78,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/mediarouter/mediarouter/src/main/res/values-ca/strings.xml b/mediarouter/mediarouter/src/main/res/values-ca/strings.xml
index 6744047..a02305f 100644
--- a/mediarouter/mediarouter/src/main/res/values-ca/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-ca/strings.xml
@@ -33,7 +33,7 @@
<string name="mr_controller_stop" msgid="5497722768305745508">"Atura"</string>
<string name="mr_controller_expand_group" msgid="4521419834052044261">"Desplega"</string>
<string name="mr_controller_collapse_group" msgid="2585048604188129749">"Replega"</string>
- <string name="mr_controller_album_art" msgid="3330502667672708728">"Imatge de l\'àlbum"</string>
+ <string name="mr_controller_album_art" msgid="3330502667672708728">"Coberta de l\'àlbum"</string>
<string name="mr_controller_volume_slider" msgid="2955862765169128170">"Control lliscant de volum"</string>
<string name="mr_controller_no_media_selected" msgid="5495452265246139458">"No hi ha contingut multimèdia seleccionat"</string>
<string name="mr_controller_no_info_available" msgid="855271725131981086">"No hi ha informació disponible"</string>
diff --git a/mediarouter/mediarouter/src/main/res/values-fa/strings.xml b/mediarouter/mediarouter/src/main/res/values-fa/strings.xml
index b27a5dc..d13e45f 100644
--- a/mediarouter/mediarouter/src/main/res/values-fa/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-fa/strings.xml
@@ -33,7 +33,7 @@
<string name="mr_controller_stop" msgid="5497722768305745508">"متوقف کردن"</string>
<string name="mr_controller_expand_group" msgid="4521419834052044261">"بزرگ کردن"</string>
<string name="mr_controller_collapse_group" msgid="2585048604188129749">"کوچک کردن"</string>
- <string name="mr_controller_album_art" msgid="3330502667672708728">"عکس روی جلد آلبوم"</string>
+ <string name="mr_controller_album_art" msgid="3330502667672708728">"جلد آلبوم"</string>
<string name="mr_controller_volume_slider" msgid="2955862765169128170">"لغزنده میزان صدا"</string>
<string name="mr_controller_no_media_selected" msgid="5495452265246139458">"رسانهای انتخاب نشده است"</string>
<string name="mr_controller_no_info_available" msgid="855271725131981086">"اطلاعاتی در دسترس نیست"</string>
diff --git a/mediarouter/mediarouter/src/main/res/values-fr/strings.xml b/mediarouter/mediarouter/src/main/res/values-fr/strings.xml
index 754f775..dc37209 100644
--- a/mediarouter/mediarouter/src/main/res/values-fr/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-fr/strings.xml
@@ -33,7 +33,7 @@
<string name="mr_controller_stop" msgid="5497722768305745508">"Arrêt"</string>
<string name="mr_controller_expand_group" msgid="4521419834052044261">"Développer"</string>
<string name="mr_controller_collapse_group" msgid="2585048604188129749">"Réduire"</string>
- <string name="mr_controller_album_art" msgid="3330502667672708728">"Image de l\'album"</string>
+ <string name="mr_controller_album_art" msgid="3330502667672708728">"Pochette de l\'album"</string>
<string name="mr_controller_volume_slider" msgid="2955862765169128170">"Curseur de volume"</string>
<string name="mr_controller_no_media_selected" msgid="5495452265246139458">"Aucun contenu multimédia sélectionné"</string>
<string name="mr_controller_no_info_available" msgid="855271725131981086">"Aucune information disponible"</string>
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index d55a029..9cac784 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
-distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
index 2e8451a..73911a2 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
@@ -40,12 +40,6 @@
property public final long version;
}
- @Deprecated @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
- ctor @Deprecated public SandboxedSdkProviderAdapter();
- method @Deprecated public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
- }
-
public abstract class SandboxedSdkProviderCompat {
ctor public SandboxedSdkProviderCompat();
method public final void attachContext(android.content.Context context);
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
index 2e8451a..73911a2 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
@@ -40,12 +40,6 @@
property public final long version;
}
- @Deprecated @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
- ctor @Deprecated public SandboxedSdkProviderAdapter();
- method @Deprecated public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
- method @Deprecated @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
- }
-
public abstract class SandboxedSdkProviderCompat {
ctor public SandboxedSdkProviderCompat();
method public final void attachContext(android.content.Context context);
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt
deleted file mode 100644
index 3d062e4..0000000
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt
+++ /dev/null
@@ -1 +0,0 @@
-androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderAdapterTest$TestOnLoadReturnResultSdkProvider
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt
deleted file mode 100644
index ba919c3..0000000
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt
+++ /dev/null
@@ -1,261 +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.privacysandbox.sdkruntime.core
-
-import android.app.sdksandbox.LoadSdkException
-import android.content.Context
-import android.os.Binder
-import android.os.Build.VERSION_CODES.TIRAMISU
-import android.os.Bundle
-import android.os.ext.SdkExtensions.AD_SERVICES
-import android.view.View
-import androidx.annotation.RequiresExtension
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.reflect.KClass
-import org.junit.Assert.assertThrows
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-// TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
-@RequiresExtension(extension = AD_SERVICES, version = 4)
-@SdkSuppress(minSdkVersion = TIRAMISU)
-@Suppress("DEPRECATION")
-class SandboxedSdkProviderAdapterTest {
-
- private lateinit var context: Context
-
- @Before
- fun setUp() {
- assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
- context = ApplicationProvider.getApplicationContext()
- }
-
- @Test
- fun testAdapterGetCompatClassNameFromAsset() {
- val expectedClassName = context.assets
- .open("SandboxedSdkProviderCompatClassName.txt")
- .use { inputStream ->
- inputStream.bufferedReader().readLine()
- }
-
- val adapter = SandboxedSdkProviderAdapter()
- adapter.attachContext(context)
-
- adapter.onLoadSdk(Bundle())
-
- val delegate = adapter.extractDelegate<SandboxedSdkProviderCompat>()
- assertThat(delegate.javaClass.name)
- .isEqualTo(expectedClassName)
- }
-
- @Test
- fun onLoadSdk_shouldInstantiateDelegateAndAttachContext() {
- val adapter = createAdapterFor(TestOnLoadReturnResultSdkProvider::class)
-
- adapter.onLoadSdk(Bundle())
-
- val delegate = adapter.extractDelegate<TestOnLoadReturnResultSdkProvider>()
- assertThat(delegate.context)
- .isSameInstanceAs(context)
- }
-
- @Test
- fun onLoadSdk_shouldDelegateToCompatClassAndReturnResult() {
- val adapter = createAdapterFor(TestOnLoadReturnResultSdkProvider::class)
- val params = Bundle()
-
- val result = adapter.onLoadSdk(params)
-
- val delegate = adapter.extractDelegate<TestOnLoadReturnResultSdkProvider>()
- assertThat(delegate.mLastOnLoadSdkBundle)
- .isSameInstanceAs(params)
- assertThat(result.getInterface())
- .isEqualTo(delegate.mResult.getInterface())
- }
-
- @Test
- fun loadSdk_shouldRethrowExceptionFromCompatClass() {
- val adapter = createAdapterFor(TestOnLoadThrowSdkProvider::class)
-
- val ex = assertThrows(LoadSdkException::class.java) {
- adapter.onLoadSdk(Bundle())
- }
-
- val delegate = adapter.extractDelegate<TestOnLoadThrowSdkProvider>()
- assertThat(ex.cause)
- .isSameInstanceAs(delegate.mError.cause)
- assertThat(ex.extraInformation)
- .isSameInstanceAs(delegate.mError.extraInformation)
- }
-
- @Test
- fun loadSdk_shouldThrowIfCompatClassNotExists() {
- val adapter = createAdapterFor("NOTEXISTS")
-
- assertThrows(ClassNotFoundException::class.java) {
- adapter.onLoadSdk(Bundle())
- }
- }
-
- @Test
- fun beforeUnloadSdk_shouldDelegateToCompatProvider() {
- val adapter = createAdapterFor(TestOnBeforeUnloadDelegateSdkProvider::class)
-
- adapter.beforeUnloadSdk()
-
- val delegate = adapter.extractDelegate<TestOnBeforeUnloadDelegateSdkProvider>()
- assertThat(delegate.mBeforeUnloadSdkCalled)
- .isTrue()
- }
-
- @Test
- fun getView_shouldDelegateToCompatProviderAndReturnResult() {
- val adapter = createAdapterFor(TestGetViewSdkProvider::class)
- val windowContext = mock(Context::class.java)
- val params = Bundle()
- val width = 1
- val height = 2
-
- val result = adapter.getView(windowContext, params, width, height)
-
- val delegate = adapter.extractDelegate<TestGetViewSdkProvider>()
- assertThat(result)
- .isSameInstanceAs(delegate.mView)
- assertThat(delegate.mLastWindowContext)
- .isSameInstanceAs(windowContext)
- assertThat(delegate.mLastParams)
- .isSameInstanceAs(params)
- assertThat(delegate.mLastWidth)
- .isSameInstanceAs(width)
- assertThat(delegate.mLastHeigh)
- .isSameInstanceAs(height)
- }
-
- private fun createAdapterFor(
- clazz: KClass<out SandboxedSdkProviderCompat>
- ): SandboxedSdkProviderAdapter = createAdapterFor(clazz.java.name)
-
- private fun createAdapterFor(delegateClassName: String): SandboxedSdkProviderAdapter {
- val adapter = SandboxedSdkProviderAdapter(
- object : SandboxedSdkProviderAdapter.CompatClassNameProvider {
- override fun getCompatProviderClassName(context: Context): String {
- return delegateClassName
- }
- })
- adapter.attachContext(context)
- return adapter
- }
-
- private inline fun <reified T : SandboxedSdkProviderCompat>
- SandboxedSdkProviderAdapter.extractDelegate(): T = delegate as T
-
- class TestOnLoadReturnResultSdkProvider : SandboxedSdkProviderCompat() {
- var mResult = SandboxedSdkCompat(Binder())
- var mLastOnLoadSdkBundle: Bundle? = null
-
- override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
- mLastOnLoadSdkBundle = params
- return mResult
- }
-
- override fun getView(
- windowContext: Context,
- params: Bundle,
- width: Int,
- height: Int
- ): View {
- throw RuntimeException("Not implemented")
- }
- }
-
- class TestOnLoadThrowSdkProvider : SandboxedSdkProviderCompat() {
- var mError = LoadSdkCompatException(RuntimeException(), Bundle())
-
- @Throws(LoadSdkCompatException::class)
- override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
- throw mError
- }
-
- override fun getView(
- windowContext: Context,
- params: Bundle,
- width: Int,
- height: Int
- ): View {
- throw RuntimeException("Stub!")
- }
- }
-
- class TestOnBeforeUnloadDelegateSdkProvider : SandboxedSdkProviderCompat() {
- var mBeforeUnloadSdkCalled = false
-
- override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
- throw RuntimeException("Not implemented")
- }
-
- override fun beforeUnloadSdk() {
- mBeforeUnloadSdkCalled = true
- }
-
- override fun getView(
- windowContext: Context,
- params: Bundle,
- width: Int,
- height: Int
- ): View {
- throw RuntimeException("Not implemented")
- }
- }
-
- class TestGetViewSdkProvider : SandboxedSdkProviderCompat() {
- val mView: View = mock(View::class.java)
-
- var mLastWindowContext: Context? = null
- var mLastParams: Bundle? = null
- var mLastWidth = 0
- var mLastHeigh = 0
-
- override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
- throw RuntimeException("Not implemented")
- }
-
- override fun getView(
- windowContext: Context,
- params: Bundle,
- width: Int,
- height: Int
- ): View {
- mLastWindowContext = windowContext
- mLastParams = params
- mLastWidth = width
- mLastHeigh = height
-
- return mView
- }
- }
-
- private fun isSandboxApiAvailable() =
- AdServicesInfo.isAtLeastV4()
-}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerAppOwnedInterfacesTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerAppOwnedInterfacesTest.kt
index c7cc743..6e1b54f 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerAppOwnedInterfacesTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerAppOwnedInterfacesTest.kt
@@ -20,7 +20,6 @@
import android.app.sdksandbox.sdkprovider.SdkSandboxController
import android.content.Context
import android.os.Binder
-import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.RequiresExtension
import androidx.core.os.BuildCompat
@@ -39,31 +38,11 @@
import org.mockito.Mockito.`when`
// TODO(b/249982507) Rewrite test to use real SDK in sandbox instead of mocking controller
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+@SdkSuppress(minSdkVersion = 34)
class SdkSandboxControllerAppOwnedInterfacesTest {
@Test
- fun getAppOwnedSdkSandboxInterfaces_whenControllerNotAvailable_returnsEmptyList() {
- assumeFalse(
- "Requires SandboxController not available",
- isSandboxControllerAvailable()
- )
-
- val context = ApplicationProvider.getApplicationContext<Context>()
- val controllerCompat = SdkSandboxControllerCompat.from(context)
-
- val appOwnedInterfaces = controllerCompat.getAppOwnedSdkSandboxInterfaces()
- assertThat(appOwnedInterfaces).isEmpty()
- }
-
- @Test
- @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
fun getAppOwnedSdkSandboxInterfaces_whenApiNotAvailable_returnsEmptyList() {
- assumeTrue(
- "Requires SandboxController available",
- isSandboxControllerAvailable()
- )
assumeFalse(
"Requires AppOwnedInterfaces API not available",
isAppOwnedInterfacesApiAvailable()
@@ -83,7 +62,7 @@
@Test
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 8)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ @SdkSuppress(minSdkVersion = 34)
fun getAppOwnedSdkSandboxInterfaces_whenApiAvailable_delegateToPlatform() {
assumeTrue(
"Requires AppOwnedInterfaces API available",
@@ -112,9 +91,6 @@
assertThat(resultObj.getInterface()).isEqualTo(expectedObj.getInterface())
}
- private fun isSandboxControllerAvailable() =
- AdServicesInfo.isAtLeastV5()
-
private fun isAppOwnedInterfacesApiAvailable() =
BuildCompat.AD_SERVICES_EXTENSION_INT >= 8 || AdServicesInfo.isDeveloperPreview()
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
index be2fb94..c8a0ba0f 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatSandboxedTest.kt
@@ -23,13 +23,9 @@
import android.app.sdksandbox.sdkprovider.SdkSandboxController
import android.content.Context
import android.os.Binder
-import android.os.Build
import android.os.Bundle
-import android.os.ext.SdkExtensions
import android.window.OnBackInvokedDispatcher
-import androidx.annotation.RequiresExtension
import androidx.lifecycle.Lifecycle
-import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
@@ -39,52 +35,34 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Assert
-import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.`when`
// TODO(b/249982507) Rewrite test to use real SDK in sandbox instead of mocking controller
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
class SdkSandboxControllerCompatSandboxedTest {
@Test
- fun controllerAPIs_whenApiNotAvailable_notDelegateToSandbox() {
- assumeFalse(
- "Requires SandboxController API not available",
- isSandboxControllerAvailable()
- )
+ @SdkSuppress(maxSdkVersion = 33)
+ fun from_whenApiBelow34_throwUnsupportedOperationException() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ Assert.assertThrows(UnsupportedOperationException::class.java) {
+ SdkSandboxControllerCompat.from(context)
+ }
+ }
+ @Test
+ @SdkSuppress(minSdkVersion = 34)
+ fun loadSdk_withoutLocalImpl_throwsLoadSdkCompatException() {
val context = spy(ApplicationProvider.getApplicationContext<Context>())
- val controllerCompat = SdkSandboxControllerCompat.from(context)
+ val sdkSandboxController = mock(SdkSandboxController::class.java)
+ doReturn(sdkSandboxController)
+ .`when`(context).getSystemService(SdkSandboxController::class.java)
- controllerCompat.getSandboxedSdks()
- val handlerCompat = object : SdkSandboxActivityHandlerCompat {
- override fun onActivityCreated(activityHolder: ActivityHolder) {}
- }
- Assert.assertThrows(UnsupportedOperationException::class.java) {
- controllerCompat.registerSdkSandboxActivityHandler(handlerCompat)
- }
- Assert.assertThrows(UnsupportedOperationException::class.java) {
- controllerCompat.unregisterSdkSandboxActivityHandler(handlerCompat)
- }
- Assert.assertThrows(LoadSdkCompatException::class.java) {
- runBlocking {
- controllerCompat.loadSdk("SDK", Bundle())
- }
- }
- verifyZeroInteractions(context)
- }
-
- @Test
- fun loadSdk_throwsLoadSdkCompatException() {
- val context = ApplicationProvider.getApplicationContext<Context>()
val controllerCompat = SdkSandboxControllerCompat.from(context)
Assert.assertThrows(LoadSdkCompatException::class.java) {
@@ -95,30 +73,8 @@
}
@Test
- fun getSandboxedSdks_whenApiNotAvailable_returnsEmptyList() {
- assumeFalse(
- "Requires SandboxController API not available",
- isSandboxControllerAvailable()
- )
-
- val context = ApplicationProvider.getApplicationContext<Context>()
- val controllerCompat = SdkSandboxControllerCompat.from(context)
-
- val sandboxedSdks = controllerCompat.getSandboxedSdks()
-
- assertThat(sandboxedSdks).isEmpty()
- }
-
- @Test
- // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
- @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
- fun getSandboxedSdks_whenApiAvailable_returnsListFromPlatformApi() {
- assumeTrue(
- "Requires SandboxController API available",
- isSandboxControllerAvailable()
- )
-
+ @SdkSuppress(minSdkVersion = 34)
+ fun getSandboxedSdks_withoutLocalImpl_returnsListFromPlatformApi() {
val context = spy(ApplicationProvider.getApplicationContext<Context>())
val sdkSandboxController = mock(SdkSandboxController::class.java)
doReturn(sdkSandboxController)
@@ -137,10 +93,8 @@
}
@Test
- // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
- @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
- fun registerSdkSandboxHandlerCompat_whenApiAvailable_registerItToPlatform() {
+ @SdkSuppress(minSdkVersion = 34)
+ fun registerSdkSandboxHandlerCompat_withoutLocalImpl_registerItToPlatform() {
val context = spy(ApplicationProvider.getApplicationContext<Context>())
val sdkSandboxController = mock(SdkSandboxController::class.java)
doReturn(sdkSandboxController)
@@ -211,10 +165,8 @@
}
@Test
- // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
- @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
- fun unregisterSdkSandboxHandlerCompat_whenApiAvailable_unregisterItToPlatform() {
+ @SdkSuppress(minSdkVersion = 34)
+ fun unregisterSdkSandboxHandlerCompat_withoutLocalImpl_unregisterItToPlatform() {
val context = spy(ApplicationProvider.getApplicationContext<Context>())
val sdkSandboxController = mock(SdkSandboxController::class.java)
doReturn(sdkSandboxController)
@@ -242,9 +194,6 @@
assertThat(unregisteredHandlerCaptor.value).isEqualTo(registeredHandlerCaptor.value)
}
- private fun isSandboxControllerAvailable() =
- AdServicesInfo.isAtLeastV5()
-
// To capture non null arguments.
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt
index e62fc1d..24771d7 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt
@@ -23,7 +23,9 @@
import android.os.Bundle
import android.os.ext.SdkExtensions.AD_SERVICES
import android.view.View
+import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
/**
* Implementation of platform [SandboxedSdkProvider] that delegate to [SandboxedSdkProviderCompat]
@@ -31,6 +33,7 @@
*
*/
@SuppressLint("Override") // b/273473397
+@RequiresApi(34)
@RequiresExtension(extension = AD_SERVICES, version = 4)
// TODO(b/301437557) Remove after documentation migration to sdkruntime-provider
@Deprecated(
@@ -38,8 +41,10 @@
replaceWith = ReplaceWith(
expression = "SandboxedSdkProviderAdapter",
imports = arrayOf("androidx.privacysandbox.sdkruntime.provider.SandboxedSdkProviderAdapter")
- )
+ ),
+ level = DeprecationLevel.HIDDEN
)
+@RestrictTo(RestrictTo.Scope.LIBRARY) // removing from public API surface
class SandboxedSdkProviderAdapter internal constructor(
private val classNameProvider: CompatClassNameProvider
) : SandboxedSdkProvider() {
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
index c56f630..3d1a408 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
@@ -33,7 +33,6 @@
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
import androidx.privacysandbox.sdkruntime.core.controller.impl.LocalImpl
import androidx.privacysandbox.sdkruntime.core.controller.impl.NoOpImpl
-import androidx.privacysandbox.sdkruntime.core.controller.impl.PlatformImpl
import androidx.privacysandbox.sdkruntime.core.controller.impl.PlatformUDCImpl
import org.jetbrains.annotations.TestOnly
@@ -181,13 +180,10 @@
private object PlatformImplFactory {
fun create(context: Context): SandboxControllerImpl {
- if (AdServicesInfo.isAtLeastV5()) {
- if (Build.VERSION.SDK_INT >= 34 || AdServicesInfo.isDeveloperPreview()) {
- return PlatformUDCImpl.from(context)
- }
- return PlatformImpl.from(context)
+ if (Build.VERSION.SDK_INT >= 34 || AdServicesInfo.isDeveloperPreview()) {
+ return PlatformUDCImpl.from(context)
}
- return NoOpImpl()
+ throw UnsupportedOperationException("SDK should be loaded locally on API below 34")
}
}
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt
deleted file mode 100644
index 7c1de6b..0000000
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformImpl.kt
+++ /dev/null
@@ -1,78 +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.privacysandbox.sdkruntime.core.controller.impl
-
-import android.app.sdksandbox.sdkprovider.SdkSandboxController
-import android.content.Context
-import android.os.Bundle
-import android.os.IBinder
-import android.os.ext.SdkExtensions.AD_SERVICES
-import androidx.annotation.RequiresApi
-import androidx.annotation.RequiresExtension
-import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
-import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_NOT_FOUND
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
-import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
-
-/**
- * Implementation that delegates to platform [SdkSandboxController].
- */
-@RequiresApi(33)
-@RequiresExtension(extension = AD_SERVICES, version = 5)
-internal open class PlatformImpl(
- private val controller: SdkSandboxController
-) : SdkSandboxControllerCompat.SandboxControllerImpl {
-
- private val appOwnedSdkProvider = AppOwnedSdkProvider.create(controller)
-
- override suspend fun loadSdk(sdkName: String, params: Bundle): SandboxedSdkCompat {
- throw LoadSdkCompatException(
- LOAD_SDK_NOT_FOUND,
- "Loading SDK not supported on this device"
- )
- }
-
- override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
- return controller
- .sandboxedSdks
- .map { platformSdk -> SandboxedSdkCompat(platformSdk) }
- }
-
- override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> =
- appOwnedSdkProvider.getAppOwnedSdkSandboxInterfaces()
-
- override fun registerSdkSandboxActivityHandler(
- handlerCompat: SdkSandboxActivityHandlerCompat
- ): IBinder {
- throw UnsupportedOperationException("This API only available for devices run on Android U+")
- }
-
- override fun unregisterSdkSandboxActivityHandler(
- handlerCompat: SdkSandboxActivityHandlerCompat
- ) {
- throw UnsupportedOperationException("This API only available for devices run on Android U+")
- }
-
- companion object {
- fun from(context: Context): PlatformImpl {
- val sdkSandboxController = context.getSystemService(SdkSandboxController::class.java)
- return PlatformImpl(sdkSandboxController)
- }
- }
-}
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt
index a143ea0..601d474 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/PlatformUDCImpl.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.sdkruntime.core.controller.impl
+import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.app.sdksandbox.sdkprovider.SdkSandboxActivityHandler
@@ -23,27 +24,47 @@
import android.content.Context
import android.os.Bundle
import android.os.IBinder
-import android.os.ext.SdkExtensions
import androidx.activity.OnBackPressedDispatcher
import androidx.annotation.RequiresApi
-import androidx.annotation.RequiresExtension
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
+import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.activity.ActivityHolder
import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
+import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
/**
* Implementation that delegates to platform [SdkSandboxController] for Android U.
*/
-@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
@RequiresApi(34)
+@SuppressLint("NewApi", "ClassVerificationFailure") // until updating checks to requires api 34
internal class PlatformUDCImpl(
private val controller: SdkSandboxController
-) : PlatformImpl(controller) {
+) : SdkSandboxControllerCompat.SandboxControllerImpl {
+
+ private val appOwnedSdkProvider = AppOwnedSdkProvider.create(controller)
private val compatToPlatformMap =
hashMapOf<SdkSandboxActivityHandlerCompat, SdkSandboxActivityHandler>()
+ override suspend fun loadSdk(sdkName: String, params: Bundle): SandboxedSdkCompat {
+ throw LoadSdkCompatException(
+ LoadSdkCompatException.LOAD_SDK_NOT_FOUND,
+ "Loading SDK not supported on this device"
+ )
+ }
+
+ override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
+ return controller
+ .sandboxedSdks
+ .map { platformSdk -> SandboxedSdkCompat(platformSdk) }
+ }
+
+ override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> =
+ appOwnedSdkProvider.getAppOwnedSdkSandboxInterfaces()
+
override fun registerSdkSandboxActivityHandler(
handlerCompat: SdkSandboxActivityHandlerCompat
): IBinder {
@@ -142,7 +163,7 @@
}
companion object {
- fun from(context: Context): PlatformImpl {
+ fun from(context: Context): PlatformUDCImpl {
val sdkSandboxController = context.getSystemService(SdkSandboxController::class.java)
return PlatformUDCImpl(sdkSandboxController)
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-provider/api/current.txt
index bad8190..00612b5 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.privacysandbox.sdkruntime.provider {
- @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
+ @RequiresApi(34) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
ctor public SandboxedSdkProviderAdapter();
method public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
method @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-provider/api/restricted_current.txt
index bad8190..00612b5 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.privacysandbox.sdkruntime.provider {
- @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
+ @RequiresApi(34) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
ctor public SandboxedSdkProviderAdapter();
method public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
method @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/src/androidTest/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapterTest.kt b/privacysandbox/sdkruntime/sdkruntime-provider/src/androidTest/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapterTest.kt
index debd913a..5bd11b1 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/src/androidTest/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapterTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/src/androidTest/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapterTest.kt
@@ -19,12 +19,8 @@
import android.app.sdksandbox.LoadSdkException
import android.content.Context
import android.os.Binder
-import android.os.Build.VERSION_CODES.TIRAMISU
import android.os.Bundle
-import android.os.ext.SdkExtensions.AD_SERVICES
import android.view.View
-import androidx.annotation.RequiresExtension
-import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
@@ -35,7 +31,6 @@
import com.google.common.truth.Truth.assertThat
import kotlin.reflect.KClass
import org.junit.Assert.assertThrows
-import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,16 +38,13 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-// TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
-@RequiresExtension(extension = AD_SERVICES, version = 4)
-@SdkSuppress(minSdkVersion = TIRAMISU)
+@SdkSuppress(minSdkVersion = 34)
class SandboxedSdkProviderAdapterTest {
private lateinit var context: Context
@Before
fun setUp() {
- assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
context = ApplicationProvider.getApplicationContext()
}
@@ -259,7 +251,4 @@
return mView
}
}
-
- private fun isSandboxApiAvailable() =
- AdServicesInfo.isAtLeastV4()
}
diff --git a/privacysandbox/sdkruntime/sdkruntime-provider/src/main/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapter.kt b/privacysandbox/sdkruntime/sdkruntime-provider/src/main/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapter.kt
index 05a6eb0..5c70191 100644
--- a/privacysandbox/sdkruntime/sdkruntime-provider/src/main/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapter.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-provider/src/main/java/androidx/privacysandbox/sdkruntime/provider/SandboxedSdkProviderAdapter.kt
@@ -22,9 +22,8 @@
import android.app.sdksandbox.SandboxedSdkProvider
import android.content.Context
import android.os.Bundle
-import android.os.ext.SdkExtensions.AD_SERVICES
import android.view.View
-import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresApi
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
@@ -33,8 +32,8 @@
* Gets compat class name from asset "SandboxedSdkProviderCompatClassName.txt"
*
*/
-@RequiresExtension(extension = AD_SERVICES, version = 4)
-@SuppressLint("ClassVerificationFailure")
+@RequiresApi(34)
+@SuppressLint("NewApi", "ClassVerificationFailure") // until updating checks to requires api 34
class SandboxedSdkProviderAdapter internal constructor(
private val classNameProvider: CompatClassNameProvider
) : SandboxedSdkProvider() {
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
index 2e7bc22..a7a4b8d 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -49,6 +49,8 @@
config = params.config
).also { processor = it }
}
+
+ fun isProcessorInitialized() = this::processor.isInitialized
}
val args = TestCompilationArguments(
sources = params.sources,
@@ -62,6 +64,23 @@
workingDir = workingDir,
arguments = args
)
+ if (!processorProvider.isProcessorInitialized()) {
+ // KSP did not completely run, report diagnostic messages those with an exception.
+ val exceptionMsg = buildString {
+ append("KSP did not completely run!")
+ if (result.diagnostics.isNotEmpty()) {
+ appendLine()
+ appendLine("--- Diagnostic messages:")
+ result.diagnostics.values.flatten().forEach {
+ appendLine("${it.kind}: ${it.msg}")
+ }
+ append("--- End of Diagnostic messages")
+ } else {
+ append(" No diagnostic messages...")
+ }
+ }
+ error(exceptionMsg)
+ }
return KotlinCompilationResult(
testRunner = this,
processor = processorProvider.processor,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/AnnotationMirrorExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/AnnotationMirrorExt.kt
index 3b1368b..244390e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/AnnotationMirrorExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/AnnotationMirrorExt.kt
@@ -25,33 +25,39 @@
/** Returns true if the given [AnnotationMirror] represents a repeatable annotation. */
internal fun AnnotationMirror.isRepeatable(): Boolean {
- val valueType =
- ElementFilter.methodsIn(MoreTypes.asTypeElement(annotationType).enclosedElements)
- .singleOrNull { it.simpleName.toString() == "value" }
- ?.returnType
+ try {
+ val valueType =
+ ElementFilter.methodsIn(MoreTypes.asTypeElement(annotationType).enclosedElements)
+ .singleOrNull { it.simpleName.toString() == "value" }
+ ?.returnType
- // The contract of a repeatable annotation requires that the container annotation have a
- // single "default" method that returns an array typed with the repeatable annotation type.
- if (valueType == null || valueType.kind != TypeKind.ARRAY) {
+ // The contract of a repeatable annotation requires that the container annotation have a
+ // single "default" method that returns an array typed with the repeatable annotation type.
+ if (valueType == null || valueType.kind != TypeKind.ARRAY) {
+ return false
+ }
+ val componentType = MoreTypes.asArray(valueType).componentType
+ if (componentType.kind != TypeKind.DECLARED) {
+ return false
+ }
+ val componentElement = MoreTypes.asDeclared(componentType).asElement()
+
+ // Ideally we would read the value of the Repeatable annotation to get the container class
+ // type and check that it matches "this" type. However, there seems to be a KSP bug where
+ // the value of Repeatable is not present so the best we can do is check that all the nested
+ // members are annotated with repeatable.
+ // https://github.com/google/ksp/issues/358
+ return MoreElements.isAnnotationPresent(componentElement, Repeatable::class.java) ||
+ // The java and kotlin versions of Repeatable are not interchangeable.
+ // https://github.com/google/ksp/issues/459 asks whether the built in type
+ // mapper should convert them, but it may not be possible because there are
+ // differences to how they work (eg different parameters).
+ MoreElements.isAnnotationPresent(
+ componentElement, kotlin.annotation.Repeatable::class.java
+ )
+ } catch (t: Throwable) {
+ // TODO(b/314160063): Turbine can throw when getting enclosed elements. Remove this once
+ // we have a better way to work around Turbine exception.
return false
}
- val componentType = MoreTypes.asArray(valueType).componentType
- if (componentType.kind != TypeKind.DECLARED) {
- return false
- }
- val componentElement = MoreTypes.asDeclared(componentType).asElement()
-
- // Ideally we would read the value of the Repeatable annotation to get the container class
- // type and check that it matches "this" type. However, there seems to be a KSP bug where
- // the value of Repeatable is not present so the best we can do is check that all the nested
- // members are annotated with repeatable.
- // https://github.com/google/ksp/issues/358
- return MoreElements.isAnnotationPresent(componentElement, Repeatable::class.java) ||
- // The java and kotlin versions of Repeatable are not interchangeable.
- // https://github.com/google/ksp/issues/459 asks whether the built in type
- // mapper should convert them, but it may not be possible because there are
- // differences to how they work (eg different parameters).
- MoreElements.isAnnotationPresent(
- componentElement, kotlin.annotation.Repeatable::class.java
- )
}
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
index aaf77af..8ca3a1e 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
@@ -17,10 +17,10 @@
package com.android.build.gradle.internal.fixtures
import java.util.function.BiFunction
-import java.util.function.Predicate
import org.gradle.api.Transformer
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
+import org.gradle.api.specs.Spec
class FakeGradleProperty<T>(private var value: T? = null) : Property<T> {
@@ -47,8 +47,7 @@
value ?: valueProvider?.get() ?: convention ?: throw IllegalStateException("Value not set")
override fun getOrNull() = value ?: valueProvider?.get() ?: convention
-
- override fun filter(predicate: Predicate<in T>): Provider<T> {
+ override fun filter(spec: Spec<in T>): Provider<T> {
throw NotImplementedError()
}
diff --git a/test/uiautomator/uiautomator/api/2.3.0-beta01.txt b/test/uiautomator/uiautomator/api/2.3.0-beta01.txt
new file mode 100644
index 0000000..bfaecd4
--- /dev/null
+++ b/test/uiautomator/uiautomator/api/2.3.0-beta01.txt
@@ -0,0 +1,467 @@
+// Signature format: 4.0
+package androidx.test.uiautomator {
+
+ public class By {
+ method public static androidx.test.uiautomator.BySelector checkable(boolean);
+ method public static androidx.test.uiautomator.BySelector checked(boolean);
+ method public static androidx.test.uiautomator.BySelector clazz(Class);
+ method public static androidx.test.uiautomator.BySelector clazz(String);
+ method public static androidx.test.uiautomator.BySelector clazz(String, String);
+ method public static androidx.test.uiautomator.BySelector clazz(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector clickable(boolean);
+ method public static androidx.test.uiautomator.BySelector copy(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector depth(int);
+ method public static androidx.test.uiautomator.BySelector desc(String);
+ method public static androidx.test.uiautomator.BySelector desc(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector descContains(String);
+ method public static androidx.test.uiautomator.BySelector descEndsWith(String);
+ method public static androidx.test.uiautomator.BySelector descStartsWith(String);
+ method @RequiresApi(30) public static androidx.test.uiautomator.BySelector displayId(int);
+ method public static androidx.test.uiautomator.BySelector enabled(boolean);
+ method public static androidx.test.uiautomator.BySelector focusable(boolean);
+ method public static androidx.test.uiautomator.BySelector focused(boolean);
+ method public static androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector, @IntRange(from=1) int);
+ method public static androidx.test.uiautomator.BySelector hasChild(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector, int);
+ method public static androidx.test.uiautomator.BySelector hasParent(androidx.test.uiautomator.BySelector);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hint(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hint(java.util.regex.Pattern);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintContains(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintEndsWith(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintStartsWith(String);
+ method public static androidx.test.uiautomator.BySelector longClickable(boolean);
+ method public static androidx.test.uiautomator.BySelector pkg(String);
+ method public static androidx.test.uiautomator.BySelector pkg(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector res(String);
+ method public static androidx.test.uiautomator.BySelector res(String, String);
+ method public static androidx.test.uiautomator.BySelector res(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector scrollable(boolean);
+ method public static androidx.test.uiautomator.BySelector selected(boolean);
+ method public static androidx.test.uiautomator.BySelector text(String);
+ method public static androidx.test.uiautomator.BySelector text(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector textContains(String);
+ method public static androidx.test.uiautomator.BySelector textEndsWith(String);
+ method public static androidx.test.uiautomator.BySelector textStartsWith(String);
+ }
+
+ public class BySelector {
+ method public androidx.test.uiautomator.BySelector checkable(boolean);
+ method public androidx.test.uiautomator.BySelector checked(boolean);
+ method public androidx.test.uiautomator.BySelector clazz(Class);
+ method public androidx.test.uiautomator.BySelector clazz(String);
+ method public androidx.test.uiautomator.BySelector clazz(String, String);
+ method public androidx.test.uiautomator.BySelector clazz(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector clickable(boolean);
+ method public androidx.test.uiautomator.BySelector depth(int);
+ method public androidx.test.uiautomator.BySelector depth(int, int);
+ method public androidx.test.uiautomator.BySelector desc(String);
+ method public androidx.test.uiautomator.BySelector desc(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector descContains(String);
+ method public androidx.test.uiautomator.BySelector descEndsWith(String);
+ method public androidx.test.uiautomator.BySelector descStartsWith(String);
+ method @RequiresApi(30) public androidx.test.uiautomator.BySelector displayId(int);
+ method public androidx.test.uiautomator.BySelector enabled(boolean);
+ method public androidx.test.uiautomator.BySelector focusable(boolean);
+ method public androidx.test.uiautomator.BySelector focused(boolean);
+ method public androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector, @IntRange(from=1) int);
+ method public androidx.test.uiautomator.BySelector hasChild(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector, int);
+ method public androidx.test.uiautomator.BySelector hasParent(androidx.test.uiautomator.BySelector);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hint(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hint(java.util.regex.Pattern);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintContains(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintEndsWith(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintStartsWith(String);
+ method public androidx.test.uiautomator.BySelector longClickable(boolean);
+ method public androidx.test.uiautomator.BySelector maxDepth(int);
+ method public androidx.test.uiautomator.BySelector minDepth(int);
+ method public androidx.test.uiautomator.BySelector pkg(String);
+ method public androidx.test.uiautomator.BySelector pkg(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector res(String);
+ method public androidx.test.uiautomator.BySelector res(String, String);
+ method public androidx.test.uiautomator.BySelector res(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector scrollable(boolean);
+ method public androidx.test.uiautomator.BySelector selected(boolean);
+ method public androidx.test.uiautomator.BySelector text(String);
+ method public androidx.test.uiautomator.BySelector text(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector textContains(String);
+ method public androidx.test.uiautomator.BySelector textEndsWith(String);
+ method public androidx.test.uiautomator.BySelector textStartsWith(String);
+ }
+
+ public interface Condition<T, U> {
+ method public U! apply(T!);
+ }
+
+ public final class Configurator {
+ method public long getActionAcknowledgmentTimeout();
+ method public static androidx.test.uiautomator.Configurator getInstance();
+ method public long getKeyInjectionDelay();
+ method public long getScrollAcknowledgmentTimeout();
+ method public int getToolType();
+ method public int getUiAutomationFlags();
+ method public long getWaitForIdleTimeout();
+ method public long getWaitForSelectorTimeout();
+ method public androidx.test.uiautomator.Configurator setActionAcknowledgmentTimeout(long);
+ method public androidx.test.uiautomator.Configurator setKeyInjectionDelay(long);
+ method public androidx.test.uiautomator.Configurator setScrollAcknowledgmentTimeout(long);
+ method public androidx.test.uiautomator.Configurator setToolType(int);
+ method public androidx.test.uiautomator.Configurator setUiAutomationFlags(int);
+ method public androidx.test.uiautomator.Configurator setWaitForIdleTimeout(long);
+ method public androidx.test.uiautomator.Configurator setWaitForSelectorTimeout(long);
+ }
+
+ public enum Direction {
+ method public static androidx.test.uiautomator.Direction reverse(androidx.test.uiautomator.Direction);
+ enum_constant public static final androidx.test.uiautomator.Direction DOWN;
+ enum_constant public static final androidx.test.uiautomator.Direction LEFT;
+ enum_constant public static final androidx.test.uiautomator.Direction RIGHT;
+ enum_constant public static final androidx.test.uiautomator.Direction UP;
+ }
+
+ public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
+ ctor public EventCondition();
+ method public abstract U! getResult();
+ }
+
+ public interface IAutomationSupport {
+ method public void sendStatus(int, android.os.Bundle);
+ }
+
+ public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
+ ctor public SearchCondition();
+ }
+
+ public class StaleObjectException extends java.lang.RuntimeException {
+ ctor public StaleObjectException();
+ }
+
+ @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+ ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+ method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+ method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+ }
+
+ @Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
+ ctor @Deprecated public UiAutomatorTestCase();
+ method @Deprecated public androidx.test.uiautomator.IAutomationSupport! getAutomationSupport();
+ method @Deprecated public android.os.Bundle! getParams();
+ method @Deprecated public androidx.test.uiautomator.UiDevice! getUiDevice();
+ method @Deprecated public void sleep(long);
+ }
+
+ public class UiCollection extends androidx.test.uiautomator.UiObject {
+ ctor public UiCollection(androidx.test.uiautomator.UiSelector);
+ method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByInstance(androidx.test.uiautomator.UiSelector, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getChildCount(androidx.test.uiautomator.UiSelector);
+ }
+
+ public class UiDevice {
+ method public void clearLastTraversedText();
+ method public boolean click(int, int);
+ method public boolean drag(int, int, int, int, int);
+ method public void dumpWindowHierarchy(java.io.File) throws java.io.IOException;
+ method public void dumpWindowHierarchy(java.io.OutputStream) throws java.io.IOException;
+ method @Deprecated public void dumpWindowHierarchy(String);
+ method @Discouraged(message="Can be useful for simple commands, but lacks support for proper error handling, input data, or complex commands (quotes, pipes) that can be obtained from UiAutomation#executeShellCommandRwe or similar utilities.") @RequiresApi(21) public String executeShellCommand(String) throws java.io.IOException;
+ method public androidx.test.uiautomator.UiObject2! findObject(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.UiObject findObject(androidx.test.uiautomator.UiSelector);
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> findObjects(androidx.test.uiautomator.BySelector);
+ method public void freezeRotation() throws android.os.RemoteException;
+ method @RequiresApi(30) public void freezeRotation(int);
+ method @Deprecated public String! getCurrentActivityName();
+ method public String! getCurrentPackageName();
+ method @Px public int getDisplayHeight();
+ method @Px public int getDisplayHeight(int);
+ method public int getDisplayRotation();
+ method public int getDisplayRotation(int);
+ method public android.graphics.Point getDisplaySizeDp();
+ method @Px public int getDisplayWidth();
+ method @Px public int getDisplayWidth(int);
+ method @Deprecated public static androidx.test.uiautomator.UiDevice getInstance();
+ method public static androidx.test.uiautomator.UiDevice getInstance(android.app.Instrumentation);
+ method public String! getLastTraversedText();
+ method public String! getLauncherPackageName();
+ method public String getProductName();
+ method public boolean hasAnyWatcherTriggered();
+ method public boolean hasObject(androidx.test.uiautomator.BySelector);
+ method public boolean hasWatcherTriggered(String?);
+ method public boolean isNaturalOrientation();
+ method public boolean isScreenOn() throws android.os.RemoteException;
+ method public boolean openNotification();
+ method public boolean openQuickSettings();
+ method public <U> U! performActionAndWait(Runnable, androidx.test.uiautomator.EventCondition<U!>, long);
+ method public boolean pressBack();
+ method public boolean pressDPadCenter();
+ method public boolean pressDPadDown();
+ method public boolean pressDPadLeft();
+ method public boolean pressDPadRight();
+ method public boolean pressDPadUp();
+ method public boolean pressDelete();
+ method public boolean pressEnter();
+ method public boolean pressHome();
+ method public boolean pressKeyCode(int);
+ method public boolean pressKeyCode(int, int);
+ method public boolean pressKeyCodes(int[]);
+ method public boolean pressKeyCodes(int[], int);
+ method public boolean pressMenu();
+ method public boolean pressRecentApps() throws android.os.RemoteException;
+ method public boolean pressSearch();
+ method public void registerWatcher(String?, androidx.test.uiautomator.UiWatcher?);
+ method public void removeWatcher(String?);
+ method public void resetWatcherTriggers();
+ method public void runWatchers();
+ method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+ method public void setCompressedLayoutHierarchy(boolean);
+ method public void setOrientationLandscape() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationLandscape(int);
+ method public void setOrientationLeft() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationLeft(int);
+ method public void setOrientationNatural() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationNatural(int);
+ method public void setOrientationPortrait() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationPortrait(int);
+ method public void setOrientationRight() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationRight(int);
+ method public void sleep() throws android.os.RemoteException;
+ method public boolean swipe(android.graphics.Point![], int);
+ method public boolean swipe(int, int, int, int, int);
+ method public boolean takeScreenshot(java.io.File);
+ method public boolean takeScreenshot(java.io.File, float, int);
+ method public void unfreezeRotation() throws android.os.RemoteException;
+ method @RequiresApi(30) public void unfreezeRotation(int);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public void waitForIdle();
+ method public void waitForIdle(long);
+ method public boolean waitForWindowUpdate(String?, long);
+ method public void wakeUp() throws android.os.RemoteException;
+ }
+
+ public class UiObject {
+ ctor @Deprecated public UiObject(androidx.test.uiautomator.UiSelector!);
+ method public void clearTextField() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean click() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickAndWaitForNewWindow() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickAndWaitForNewWindow(long) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickBottomRight() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickTopLeft() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean dragTo(androidx.test.uiautomator.UiObject, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean dragTo(int, int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean exists();
+ method protected android.view.accessibility.AccessibilityNodeInfo? findAccessibilityNodeInfo(long);
+ method public android.graphics.Rect getBounds() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChild(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getChildCount() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getClassName() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getContentDescription() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getFromParent(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getPackageName() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public final androidx.test.uiautomator.UiSelector getSelector();
+ method public String getText() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public android.graphics.Rect getVisibleBounds() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isCheckable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isChecked() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isClickable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isEnabled() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isFocusable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isFocused() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isLongClickable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isScrollable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isSelected() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClick() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClickBottomRight() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClickTopLeft() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean performMultiPointerGesture(android.view.MotionEvent.PointerCoords![]!...);
+ method public boolean performTwoPointerGesture(android.graphics.Point, android.graphics.Point, android.graphics.Point, android.graphics.Point, int);
+ method public boolean pinchIn(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean pinchOut(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean setText(String?) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeDown(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeLeft(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeRight(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeUp(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean waitForExists(long);
+ method public boolean waitUntilGone(long);
+ field protected static final int FINGER_TOUCH_HALF_WIDTH = 20; // 0x14
+ field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5
+ field @Deprecated protected static final long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L
+ field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L
+ field @Deprecated protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L
+ field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL
+ }
+
+ public class UiObject2 {
+ method public void clear();
+ method public void click();
+ method public void click(android.graphics.Point);
+ method public void click(android.graphics.Point, long);
+ method public void click(long);
+ method public <U> U! clickAndWait(android.graphics.Point, androidx.test.uiautomator.EventCondition<U!>, long);
+ method public <U> U! clickAndWait(androidx.test.uiautomator.EventCondition<U!>, long);
+ method public void drag(android.graphics.Point);
+ method public void drag(android.graphics.Point, int);
+ method public androidx.test.uiautomator.UiObject2! findObject(androidx.test.uiautomator.BySelector);
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> findObjects(androidx.test.uiautomator.BySelector);
+ method public boolean fling(androidx.test.uiautomator.Direction);
+ method public boolean fling(androidx.test.uiautomator.Direction, int);
+ method public String! getApplicationPackage();
+ method public int getChildCount();
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> getChildren();
+ method public String! getClassName();
+ method public String! getContentDescription();
+ method public int getDisplayId();
+ method @RequiresApi(24) public int getDrawingOrder();
+ method @RequiresApi(26) public String? getHint();
+ method public androidx.test.uiautomator.UiObject2! getParent();
+ method public String! getResourceName();
+ method public String! getText();
+ method public android.graphics.Rect getVisibleBounds();
+ method public android.graphics.Point getVisibleCenter();
+ method public boolean hasObject(androidx.test.uiautomator.BySelector);
+ method public boolean isCheckable();
+ method public boolean isChecked();
+ method public boolean isClickable();
+ method public boolean isEnabled();
+ method public boolean isFocusable();
+ method public boolean isFocused();
+ method public boolean isLongClickable();
+ method public boolean isScrollable();
+ method public boolean isSelected();
+ method public void longClick();
+ method public void pinchClose(float);
+ method public void pinchClose(float, int);
+ method public void pinchOpen(float);
+ method public void pinchOpen(float, int);
+ method public void recycle();
+ method public boolean scroll(androidx.test.uiautomator.Direction, float);
+ method public boolean scroll(androidx.test.uiautomator.Direction, float, int);
+ method public <U> U! scrollUntil(androidx.test.uiautomator.Direction, androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>);
+ method public <U> U! scrollUntil(androidx.test.uiautomator.Direction, androidx.test.uiautomator.EventCondition<U!>);
+ method public void setGestureMargin(int);
+ method public void setGestureMarginPercent(@FloatRange(from=0.0f, to=0.5f) float);
+ method public void setGestureMarginPercent(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float);
+ method public void setGestureMargins(int, int, int, int);
+ method public void setText(String?);
+ method public void swipe(androidx.test.uiautomator.Direction, float);
+ method public void swipe(androidx.test.uiautomator.Direction, float, int);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
+ }
+
+ public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
+ ctor public UiObject2Condition();
+ }
+
+ public class UiObjectNotFoundException extends java.lang.Exception {
+ ctor public UiObjectNotFoundException(String);
+ ctor public UiObjectNotFoundException(String, Throwable?);
+ ctor public UiObjectNotFoundException(Throwable?);
+ }
+
+ public class UiScrollable extends androidx.test.uiautomator.UiCollection {
+ ctor public UiScrollable(androidx.test.uiautomator.UiSelector);
+ method protected boolean exists(androidx.test.uiautomator.UiSelector);
+ method public boolean flingBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getMaxSearchSwipes();
+ method public double getSwipeDeadZonePercentage();
+ method public boolean scrollBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollBackward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollDescriptionIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollForward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollIntoView(androidx.test.uiautomator.UiObject) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollTextIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToBeginning(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToEnd(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiScrollable setAsHorizontalList();
+ method public androidx.test.uiautomator.UiScrollable setAsVerticalList();
+ method public androidx.test.uiautomator.UiScrollable setMaxSearchSwipes(int);
+ method public androidx.test.uiautomator.UiScrollable setSwipeDeadZonePercentage(double);
+ }
+
+ public class UiSelector {
+ ctor public UiSelector();
+ method public androidx.test.uiautomator.UiSelector checkable(boolean);
+ method public androidx.test.uiautomator.UiSelector checked(boolean);
+ method public androidx.test.uiautomator.UiSelector childSelector(androidx.test.uiautomator.UiSelector);
+ method public <T> androidx.test.uiautomator.UiSelector className(Class<T!>);
+ method public androidx.test.uiautomator.UiSelector className(String);
+ method public androidx.test.uiautomator.UiSelector classNameMatches(String);
+ method public androidx.test.uiautomator.UiSelector clickable(boolean);
+ method protected androidx.test.uiautomator.UiSelector cloneSelector();
+ method public androidx.test.uiautomator.UiSelector description(String);
+ method public androidx.test.uiautomator.UiSelector descriptionContains(String);
+ method public androidx.test.uiautomator.UiSelector descriptionMatches(String);
+ method public androidx.test.uiautomator.UiSelector descriptionStartsWith(String);
+ method public androidx.test.uiautomator.UiSelector enabled(boolean);
+ method public androidx.test.uiautomator.UiSelector focusable(boolean);
+ method public androidx.test.uiautomator.UiSelector focused(boolean);
+ method public androidx.test.uiautomator.UiSelector fromParent(androidx.test.uiautomator.UiSelector);
+ method public androidx.test.uiautomator.UiSelector index(int);
+ method public androidx.test.uiautomator.UiSelector instance(int);
+ method public androidx.test.uiautomator.UiSelector longClickable(boolean);
+ method public androidx.test.uiautomator.UiSelector packageName(String);
+ method public androidx.test.uiautomator.UiSelector packageNameMatches(String);
+ method public androidx.test.uiautomator.UiSelector resourceId(String);
+ method public androidx.test.uiautomator.UiSelector resourceIdMatches(String);
+ method public androidx.test.uiautomator.UiSelector scrollable(boolean);
+ method public androidx.test.uiautomator.UiSelector selected(boolean);
+ method public androidx.test.uiautomator.UiSelector text(String);
+ method public androidx.test.uiautomator.UiSelector textContains(String);
+ method public androidx.test.uiautomator.UiSelector textMatches(String);
+ method public androidx.test.uiautomator.UiSelector textStartsWith(String);
+ }
+
+ public interface UiWatcher {
+ method public boolean checkForCondition();
+ }
+
+ public class Until {
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> checkable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> checked(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> clickable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descContains(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descEndsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descMatches(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descMatches(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descStartsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> enabled(boolean);
+ method public static androidx.test.uiautomator.SearchCondition<androidx.test.uiautomator.UiObject2!> findObject(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.SearchCondition<java.util.List<androidx.test.uiautomator.UiObject2!>!> findObjects(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> focusable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> focused(boolean);
+ method public static androidx.test.uiautomator.SearchCondition<java.lang.Boolean!> gone(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.SearchCondition<java.lang.Boolean!> hasObject(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> longClickable(boolean);
+ method public static androidx.test.uiautomator.EventCondition<java.lang.Boolean!> newWindow();
+ method public static androidx.test.uiautomator.EventCondition<java.lang.Boolean!> scrollFinished(androidx.test.uiautomator.Direction);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> scrollable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> selected(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textContains(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textEndsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textMatches(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textMatches(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textNotEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textStartsWith(String);
+ }
+
+}
+
diff --git a/test/uiautomator/uiautomator/api/res-2.3.0-beta01.txt b/test/uiautomator/uiautomator/api/res-2.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/uiautomator/uiautomator/api/res-2.3.0-beta01.txt
diff --git a/test/uiautomator/uiautomator/api/restricted_2.3.0-beta01.txt b/test/uiautomator/uiautomator/api/restricted_2.3.0-beta01.txt
new file mode 100644
index 0000000..bfaecd4
--- /dev/null
+++ b/test/uiautomator/uiautomator/api/restricted_2.3.0-beta01.txt
@@ -0,0 +1,467 @@
+// Signature format: 4.0
+package androidx.test.uiautomator {
+
+ public class By {
+ method public static androidx.test.uiautomator.BySelector checkable(boolean);
+ method public static androidx.test.uiautomator.BySelector checked(boolean);
+ method public static androidx.test.uiautomator.BySelector clazz(Class);
+ method public static androidx.test.uiautomator.BySelector clazz(String);
+ method public static androidx.test.uiautomator.BySelector clazz(String, String);
+ method public static androidx.test.uiautomator.BySelector clazz(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector clickable(boolean);
+ method public static androidx.test.uiautomator.BySelector copy(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector depth(int);
+ method public static androidx.test.uiautomator.BySelector desc(String);
+ method public static androidx.test.uiautomator.BySelector desc(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector descContains(String);
+ method public static androidx.test.uiautomator.BySelector descEndsWith(String);
+ method public static androidx.test.uiautomator.BySelector descStartsWith(String);
+ method @RequiresApi(30) public static androidx.test.uiautomator.BySelector displayId(int);
+ method public static androidx.test.uiautomator.BySelector enabled(boolean);
+ method public static androidx.test.uiautomator.BySelector focusable(boolean);
+ method public static androidx.test.uiautomator.BySelector focused(boolean);
+ method public static androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector, @IntRange(from=1) int);
+ method public static androidx.test.uiautomator.BySelector hasChild(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector, int);
+ method public static androidx.test.uiautomator.BySelector hasParent(androidx.test.uiautomator.BySelector);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hint(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hint(java.util.regex.Pattern);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintContains(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintEndsWith(String);
+ method @RequiresApi(26) public static androidx.test.uiautomator.BySelector hintStartsWith(String);
+ method public static androidx.test.uiautomator.BySelector longClickable(boolean);
+ method public static androidx.test.uiautomator.BySelector pkg(String);
+ method public static androidx.test.uiautomator.BySelector pkg(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector res(String);
+ method public static androidx.test.uiautomator.BySelector res(String, String);
+ method public static androidx.test.uiautomator.BySelector res(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector scrollable(boolean);
+ method public static androidx.test.uiautomator.BySelector selected(boolean);
+ method public static androidx.test.uiautomator.BySelector text(String);
+ method public static androidx.test.uiautomator.BySelector text(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.BySelector textContains(String);
+ method public static androidx.test.uiautomator.BySelector textEndsWith(String);
+ method public static androidx.test.uiautomator.BySelector textStartsWith(String);
+ }
+
+ public class BySelector {
+ method public androidx.test.uiautomator.BySelector checkable(boolean);
+ method public androidx.test.uiautomator.BySelector checked(boolean);
+ method public androidx.test.uiautomator.BySelector clazz(Class);
+ method public androidx.test.uiautomator.BySelector clazz(String);
+ method public androidx.test.uiautomator.BySelector clazz(String, String);
+ method public androidx.test.uiautomator.BySelector clazz(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector clickable(boolean);
+ method public androidx.test.uiautomator.BySelector depth(int);
+ method public androidx.test.uiautomator.BySelector depth(int, int);
+ method public androidx.test.uiautomator.BySelector desc(String);
+ method public androidx.test.uiautomator.BySelector desc(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector descContains(String);
+ method public androidx.test.uiautomator.BySelector descEndsWith(String);
+ method public androidx.test.uiautomator.BySelector descStartsWith(String);
+ method @RequiresApi(30) public androidx.test.uiautomator.BySelector displayId(int);
+ method public androidx.test.uiautomator.BySelector enabled(boolean);
+ method public androidx.test.uiautomator.BySelector focusable(boolean);
+ method public androidx.test.uiautomator.BySelector focused(boolean);
+ method public androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasAncestor(androidx.test.uiautomator.BySelector, @IntRange(from=1) int);
+ method public androidx.test.uiautomator.BySelector hasChild(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.BySelector hasDescendant(androidx.test.uiautomator.BySelector, int);
+ method public androidx.test.uiautomator.BySelector hasParent(androidx.test.uiautomator.BySelector);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hint(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hint(java.util.regex.Pattern);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintContains(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintEndsWith(String);
+ method @RequiresApi(26) public androidx.test.uiautomator.BySelector hintStartsWith(String);
+ method public androidx.test.uiautomator.BySelector longClickable(boolean);
+ method public androidx.test.uiautomator.BySelector maxDepth(int);
+ method public androidx.test.uiautomator.BySelector minDepth(int);
+ method public androidx.test.uiautomator.BySelector pkg(String);
+ method public androidx.test.uiautomator.BySelector pkg(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector res(String);
+ method public androidx.test.uiautomator.BySelector res(String, String);
+ method public androidx.test.uiautomator.BySelector res(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector scrollable(boolean);
+ method public androidx.test.uiautomator.BySelector selected(boolean);
+ method public androidx.test.uiautomator.BySelector text(String);
+ method public androidx.test.uiautomator.BySelector text(java.util.regex.Pattern);
+ method public androidx.test.uiautomator.BySelector textContains(String);
+ method public androidx.test.uiautomator.BySelector textEndsWith(String);
+ method public androidx.test.uiautomator.BySelector textStartsWith(String);
+ }
+
+ public interface Condition<T, U> {
+ method public U! apply(T!);
+ }
+
+ public final class Configurator {
+ method public long getActionAcknowledgmentTimeout();
+ method public static androidx.test.uiautomator.Configurator getInstance();
+ method public long getKeyInjectionDelay();
+ method public long getScrollAcknowledgmentTimeout();
+ method public int getToolType();
+ method public int getUiAutomationFlags();
+ method public long getWaitForIdleTimeout();
+ method public long getWaitForSelectorTimeout();
+ method public androidx.test.uiautomator.Configurator setActionAcknowledgmentTimeout(long);
+ method public androidx.test.uiautomator.Configurator setKeyInjectionDelay(long);
+ method public androidx.test.uiautomator.Configurator setScrollAcknowledgmentTimeout(long);
+ method public androidx.test.uiautomator.Configurator setToolType(int);
+ method public androidx.test.uiautomator.Configurator setUiAutomationFlags(int);
+ method public androidx.test.uiautomator.Configurator setWaitForIdleTimeout(long);
+ method public androidx.test.uiautomator.Configurator setWaitForSelectorTimeout(long);
+ }
+
+ public enum Direction {
+ method public static androidx.test.uiautomator.Direction reverse(androidx.test.uiautomator.Direction);
+ enum_constant public static final androidx.test.uiautomator.Direction DOWN;
+ enum_constant public static final androidx.test.uiautomator.Direction LEFT;
+ enum_constant public static final androidx.test.uiautomator.Direction RIGHT;
+ enum_constant public static final androidx.test.uiautomator.Direction UP;
+ }
+
+ public abstract class EventCondition<U> implements android.app.UiAutomation.AccessibilityEventFilter {
+ ctor public EventCondition();
+ method public abstract U! getResult();
+ }
+
+ public interface IAutomationSupport {
+ method public void sendStatus(int, android.os.Bundle);
+ }
+
+ public abstract class SearchCondition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.Searchable,U> {
+ ctor public SearchCondition();
+ }
+
+ public class StaleObjectException extends java.lang.RuntimeException {
+ ctor public StaleObjectException();
+ }
+
+ @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+ ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+ method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+ method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+ }
+
+ @Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
+ ctor @Deprecated public UiAutomatorTestCase();
+ method @Deprecated public androidx.test.uiautomator.IAutomationSupport! getAutomationSupport();
+ method @Deprecated public android.os.Bundle! getParams();
+ method @Deprecated public androidx.test.uiautomator.UiDevice! getUiDevice();
+ method @Deprecated public void sleep(long);
+ }
+
+ public class UiCollection extends androidx.test.uiautomator.UiObject {
+ ctor public UiCollection(androidx.test.uiautomator.UiSelector);
+ method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByInstance(androidx.test.uiautomator.UiSelector, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getChildCount(androidx.test.uiautomator.UiSelector);
+ }
+
+ public class UiDevice {
+ method public void clearLastTraversedText();
+ method public boolean click(int, int);
+ method public boolean drag(int, int, int, int, int);
+ method public void dumpWindowHierarchy(java.io.File) throws java.io.IOException;
+ method public void dumpWindowHierarchy(java.io.OutputStream) throws java.io.IOException;
+ method @Deprecated public void dumpWindowHierarchy(String);
+ method @Discouraged(message="Can be useful for simple commands, but lacks support for proper error handling, input data, or complex commands (quotes, pipes) that can be obtained from UiAutomation#executeShellCommandRwe or similar utilities.") @RequiresApi(21) public String executeShellCommand(String) throws java.io.IOException;
+ method public androidx.test.uiautomator.UiObject2! findObject(androidx.test.uiautomator.BySelector);
+ method public androidx.test.uiautomator.UiObject findObject(androidx.test.uiautomator.UiSelector);
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> findObjects(androidx.test.uiautomator.BySelector);
+ method public void freezeRotation() throws android.os.RemoteException;
+ method @RequiresApi(30) public void freezeRotation(int);
+ method @Deprecated public String! getCurrentActivityName();
+ method public String! getCurrentPackageName();
+ method @Px public int getDisplayHeight();
+ method @Px public int getDisplayHeight(int);
+ method public int getDisplayRotation();
+ method public int getDisplayRotation(int);
+ method public android.graphics.Point getDisplaySizeDp();
+ method @Px public int getDisplayWidth();
+ method @Px public int getDisplayWidth(int);
+ method @Deprecated public static androidx.test.uiautomator.UiDevice getInstance();
+ method public static androidx.test.uiautomator.UiDevice getInstance(android.app.Instrumentation);
+ method public String! getLastTraversedText();
+ method public String! getLauncherPackageName();
+ method public String getProductName();
+ method public boolean hasAnyWatcherTriggered();
+ method public boolean hasObject(androidx.test.uiautomator.BySelector);
+ method public boolean hasWatcherTriggered(String?);
+ method public boolean isNaturalOrientation();
+ method public boolean isScreenOn() throws android.os.RemoteException;
+ method public boolean openNotification();
+ method public boolean openQuickSettings();
+ method public <U> U! performActionAndWait(Runnable, androidx.test.uiautomator.EventCondition<U!>, long);
+ method public boolean pressBack();
+ method public boolean pressDPadCenter();
+ method public boolean pressDPadDown();
+ method public boolean pressDPadLeft();
+ method public boolean pressDPadRight();
+ method public boolean pressDPadUp();
+ method public boolean pressDelete();
+ method public boolean pressEnter();
+ method public boolean pressHome();
+ method public boolean pressKeyCode(int);
+ method public boolean pressKeyCode(int, int);
+ method public boolean pressKeyCodes(int[]);
+ method public boolean pressKeyCodes(int[], int);
+ method public boolean pressMenu();
+ method public boolean pressRecentApps() throws android.os.RemoteException;
+ method public boolean pressSearch();
+ method public void registerWatcher(String?, androidx.test.uiautomator.UiWatcher?);
+ method public void removeWatcher(String?);
+ method public void resetWatcherTriggers();
+ method public void runWatchers();
+ method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+ method public void setCompressedLayoutHierarchy(boolean);
+ method public void setOrientationLandscape() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationLandscape(int);
+ method public void setOrientationLeft() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationLeft(int);
+ method public void setOrientationNatural() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationNatural(int);
+ method public void setOrientationPortrait() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationPortrait(int);
+ method public void setOrientationRight() throws android.os.RemoteException;
+ method @RequiresApi(30) public void setOrientationRight(int);
+ method public void sleep() throws android.os.RemoteException;
+ method public boolean swipe(android.graphics.Point![], int);
+ method public boolean swipe(int, int, int, int, int);
+ method public boolean takeScreenshot(java.io.File);
+ method public boolean takeScreenshot(java.io.File, float, int);
+ method public void unfreezeRotation() throws android.os.RemoteException;
+ method @RequiresApi(30) public void unfreezeRotation(int);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiDevice,U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public void waitForIdle();
+ method public void waitForIdle(long);
+ method public boolean waitForWindowUpdate(String?, long);
+ method public void wakeUp() throws android.os.RemoteException;
+ }
+
+ public class UiObject {
+ ctor @Deprecated public UiObject(androidx.test.uiautomator.UiSelector!);
+ method public void clearTextField() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean click() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickAndWaitForNewWindow() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickAndWaitForNewWindow(long) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickBottomRight() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean clickTopLeft() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean dragTo(androidx.test.uiautomator.UiObject, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean dragTo(int, int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean exists();
+ method protected android.view.accessibility.AccessibilityNodeInfo? findAccessibilityNodeInfo(long);
+ method public android.graphics.Rect getBounds() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChild(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getChildCount() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getClassName() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getContentDescription() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getFromParent(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public String getPackageName() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public final androidx.test.uiautomator.UiSelector getSelector();
+ method public String getText() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public android.graphics.Rect getVisibleBounds() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isCheckable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isChecked() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isClickable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isEnabled() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isFocusable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isFocused() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isLongClickable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isScrollable() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean isSelected() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClick() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClickBottomRight() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean longClickTopLeft() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean performMultiPointerGesture(android.view.MotionEvent.PointerCoords![]!...);
+ method public boolean performTwoPointerGesture(android.graphics.Point, android.graphics.Point, android.graphics.Point, android.graphics.Point, int);
+ method public boolean pinchIn(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean pinchOut(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean setText(String?) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeDown(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeLeft(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeRight(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean swipeUp(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean waitForExists(long);
+ method public boolean waitUntilGone(long);
+ field protected static final int FINGER_TOUCH_HALF_WIDTH = 20; // 0x14
+ field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5
+ field @Deprecated protected static final long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L
+ field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L
+ field @Deprecated protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L
+ field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL
+ }
+
+ public class UiObject2 {
+ method public void clear();
+ method public void click();
+ method public void click(android.graphics.Point);
+ method public void click(android.graphics.Point, long);
+ method public void click(long);
+ method public <U> U! clickAndWait(android.graphics.Point, androidx.test.uiautomator.EventCondition<U!>, long);
+ method public <U> U! clickAndWait(androidx.test.uiautomator.EventCondition<U!>, long);
+ method public void drag(android.graphics.Point);
+ method public void drag(android.graphics.Point, int);
+ method public androidx.test.uiautomator.UiObject2! findObject(androidx.test.uiautomator.BySelector);
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> findObjects(androidx.test.uiautomator.BySelector);
+ method public boolean fling(androidx.test.uiautomator.Direction);
+ method public boolean fling(androidx.test.uiautomator.Direction, int);
+ method public String! getApplicationPackage();
+ method public int getChildCount();
+ method public java.util.List<androidx.test.uiautomator.UiObject2!> getChildren();
+ method public String! getClassName();
+ method public String! getContentDescription();
+ method public int getDisplayId();
+ method @RequiresApi(24) public int getDrawingOrder();
+ method @RequiresApi(26) public String? getHint();
+ method public androidx.test.uiautomator.UiObject2! getParent();
+ method public String! getResourceName();
+ method public String! getText();
+ method public android.graphics.Rect getVisibleBounds();
+ method public android.graphics.Point getVisibleCenter();
+ method public boolean hasObject(androidx.test.uiautomator.BySelector);
+ method public boolean isCheckable();
+ method public boolean isChecked();
+ method public boolean isClickable();
+ method public boolean isEnabled();
+ method public boolean isFocusable();
+ method public boolean isFocused();
+ method public boolean isLongClickable();
+ method public boolean isScrollable();
+ method public boolean isSelected();
+ method public void longClick();
+ method public void pinchClose(float);
+ method public void pinchClose(float, int);
+ method public void pinchOpen(float);
+ method public void pinchOpen(float, int);
+ method public void recycle();
+ method public boolean scroll(androidx.test.uiautomator.Direction, float);
+ method public boolean scroll(androidx.test.uiautomator.Direction, float, int);
+ method public <U> U! scrollUntil(androidx.test.uiautomator.Direction, androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>);
+ method public <U> U! scrollUntil(androidx.test.uiautomator.Direction, androidx.test.uiautomator.EventCondition<U!>);
+ method public void setGestureMargin(int);
+ method public void setGestureMarginPercent(@FloatRange(from=0.0f, to=0.5f) float);
+ method public void setGestureMarginPercent(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float);
+ method public void setGestureMargins(int, int, int, int);
+ method public void setText(String?);
+ method public void swipe(androidx.test.uiautomator.Direction, float);
+ method public void swipe(androidx.test.uiautomator.Direction, float, int);
+ method public <U> U! wait(androidx.test.uiautomator.Condition<? super androidx.test.uiautomator.UiObject2,U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.SearchCondition<U!>, long);
+ method public <U> U! wait(androidx.test.uiautomator.UiObject2Condition<U!>, long);
+ }
+
+ public abstract class UiObject2Condition<U> implements androidx.test.uiautomator.Condition<androidx.test.uiautomator.UiObject2,U> {
+ ctor public UiObject2Condition();
+ }
+
+ public class UiObjectNotFoundException extends java.lang.Exception {
+ ctor public UiObjectNotFoundException(String);
+ ctor public UiObjectNotFoundException(String, Throwable?);
+ ctor public UiObjectNotFoundException(Throwable?);
+ }
+
+ public class UiScrollable extends androidx.test.uiautomator.UiCollection {
+ ctor public UiScrollable(androidx.test.uiautomator.UiSelector);
+ method protected boolean exists(androidx.test.uiautomator.UiSelector);
+ method public boolean flingBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean flingToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public int getMaxSearchSwipes();
+ method public double getSwipeDeadZonePercentage();
+ method public boolean scrollBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollBackward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollDescriptionIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollForward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollIntoView(androidx.test.uiautomator.UiObject) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollTextIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToBeginning(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public boolean scrollToEnd(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
+ method public androidx.test.uiautomator.UiScrollable setAsHorizontalList();
+ method public androidx.test.uiautomator.UiScrollable setAsVerticalList();
+ method public androidx.test.uiautomator.UiScrollable setMaxSearchSwipes(int);
+ method public androidx.test.uiautomator.UiScrollable setSwipeDeadZonePercentage(double);
+ }
+
+ public class UiSelector {
+ ctor public UiSelector();
+ method public androidx.test.uiautomator.UiSelector checkable(boolean);
+ method public androidx.test.uiautomator.UiSelector checked(boolean);
+ method public androidx.test.uiautomator.UiSelector childSelector(androidx.test.uiautomator.UiSelector);
+ method public <T> androidx.test.uiautomator.UiSelector className(Class<T!>);
+ method public androidx.test.uiautomator.UiSelector className(String);
+ method public androidx.test.uiautomator.UiSelector classNameMatches(String);
+ method public androidx.test.uiautomator.UiSelector clickable(boolean);
+ method protected androidx.test.uiautomator.UiSelector cloneSelector();
+ method public androidx.test.uiautomator.UiSelector description(String);
+ method public androidx.test.uiautomator.UiSelector descriptionContains(String);
+ method public androidx.test.uiautomator.UiSelector descriptionMatches(String);
+ method public androidx.test.uiautomator.UiSelector descriptionStartsWith(String);
+ method public androidx.test.uiautomator.UiSelector enabled(boolean);
+ method public androidx.test.uiautomator.UiSelector focusable(boolean);
+ method public androidx.test.uiautomator.UiSelector focused(boolean);
+ method public androidx.test.uiautomator.UiSelector fromParent(androidx.test.uiautomator.UiSelector);
+ method public androidx.test.uiautomator.UiSelector index(int);
+ method public androidx.test.uiautomator.UiSelector instance(int);
+ method public androidx.test.uiautomator.UiSelector longClickable(boolean);
+ method public androidx.test.uiautomator.UiSelector packageName(String);
+ method public androidx.test.uiautomator.UiSelector packageNameMatches(String);
+ method public androidx.test.uiautomator.UiSelector resourceId(String);
+ method public androidx.test.uiautomator.UiSelector resourceIdMatches(String);
+ method public androidx.test.uiautomator.UiSelector scrollable(boolean);
+ method public androidx.test.uiautomator.UiSelector selected(boolean);
+ method public androidx.test.uiautomator.UiSelector text(String);
+ method public androidx.test.uiautomator.UiSelector textContains(String);
+ method public androidx.test.uiautomator.UiSelector textMatches(String);
+ method public androidx.test.uiautomator.UiSelector textStartsWith(String);
+ }
+
+ public interface UiWatcher {
+ method public boolean checkForCondition();
+ }
+
+ public class Until {
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> checkable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> checked(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> clickable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descContains(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descEndsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descMatches(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descMatches(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> descStartsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> enabled(boolean);
+ method public static androidx.test.uiautomator.SearchCondition<androidx.test.uiautomator.UiObject2!> findObject(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.SearchCondition<java.util.List<androidx.test.uiautomator.UiObject2!>!> findObjects(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> focusable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> focused(boolean);
+ method public static androidx.test.uiautomator.SearchCondition<java.lang.Boolean!> gone(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.SearchCondition<java.lang.Boolean!> hasObject(androidx.test.uiautomator.BySelector);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> longClickable(boolean);
+ method public static androidx.test.uiautomator.EventCondition<java.lang.Boolean!> newWindow();
+ method public static androidx.test.uiautomator.EventCondition<java.lang.Boolean!> scrollFinished(androidx.test.uiautomator.Direction);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> scrollable(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> selected(boolean);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textContains(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textEndsWith(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textMatches(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textMatches(java.util.regex.Pattern);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textNotEquals(String);
+ method public static androidx.test.uiautomator.UiObject2Condition<java.lang.Boolean!> textStartsWith(String);
+ }
+
+}
+
diff --git a/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt b/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt
deleted file mode 100644
index 52beb96..0000000
--- a/testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/VerifyWithPolling.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils
-
-import java.lang.SuppressWarnings
-import org.junit.Assert
-
-@SuppressWarnings("BanThreadSleep")
-fun verifyWithPolling(
- message: String,
- periodMs: Long,
- timeoutMs: Long,
- tryBlock: () -> Boolean
-): Long {
- var totalDurationMs = 0L
- while (!tryBlock()) {
- Thread.sleep(periodMs)
-
- totalDurationMs += periodMs
- if (totalDurationMs > timeoutMs) {
- Assert.fail(message)
- }
- }
- return totalDurationMs
-}
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java b/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
index 20e4282..4c5f4e8 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
@@ -22,9 +22,7 @@
import android.view.ViewGroupOverlay;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-@RequiresApi(18)
class ViewGroupOverlayApi18 implements ViewGroupOverlayImpl {
private final ViewGroupOverlay mViewGroupOverlay;
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java b/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
index 37126f5..52c2f97 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
@@ -44,10 +44,7 @@
* Backward-compatible {@link ViewGroup#getOverlay()}.
*/
static ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new ViewGroupOverlayApi18(group);
- }
- return ViewGroupOverlayApi14.createFrom(group);
+ return new ViewGroupOverlayApi18(group);
}
/**
@@ -56,14 +53,11 @@
static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.suppressLayout(group, suppress);
- } else if (Build.VERSION.SDK_INT >= 18) {
- hiddenSuppressLayout(group, suppress);
} else {
- ViewGroupUtilsApi14.suppressLayout(group, suppress);
+ hiddenSuppressLayout(group, suppress);
}
}
- @RequiresApi(18)
@SuppressLint("NewApi") // Lint doesn't know about the hidden method.
private static void hiddenSuppressLayout(@NonNull ViewGroup group, boolean suppress) {
if (sTryHiddenSuppressLayout) {
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java b/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
deleted file mode 100644
index 2a83cc1..0000000
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 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.transition;
-
-import android.animation.LayoutTransition;
-import android.annotation.SuppressLint;
-import android.util.Log;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-class ViewGroupUtilsApi14 {
-
- private static final String TAG = "ViewGroupUtilsApi14";
-
- private static final int LAYOUT_TRANSITION_CHANGING = 4;
-
- private static LayoutTransition sEmptyLayoutTransition;
-
- private static Field sLayoutSuppressedField;
- private static boolean sLayoutSuppressedFieldFetched;
-
- private static Method sCancelMethod;
- private static boolean sCancelMethodFetched;
-
- static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
- // Prepare the empty LayoutTransition
- if (sEmptyLayoutTransition == null) {
- sEmptyLayoutTransition = new LayoutTransition() {
- @Override
- public boolean isChangingLayout() {
- return true;
- }
- };
- sEmptyLayoutTransition.setAnimator(LayoutTransition.APPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, null);
- sEmptyLayoutTransition.setAnimator(LAYOUT_TRANSITION_CHANGING, null);
- }
- if (suppress) {
- // Save the current LayoutTransition
- final LayoutTransition layoutTransition = group.getLayoutTransition();
- if (layoutTransition != null) {
- if (layoutTransition.isRunning()) {
- cancelLayoutTransition(layoutTransition);
- }
- if (layoutTransition != sEmptyLayoutTransition) {
- group.setTag(R.id.transition_layout_save, layoutTransition);
- }
- }
- // Suppress the layout
- group.setLayoutTransition(sEmptyLayoutTransition);
- } else {
- // Thaw the layout suppression
- group.setLayoutTransition(null);
- // Request layout if necessary
- if (!sLayoutSuppressedFieldFetched) {
- try {
- sLayoutSuppressedField = ViewGroup.class.getDeclaredField("mLayoutSuppressed");
- sLayoutSuppressedField.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.i(TAG, "Failed to access mLayoutSuppressed field by reflection");
- }
- sLayoutSuppressedFieldFetched = true;
- }
- boolean layoutSuppressed = false;
- if (sLayoutSuppressedField != null) {
- try {
- layoutSuppressed = sLayoutSuppressedField.getBoolean(group);
- if (layoutSuppressed) {
- sLayoutSuppressedField.setBoolean(group, false);
- }
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Failed to get mLayoutSuppressed field by reflection");
- }
- }
- if (layoutSuppressed) {
- group.requestLayout();
- }
- // Restore the saved LayoutTransition
- final LayoutTransition layoutTransition =
- (LayoutTransition) group.getTag(R.id.transition_layout_save);
- if (layoutTransition != null) {
- group.setTag(R.id.transition_layout_save, null);
- group.setLayoutTransition(layoutTransition);
- }
- }
- }
-
- /**
- * Note, this is only called on API 17 and older.
- */
- @SuppressLint({"SoonBlockedPrivateApi", "BanUncheckedReflection"})
- private static void cancelLayoutTransition(LayoutTransition t) {
- if (!sCancelMethodFetched) {
- try {
- sCancelMethod = LayoutTransition.class.getDeclaredMethod("cancel");
- sCancelMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to access cancel method by reflection");
- }
- sCancelMethodFetched = true;
- }
- if (sCancelMethod != null) {
- try {
- sCancelMethod.invoke(t);
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Failed to access cancel method by reflection");
- } catch (InvocationTargetException e) {
- Log.i(TAG, "Failed to invoke cancel method by reflection");
- }
- }
- }
-
- private ViewGroupUtilsApi14() {
- }
-}
diff --git a/transition/transition/src/main/java/androidx/transition/ViewUtils.java b/transition/transition/src/main/java/androidx/transition/ViewUtils.java
index 388c11d..7d609b0 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewUtils.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewUtils.java
@@ -87,20 +87,14 @@
* Backward-compatible {@link View#getOverlay()}.
*/
static ViewOverlayImpl getOverlay(@NonNull View view) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new ViewOverlayApi18(view);
- }
- return ViewOverlayApi14.createFrom(view);
+ return new ViewOverlayApi18(view);
}
/**
* Backward-compatible {@link View#getWindowId()}.
*/
static @NonNull WindowIdImpl getWindowId(@NonNull View view) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new WindowIdApi18(view);
- }
- return new WindowIdApi14(view.getWindowToken());
+ return new WindowIdApi18(view);
}
static void setTransitionAlpha(@NonNull View view, float alpha) {
diff --git a/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java b/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java
deleted file mode 100644
index 6a9231e..0000000
--- a/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 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.transition;
-
-import android.os.IBinder;
-
-class WindowIdApi14 implements WindowIdImpl {
-
- private final IBinder mToken;
-
- WindowIdApi14(IBinder token) {
- mToken = token;
- }
-
- @Override
- public boolean equals(Object o) {
- return o instanceof WindowIdApi14 && ((WindowIdApi14) o).mToken.equals(this.mToken);
- }
-
- @Override
- public int hashCode() {
- return mToken.hashCode();
- }
-}
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TextField.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TextField.kt
index de4d6e1..7c71660 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TextField.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/TextField.kt
@@ -41,7 +41,7 @@
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.text.AndroidImeOptions
+import androidx.tv.foundation.text.PlatformImeOptions
import androidx.tv.foundation.text.TvKeyboardAlignment
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
@@ -87,7 +87,7 @@
},
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
- platformImeOptions = AndroidImeOptions(TvKeyboardAlignment.Left),
+ platformImeOptions = PlatformImeOptions(TvKeyboardAlignment.Left),
imeAction = ImeAction.Next
),
colors = OutlinedTextFieldDefaults.colors(
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 2049638..daa9a60 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -254,8 +254,8 @@
package androidx.tv.foundation.text {
public final class TvImeOptionsKt {
- method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.AndroidImeOptions AndroidImeOptions(androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
- method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.AndroidImeOptions keyboardAlignment(androidx.compose.ui.text.input.AndroidImeOptions, androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
+ method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.PlatformImeOptions PlatformImeOptions(androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
+ method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.PlatformImeOptions keyboardAlignment(androidx.compose.ui.text.input.PlatformImeOptions, androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
}
@SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public enum TvKeyboardAlignment {
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 2049638..daa9a60 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -254,8 +254,8 @@
package androidx.tv.foundation.text {
public final class TvImeOptionsKt {
- method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.AndroidImeOptions AndroidImeOptions(androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
- method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.AndroidImeOptions keyboardAlignment(androidx.compose.ui.text.input.AndroidImeOptions, androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
+ method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.PlatformImeOptions PlatformImeOptions(androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
+ method @SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public static androidx.compose.ui.text.input.PlatformImeOptions keyboardAlignment(androidx.compose.ui.text.input.PlatformImeOptions, androidx.tv.foundation.text.TvKeyboardAlignment horizontalAlignment);
}
@SuppressCompatibility @androidx.tv.foundation.ExperimentalTvFoundationApi public enum TvKeyboardAlignment {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/text/TvImeOptionsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/text/TvImeOptionsTest.kt
index 7a29001..c77b930 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/text/TvImeOptionsTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/text/TvImeOptionsTest.kt
@@ -16,7 +16,7 @@
package androidx.tv.foundation.text
-import androidx.compose.ui.text.input.AndroidImeOptions
+import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.tv.foundation.ExperimentalTvFoundationApi
@@ -32,7 +32,7 @@
fun privateImeOptions_keyboardAlignment() {
val privateImeOptions = "testOptions"
val keyboardAlignment = TvKeyboardAlignment.Left
- val imeOptions = AndroidImeOptions(privateImeOptions).keyboardAlignment(keyboardAlignment)
+ val imeOptions = PlatformImeOptions(privateImeOptions).keyboardAlignment(keyboardAlignment)
assertThat(
imeOptions.privateImeOptions == "$privateImeOptions,${keyboardAlignment.option}"
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/text/TvImeOptions.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/text/TvImeOptions.kt
index 135534a..da6e10b 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/text/TvImeOptions.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/text/TvImeOptions.kt
@@ -16,7 +16,7 @@
package androidx.tv.foundation.text
-import androidx.compose.ui.text.input.AndroidImeOptions
+import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.tv.foundation.ExperimentalTvFoundationApi
/**
@@ -28,9 +28,9 @@
* keyboard.
*/
@ExperimentalTvFoundationApi
-fun AndroidImeOptions(
+fun PlatformImeOptions(
horizontalAlignment: TvKeyboardAlignment
-) = AndroidImeOptions(horizontalAlignment.option)
+) = PlatformImeOptions(horizontalAlignment.option)
/**
* Adds the keyboard alignment option to the private IME configuration options.
@@ -41,12 +41,12 @@
* keyboard.
*/
@ExperimentalTvFoundationApi
-fun AndroidImeOptions.keyboardAlignment(
+fun PlatformImeOptions.keyboardAlignment(
horizontalAlignment: TvKeyboardAlignment
-): AndroidImeOptions {
+): PlatformImeOptions {
val privateImeOptions =
if (!privateImeOptions.isNullOrBlank()) this.privateImeOptions + "," else ""
- return AndroidImeOptions(privateImeOptions + horizontalAlignment.option)
+ return PlatformImeOptions(privateImeOptions + horizontalAlignment.option)
}
/**
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
index 2abc406..9e64aee 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
@@ -57,10 +57,8 @@
localeUtil.resetLocale()
localeUtil.setLocale(LocaleTestUtils.RTL_LANGUAGE)
}
- if (Build.VERSION.SDK_INT >= 18) {
- // Make sure accessibility is enabled (side effect of creating a UI Automator instance)
- uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- }
+ // Make sure accessibility is enabled (side effect of creating a UI Automator instance)
+ uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
override fun tearDown() {
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index f24e3ef..54fcef0 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -42,5 +42,5 @@
implementation 'androidx.core:core-ktx'
implementation(libs.material)
implementation(project(":profileinstaller:profileinstaller"))
- implementation 'androidx.wear:wear:1.1.0'
+ implementation 'androidx.wear:wear:1.3.0'
}
diff --git a/wear/compose/compose-ui-tooling/build.gradle b/wear/compose/compose-ui-tooling/build.gradle
index c31d84f..4d68f5b 100644
--- a/wear/compose/compose-ui-tooling/build.gradle
+++ b/wear/compose/compose-ui-tooling/build.gradle
@@ -28,7 +28,7 @@
implementation(libs.kotlinStdlibCommon)
implementation(project(":compose:ui:ui-tooling-preview"))
- implementation("androidx.wear:wear-tooling-preview:1.0.0-beta01")
+ implementation("androidx.wear:wear-tooling-preview:1.0.0")
samples(project(":wear:compose:compose-material-samples"))
}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index ace57b9..8a2f65e 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -389,7 +389,8 @@
public final class VersionBuilders {
}
- @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo> {
+ method public int compareTo(androidx.wear.protolayout.expression.VersionBuilders.VersionInfo);
method public int getMajor();
method public int getMinor();
}
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index ace57b9..8a2f65e 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -389,7 +389,8 @@
public final class VersionBuilders {
}
- @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class VersionBuilders.VersionInfo implements java.lang.Comparable<androidx.wear.protolayout.expression.VersionBuilders.VersionInfo> {
+ method public int compareTo(androidx.wear.protolayout.expression.VersionBuilders.VersionInfo);
method public int getMajor();
method public int getMinor();
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
index e5a748e..6041b9f 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
@@ -27,7 +27,13 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
-/** Indicates the minimum schema version the annotated type is supported at. */
+/**
+ * Indicates the minimum schema version the annotated type is supported at. {@link #major()} and
+ * {@link #minor()} correspond to {@link VersionBuilders.VersionInfo#getMajor()} and {@link
+ * VersionBuilders.VersionInfo#getMinor()} values reported by renderers/evaluators of ProtoLayout.
+ *
+ * <p>Note that {@link #minor()} version is usually in the form of {@code x00} such as 100, 200, ...
+ */
@MustBeDocumented
@Retention(CLASS)
@Target({TYPE, METHOD, CONSTRUCTOR, FIELD})
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java
index 56d8c841..7c0196c 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java
@@ -22,6 +22,8 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.expression.proto.VersionProto;
+import java.util.Objects;
+
/** Builders for the schema version information of a layout (or an expression). */
public final class VersionBuilders {
private VersionBuilders() {}
@@ -31,7 +33,7 @@
* layout).
*/
@RequiresSchemaVersion(major = 1, minor = 0)
- public static final class VersionInfo {
+ public static final class VersionInfo implements Comparable<VersionInfo> {
private final VersionProto.VersionInfo mImpl;
@Nullable private final Fingerprint mFingerprint;
@@ -94,6 +96,28 @@
return "VersionInfo{" + "major=" + getMajor() + ", minor=" + getMinor() + "}";
}
+ @Override
+ public int compareTo(@NonNull VersionInfo other) {
+ if (this.getMajor() == other.getMajor()) {
+ return Integer.compare(this.getMinor(), other.getMinor());
+ }
+ return Integer.compare(this.getMajor(), other.getMajor());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getMajor(), getMinor());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof VersionInfo) {
+ VersionInfo that = (VersionInfo) obj;
+ return this.getMajor() == that.getMajor() && this.getMinor() == that.getMinor();
+ }
+ return false;
+ }
+
/** Builder for {@link VersionInfo} */
public static final class Builder {
private final VersionProto.VersionInfo.Builder mImpl =
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java
index cc5c2b8..6ae2b23 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java
@@ -18,10 +18,16 @@
import static com.google.common.truth.Truth.assertThat;
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
public final class VersionInfoTest {
@Test
@@ -29,10 +35,22 @@
int major = 10;
int minor = 20;
- VersionBuilders.VersionInfo versionInfo =
- new VersionBuilders.VersionInfo.Builder().setMajor(major).setMinor(minor).build();
+ VersionInfo versionInfo = new VersionInfo.Builder().setMajor(major).setMinor(minor).build();
assertThat(versionInfo.toProto().getMajor()).isEqualTo(major);
assertThat(versionInfo.toProto().getMinor()).isEqualTo(minor);
}
+
+ @Test
+ public void versionInfoComparison() {
+ VersionInfo v1_0 = new VersionInfo.Builder().setMajor(1).setMinor(0).build();
+ VersionInfo v1_1 = new VersionInfo.Builder().setMajor(1).setMinor(1).build();
+ VersionInfo v2_0 = new VersionInfo.Builder().setMajor(2).setMinor(0).build();
+ VersionInfo v2_1 = new VersionInfo.Builder().setMajor(2).setMinor(1).build();
+ List<VersionInfo> versions = Arrays.asList(v2_1, v2_0, v1_1, v2_0, v1_0);
+
+ Collections.sort(versions);
+
+ assertThat(versions).containsExactly(v1_0, v1_1, v2_0, v2_0, v2_1).inOrder();
+ }
}
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index 95af193..8e93c514 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -28,7 +28,7 @@
implementation(project(":wear:protolayout:protolayout-proto"))
implementation(project(":wear:tiles:tiles"))
- api("androidx.wear:wear-tooling-preview:1.0.0-alpha01")
+ api("androidx.wear:wear-tooling-preview:1.0.0")
api("androidx.annotation:annotation:1.6.0")
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index ff20104..ea4cc25 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -124,10 +124,10 @@
* @property dataSource The [ComponentName] of the
* [androidx.wear.watchface.complications.datasource.ComplicationDataSourceService] that provided
* the ComplicationData. This may be `null` when run on old systems.
- * @property persistencePolicy The [ComplicationPersistencePolicy] for this complication. This
- * requires the watchface to be built with a compatible library to work.
- * @property displayPolicy The [ComplicationDisplayPolicy] for this complication. This requires the
- * watchface to be built with a compatible library to work.
+ * @property persistencePolicy The [persistence policy][ComplicationPersistencePolicies] for this
+ * complication. This requires the watchface to be built with a compatible library to work.
+ * @property displayPolicy The [display policy][ComplicationDisplayPolicies] for this complication.
+ * This requires the watchface to be built with a compatible library to work.
* @property dynamicValueInvalidationFallback Used in case any dynamic value has been invalidated.
*
* IMPORTANT: This is only used when the system supports dynamic values. See each dynamic field's
@@ -255,7 +255,7 @@
return this as BuilderT
}
- /** Sets the complication's [ComplicationPersistencePolicy]. */
+ /** Sets the complication's [persistence policy][ComplicationPersistencePolicies]. */
@Suppress("UNCHECKED_CAST", "SetterReturnsThis")
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public fun setPersistencePolicy(
@@ -265,7 +265,7 @@
return this as BuilderT
}
- /** Sets the complication's [ComplicationDisplayPolicy]. */
+ /** Sets the complication's [display policy][ComplicationDisplayPolicies]. */
@Suppress("UNCHECKED_CAST", "SetterReturnsThis")
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public fun setDisplayPolicy(@ComplicationDisplayPolicy displayPolicy: Int): BuilderT {
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index 1462e5e..8e33dbf 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -46,6 +46,12 @@
implementation(libs.kotlinTestJunit)
}
}
+
+ androidInstrumentedTest {
+ dependencies {
+ implementation(libs.testRunner)
+ }
+ }
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index 72a068d..72d4ff3 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -28,7 +28,6 @@
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@@ -155,9 +154,7 @@
@After
public void tearDown() {
- if (Build.VERSION.SDK_INT >= 18) {
- mHandlerThread.quitSafely();
- }
+ mHandlerThread.quitSafely();
}
@Test