Merge "Add upstream to TextLayout horizontal" into androidx-main
diff --git a/window/window/src/androidTest/java/androidx/window/layout/ExtensionWindowLayoutInfoBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/ExtensionWindowLayoutInfoBackendTest.kt
index ea99c4d..bdc40da 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/ExtensionWindowLayoutInfoBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/ExtensionWindowLayoutInfoBackendTest.kt
@@ -24,6 +24,7 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.window.TestActivity
import androidx.window.TestConsumer
+import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
import androidx.window.extensions.layout.WindowLayoutComponent
@@ -50,6 +51,10 @@
public val activityScenario: ActivityScenarioRule<TestActivity> =
ActivityScenarioRule(TestActivity::class.java)
+ private val consumerAdapter = ConsumerAdapter(
+ ExtensionWindowLayoutInfoBackendTest::class.java.classLoader!!
+ )
+
@Before
fun setUp() {
assumeTrue("Must be at least API 24", Build.VERSION_CODES.N <= Build.VERSION.SDK_INT)
@@ -57,15 +62,15 @@
@Test
public fun testExtensionWindowBackend_delegatesToWindowLayoutComponent() {
- val component = mock<WindowLayoutComponent>()
+ val component = RequestTrackingWindowComponent()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
activityScenario.scenario.onActivity { activity ->
val consumer = TestConsumer<WindowLayoutInfo>()
backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
- verify(component).addWindowLayoutInfoListener(eq(activity), any())
+ assertTrue("Expected call with Activity: $activity", component.hasAddCall(activity))
}
}
@@ -73,7 +78,7 @@
public fun testExtensionWindowBackend_registerAtMostOnce() {
val component = mock<WindowLayoutComponent>()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
activityScenario.scenario.onActivity { activity ->
val consumer = TestConsumer<WindowLayoutInfo>()
@@ -95,7 +100,7 @@
val consumer = invocation.getArgument(1) as JavaConsumer<OEMWindowLayoutInfo>
consumer.accept(OEMWindowLayoutInfo(emptyList()))
}
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
activityScenario.scenario.onActivity { activity ->
val consumer = TestConsumer<WindowLayoutInfo>()
@@ -116,7 +121,7 @@
val consumer = invocation.getArgument(1) as JavaConsumer<OEMWindowLayoutInfo>
consumer.accept(OEMWindowLayoutInfo(emptyList()))
}
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
activityScenario.scenario.onActivity { activity ->
val consumer = TestConsumer<WindowLayoutInfo>()
@@ -131,7 +136,7 @@
public fun testExtensionWindowBackend_removeMatchingCallback() {
val component = mock<WindowLayoutComponent>()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
activityScenario.scenario.onActivity { activity ->
val consumer = TestConsumer<WindowLayoutInfo>()
@@ -148,7 +153,7 @@
public fun testRegisterLayoutChangeCallback_clearListeners() {
activityScenario.scenario.onActivity { activity ->
val component = FakeWindowComponent()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
// Check registering the layout change callback
val firstConsumer = mock<Consumer<WindowLayoutInfo>>()
@@ -177,7 +182,7 @@
public fun testLayoutChangeCallback_emitNewValue() {
activityScenario.scenario.onActivity { activity ->
val component = FakeWindowComponent()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
// Check that callbacks from the extension are propagated correctly
val consumer = mock<Consumer<WindowLayoutInfo>>()
@@ -194,7 +199,7 @@
public fun testWindowLayoutInfo_updatesOnSubsequentRegistration() {
activityScenario.scenario.onActivity { activity ->
val component = FakeWindowComponent()
- val backend = ExtensionWindowLayoutInfoBackend(component)
+ val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
val consumer = TestConsumer<WindowLayoutInfo>()
val oemWindowLayoutInfo = newTestOEMWindowLayoutInfo(activity)
val expected = listOf(
@@ -223,6 +228,27 @@
}
}
+ private class RequestTrackingWindowComponent : WindowLayoutComponent {
+
+ val records = mutableListOf<AddCall>()
+
+ override fun addWindowLayoutInfoListener(
+ activity: Activity,
+ consumer: JavaConsumer<OEMWindowLayoutInfo>
+ ) {
+ records.add(AddCall(activity))
+ }
+
+ override fun removeWindowLayoutInfoListener(consumer: JavaConsumer<OEMWindowLayoutInfo>) {
+ }
+
+ class AddCall(val activity: Activity)
+
+ fun hasAddCall(activity: Activity): Boolean {
+ return records.any { addRecord -> addRecord.activity == activity }
+ }
+ }
+
private class FakeWindowComponent : WindowLayoutComponent {
val consumers = mutableListOf<JavaConsumer<OEMWindowLayoutInfo>>()
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
index bfa5b7f..246e9a3 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
@@ -16,6 +16,7 @@
package androidx.window.layout
+import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.WindowExtensionsProvider
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@@ -34,7 +35,10 @@
*/
@Test
fun windowLayoutComponentIsAvailable_ifProviderIsAvailable() {
- val safeComponent = SafeWindowLayoutComponentProvider.windowLayoutComponent
+ val loader = SafeWindowLayoutComponentProviderTest::class.java.classLoader!!
+ val consumerAdapter = ConsumerAdapter(loader)
+ val safeComponent = SafeWindowLayoutComponentProvider(loader, consumerAdapter)
+ .windowLayoutComponent
try {
val extensions = WindowExtensionsProvider.getWindowExtensions()
diff --git a/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt b/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt
new file mode 100644
index 0000000..e81e16e
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.core
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import androidx.annotation.CheckResult
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+/**
+ * An adapter over {@link java.util.function.Consumer} to workaround mismatch in expected extension
+ * API signatures after library desugaring. See b/203472665
+ */
+@SuppressLint("BanUncheckedReflection")
+internal class ConsumerAdapter(
+ private val loader: ClassLoader
+) {
+ internal fun consumerClassOrNull(): Class<*>? {
+ return try {
+ unsafeConsumerClass()
+ } catch (e: ClassNotFoundException) {
+ null
+ }
+ }
+
+ private fun unsafeConsumerClass(): Class<*> {
+ return loader.loadClass("java.util.function.Consumer")
+ }
+
+ internal interface Subscription {
+ fun dispose()
+ }
+
+ private fun <T : Any> buildConsumer(clazz: KClass<T>, consumer: (T) -> Unit): Any {
+ val handler = ConsumerHandler(clazz, consumer)
+ return Proxy.newProxyInstance(loader, arrayOf(unsafeConsumerClass()), handler)
+ }
+
+ fun <T : Any> addConsumer(
+ obj: Any,
+ clazz: KClass<T>,
+ methodName: String,
+ consumer: (T) -> Unit
+ ) {
+ obj.javaClass.getMethod(methodName, unsafeConsumerClass())
+ .invoke(obj, buildConsumer(clazz, consumer))
+ }
+
+ @CheckResult
+ fun <T : Any> createSubscription(
+ obj: Any,
+ clazz: KClass<T>,
+ addMethodName: String,
+ removeMethodName: String,
+ activity: Activity,
+ consumer: (T) -> Unit
+ ): Subscription {
+ val javaConsumer = buildConsumer(clazz, consumer)
+ obj.javaClass.getMethod(addMethodName, Activity::class.java, unsafeConsumerClass())
+ .invoke(obj, activity, javaConsumer)
+ val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass())
+ return object : Subscription {
+ override fun dispose() {
+ removeMethod.invoke(obj, javaConsumer)
+ }
+ }
+ }
+
+ private class ConsumerHandler<T : Any>(
+ private val clazz: KClass<T>,
+ private val consumer: (T) -> Unit
+ ) : InvocationHandler {
+ override fun invoke(obj: Any, method: Method, parameters: Array<out Any>?): Any {
+ return when {
+ method.isAccept(parameters) -> {
+ val argument = clazz.cast(parameters?.get(0))
+ invokeAccept(argument)
+ }
+ method.isEquals(parameters) -> {
+ obj === parameters?.get(0)
+ }
+ method.isHashCode(parameters) -> {
+ consumer.hashCode()
+ }
+ method.isToString(parameters) -> {
+ consumer.toString()
+ }
+ else -> {
+ throw UnsupportedOperationException(
+ "Unexpected method call object:$obj, method: $method, args: $parameters"
+ )
+ }
+ }
+ }
+
+ fun invokeAccept(parameter: T) {
+ consumer(parameter)
+ }
+
+ private fun Method.isEquals(args: Array<out Any>?): Boolean {
+ return name == "equals" && returnType.equals(Boolean::class.java) && args?.size == 1
+ }
+
+ private fun Method.isHashCode(args: Array<out Any>?): Boolean {
+ return name == "hashCode" && returnType.equals(Int::class.java) && args == null
+ }
+
+ private fun Method.isAccept(args: Array<out Any>?): Boolean {
+ return name == "accept" && args?.size == 1
+ }
+
+ private fun Method.isToString(args: Array<out Any>?): Boolean {
+ return name == "toString" && returnType.equals(String::class.java) && args == null
+ }
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/core/PredicateAdapter.kt b/window/window/src/main/java/androidx/window/core/PredicateAdapter.kt
new file mode 100644
index 0000000..0685df8
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/core/PredicateAdapter.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.core
+
+import android.annotation.SuppressLint
+import android.util.Pair
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+/**
+ * An adapter over {@link java.util.function.Predicate} to workaround mismatch in expected extension
+ * API signatures after library desugaring. See b/203472665
+ */
+@SuppressLint("BanUncheckedReflection")
+internal class PredicateAdapter(
+ private val loader: ClassLoader
+) {
+ internal fun predicateClassOrNull(): Class<*>? {
+ return try {
+ predicateClassOrThrow()
+ } catch (e: ClassNotFoundException) {
+ null
+ }
+ }
+
+ private fun predicateClassOrThrow(): Class<*> {
+ return loader.loadClass("java.util.function.Predicate")
+ }
+
+ fun <T : Any> buildPredicate(clazz: KClass<T>, predicate: (T) -> Boolean): Any {
+ val predicateHandler = PredicateStubHandler(
+ clazz,
+ predicate
+ )
+ return Proxy.newProxyInstance(loader, arrayOf(predicateClassOrThrow()), predicateHandler)
+ }
+
+ fun <T : Any, U : Any> buildPairPredicate(
+ firstClazz: KClass<T>,
+ secondClazz: KClass<U>,
+ predicate: (T, U) -> Boolean
+ ): Any {
+ val predicateHandler = PairPredicateStubHandler(
+ firstClazz,
+ secondClazz,
+ predicate
+ )
+
+ return Proxy.newProxyInstance(loader, arrayOf(predicateClassOrThrow()), predicateHandler)
+ }
+
+ private abstract class BaseHandler<T : Any>(private val clazz: KClass<T>) : InvocationHandler {
+ override fun invoke(obj: Any, method: Method, parameters: Array<out Any>?): Any {
+ return when {
+ method.isTest(parameters) -> {
+ val argument = clazz.cast(parameters?.get(0))
+ invokeTest(obj, argument)
+ }
+ method.isEquals(parameters) -> {
+ obj === parameters?.get(0)!!
+ }
+ method.isHashCode(parameters) -> {
+ hashCode()
+ }
+ method.isToString(parameters) -> {
+ toString()
+ }
+ else -> {
+ throw UnsupportedOperationException(
+ "Unexpected method call object:$obj, method: $method, args: $parameters"
+ )
+ }
+ }
+ }
+
+ abstract fun invokeTest(obj: Any, parameter: T): Boolean
+
+ protected fun Method.isEquals(args: Array<out Any>?): Boolean {
+ return name == "equals" && returnType.equals(Boolean::class.java) && args?.size == 1
+ }
+
+ protected fun Method.isHashCode(args: Array<out Any>?): Boolean {
+ return name == "hashCode" && returnType.equals(Int::class.java) && args == null
+ }
+
+ protected fun Method.isTest(args: Array<out Any>?): Boolean {
+ return name == "test" && returnType.equals(Boolean::class.java) && args?.size == 1
+ }
+
+ protected fun Method.isToString(args: Array<out Any>?): Boolean {
+ return name == "toString" && returnType.equals(String::class.java) && args == null
+ }
+ }
+
+ private class PredicateStubHandler<T : Any>(
+ clazzT: KClass<T>,
+ private val predicate: (T) -> Boolean
+ ) : BaseHandler<T>(clazzT) {
+ override fun invokeTest(obj: Any, parameter: T): Boolean {
+ return predicate(parameter)
+ }
+
+ override fun hashCode(): Int {
+ return predicate.hashCode()
+ }
+
+ override fun toString(): String {
+ return predicate.toString()
+ }
+ }
+
+ private class PairPredicateStubHandler<T : Any, U : Any>(
+ private val clazzT: KClass<T>,
+ private val clazzU: KClass<U>,
+ private val predicate: (T, U) -> Boolean
+ ) : BaseHandler<Pair<*, *>>(Pair::class) {
+ override fun invokeTest(obj: Any, parameter: Pair<*, *>): Boolean {
+ val t = clazzT.cast(parameter.first)
+ val u = clazzU.cast(parameter.second)
+ return predicate(t, u)
+ }
+
+ override fun hashCode(): Int {
+ return predicate.hashCode()
+ }
+
+ override fun toString(): String {
+ return predicate.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index abfd24d..b5ff4b2 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -19,27 +19,31 @@
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
-import android.util.Pair
import android.view.WindowMetrics
import androidx.window.core.ExperimentalWindowApi
-import java.lang.IllegalArgumentException
-import java.util.function.Predicate
+import androidx.window.core.PredicateAdapter
+import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
+import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
+import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import androidx.window.extensions.embedding.SplitPairRule as OEMSplitPairRule
import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
+import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
/**
* Adapter class that translates data classes between Extension and Jetpack interfaces.
*/
@ExperimentalWindowApi
-internal class EmbeddingAdapter {
- fun translate(
- splitInfoList: List<androidx.window.extensions.embedding.SplitInfo>
- ): List<SplitInfo> {
+internal class EmbeddingAdapter(
+ private val predicateAdapter: PredicateAdapter
+) {
+
+ fun translate(splitInfoList: List<OEMSplitInfo>): List<SplitInfo> {
return splitInfoList.map(::translate)
}
- private fun translate(splitInfo: androidx.window.extensions.embedding.SplitInfo): SplitInfo {
+ private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
val primaryActivityStack = splitInfo.primaryActivityStack
val isPrimaryStackEmpty = try {
primaryActivityStack.isEmpty
@@ -60,65 +64,62 @@
}
val secondaryFragment = ActivityStack(
secondaryActivityStack.activities,
- isSecondaryStackEmpty)
+ isSecondaryStackEmpty
+ )
return SplitInfo(primaryFragment, secondaryFragment, splitInfo.splitRatio)
}
@SuppressLint("ClassVerificationFailure", "NewApi")
- fun translateActivityPairPredicates(
- splitPairFilters: Set<SplitPairFilter>
- ): Predicate<Pair<Activity, Activity>> {
- return Predicate<Pair<Activity, Activity>> {
- (first, second) ->
+ private fun translateActivityPairPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
+ return predicateAdapter.buildPairPredicate(
+ Activity::class,
+ Activity::class
+ ) { first: Activity, second: Activity ->
splitPairFilters.any { filter -> filter.matchesActivityPair(first, second) }
}
}
@SuppressLint("ClassVerificationFailure", "NewApi")
- fun translateActivityIntentPredicates(
- splitPairFilters: Set<SplitPairFilter>
- ): Predicate<Pair<Activity, Intent>> {
- return Predicate<Pair<Activity, Intent>> {
- (first, second) ->
+ private fun translateActivityIntentPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
+ return predicateAdapter.buildPairPredicate(
+ Activity::class,
+ Intent::class
+ ) { first, second ->
splitPairFilters.any { filter -> filter.matchesActivityIntentPair(first, second) }
}
}
@SuppressLint("ClassVerificationFailure", "NewApi")
- fun translateParentMetricsPredicate(
- splitRule: SplitRule
- ): Predicate<WindowMetrics> {
- return Predicate<WindowMetrics> {
- windowMetrics ->
+ private fun translateParentMetricsPredicate(splitRule: SplitRule): Any {
+ return predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
splitRule.checkParentMetrics(windowMetrics)
}
}
@SuppressLint("ClassVerificationFailure", "NewApi")
- fun translateActivityPredicates(
- activityFilters: Set<ActivityFilter>
- ): Predicate<Activity> {
- return Predicate<Activity> {
- activity ->
+ private fun translateActivityPredicates(activityFilters: Set<ActivityFilter>): Any {
+ return predicateAdapter.buildPredicate(Activity::class) { activity ->
activityFilters.any { filter -> filter.matchesActivity(activity) }
}
}
@SuppressLint("ClassVerificationFailure", "NewApi")
- fun translateIntentPredicates(
- activityFilters: Set<ActivityFilter>
- ): Predicate<Intent> {
- return Predicate<Intent> {
- intent ->
+ private fun translateIntentPredicates(activityFilters: Set<ActivityFilter>): Any {
+ return predicateAdapter.buildPredicate(Intent::class) { intent ->
activityFilters.any { filter -> filter.matchesIntent(intent) }
}
}
@SuppressLint("WrongConstant") // Converting from Jetpack to Extensions constants
private fun translateSplitPairRule(
- rule: SplitPairRule
- ): androidx.window.extensions.embedding.SplitPairRule {
- val builder = SplitPairRuleBuilder(
+ rule: SplitPairRule,
+ predicateClass: Class<*>
+ ): OEMSplitPairRule {
+ val builder = SplitPairRuleBuilder::class.java.getConstructor(
+ predicateClass,
+ predicateClass,
+ predicateClass
+ ).newInstance(
translateActivityPairPredicates(rule.filters),
translateActivityIntentPredicates(rule.filters),
translateParentMetricsPredicate(rule)
@@ -138,9 +139,15 @@
@SuppressLint("WrongConstant") // Converting from Jetpack to Extensions constants
private fun translateSplitPlaceholderRule(
- rule: SplitPlaceholderRule
- ): androidx.window.extensions.embedding.SplitPlaceholderRule {
- val builder = SplitPlaceholderRuleBuilder(
+ rule: SplitPlaceholderRule,
+ predicateClass: Class<*>
+ ): OEMSplitPlaceholderRule {
+ val builder = SplitPlaceholderRuleBuilder::class.java.getConstructor(
+ Intent::class.java,
+ predicateClass,
+ predicateClass,
+ predicateClass
+ ).newInstance(
rule.placeholderIntent,
translateActivityPredicates(rule.filters),
translateIntentPredicates(rule.filters),
@@ -158,33 +165,30 @@
return builder.build()
}
- fun translate(
- rules: Set<EmbeddingRule>
- ): Set<androidx.window.extensions.embedding.EmbeddingRule> {
- return rules.map {
- rule ->
+ private fun translateActivityRule(
+ rule: ActivityRule,
+ predicateClass: Class<*>
+ ): OEMActivityRule {
+ return ActivityRuleBuilder::class.java.getConstructor(
+ predicateClass,
+ predicateClass
+ ).newInstance(
+ translateActivityPredicates(rule.filters),
+ translateIntentPredicates(rule.filters)
+ )
+ .setShouldAlwaysExpand(rule.alwaysExpand)
+ .build()
+ }
+
+ fun translate(rules: Set<EmbeddingRule>): Set<OEMEmbeddingRule> {
+ val predicateClass = predicateAdapter.predicateClassOrNull() ?: return emptySet()
+ return rules.map { rule ->
when (rule) {
- is SplitPairRule ->
- translateSplitPairRule(rule)
- is SplitPlaceholderRule ->
- translateSplitPlaceholderRule(rule)
- is ActivityRule ->
- ActivityRuleBuilder(
- translateActivityPredicates(rule.filters),
- translateIntentPredicates(rule.filters)
- )
- .setShouldAlwaysExpand(rule.alwaysExpand)
- .build()
+ is SplitPairRule -> translateSplitPairRule(rule, predicateClass)
+ is SplitPlaceholderRule -> translateSplitPlaceholderRule(rule, predicateClass)
+ is ActivityRule -> translateActivityRule(rule, predicateClass)
else -> throw IllegalArgumentException("Unsupported rule type")
}
}.toSet()
}
-
- private operator fun <F, S> Pair<F, S>.component1(): F {
- return first
- }
-
- private operator fun <F, S> Pair<F, S>.component2(): S {
- return second
- }
-}
\ No newline at end of file
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index b5966da..f2129b9 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -17,13 +17,13 @@
package androidx.window.embedding
import android.util.Log
+import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExperimentalWindowApi
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.extensions.embedding.ActivityEmbeddingComponent
-import androidx.window.extensions.embedding.SplitInfo
-import java.util.function.Consumer
-import androidx.window.extensions.embedding.EmbeddingRule as ExtensionsEmbeddingRule
+import java.lang.reflect.Proxy
+import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
/**
* Adapter implementation for different historical versions of activity embedding OEM interface in
@@ -32,26 +32,23 @@
@ExperimentalWindowApi
internal class EmbeddingCompat constructor(
private val embeddingExtension: ActivityEmbeddingComponent,
- private val adapter: EmbeddingAdapter
+ private val adapter: EmbeddingAdapter,
+ private val consumerAdapter: ConsumerAdapter
) : EmbeddingInterfaceCompat {
- constructor() : this(
- embeddingComponent(),
- EmbeddingAdapter()
- )
override fun setSplitRules(rules: Set<EmbeddingRule>) {
- embeddingExtension.setEmbeddingRules(adapter.translate(rules))
+ val r = adapter.translate(rules)
+ embeddingExtension.setEmbeddingRules(r)
}
override fun setEmbeddingCallback(embeddingCallback: EmbeddingCallbackInterface) {
- try {
- embeddingExtension.setSplitInfoCallback { splitInfoList ->
- embeddingCallback.onSplitInfoChanged(
- adapter.translate(splitInfoList)
- )
- }
- } catch (e: NoSuchMethodError) {
- // TODO(b/203472665): Remove the try-catch wrapper after the issue is resolved
+ consumerAdapter.addConsumer(
+ embeddingExtension,
+ List::class,
+ "setSplitInfoCallback"
+ ) { values ->
+ val splitInfoList = values.filterIsInstance<OEMSplitInfo>()
+ embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
}
}
@@ -94,22 +91,16 @@
fun embeddingComponent(): ActivityEmbeddingComponent {
return if (isEmbeddingAvailable()) {
WindowExtensionsProvider.getWindowExtensions().getActivityEmbeddingComponent()
- ?: EmptyEmbeddingComponent()
+ ?: Proxy.newProxyInstance(
+ EmbeddingCompat::class.java.classLoader,
+ arrayOf(ActivityEmbeddingComponent::class.java)
+ ) { _, _, _ -> } as ActivityEmbeddingComponent
} else {
- EmptyEmbeddingComponent()
+ Proxy.newProxyInstance(
+ EmbeddingCompat::class.java.classLoader,
+ arrayOf(ActivityEmbeddingComponent::class.java)
+ ) { _, _, _ -> } as ActivityEmbeddingComponent
}
}
}
}
-
-// Empty implementation of the embedding component to use when the device doesn't provide one and
-// avoid null checks.
-private class EmptyEmbeddingComponent : ActivityEmbeddingComponent {
- override fun setEmbeddingRules(splitRules: MutableSet<ExtensionsEmbeddingRule>) {
- // empty
- }
-
- override fun setSplitInfoCallback(consumer: Consumer<MutableList<SplitInfo>>) {
- // empty
- }
-}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index 9eb1f0e..a134c8e 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -21,7 +21,9 @@
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
+import androidx.window.core.ConsumerAdapter
import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.PredicateAdapter
import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArraySet
@@ -74,7 +76,13 @@
if (isExtensionVersionSupported(EmbeddingCompat.getExtensionApiLevel()) &&
EmbeddingCompat.isEmbeddingAvailable()
) {
- impl = EmbeddingCompat()
+ impl = EmbeddingBackend::class.java.classLoader?.let { loader ->
+ EmbeddingCompat(
+ EmbeddingCompat.embeddingComponent(),
+ EmbeddingAdapter(PredicateAdapter(loader)),
+ ConsumerAdapter(loader)
+ )
+ }
// TODO(b/190433400): Check API conformance
}
} catch (t: Throwable) {
diff --git a/window/window/src/main/java/androidx/window/layout/ExtensionWindowLayoutInfoBackend.kt b/window/window/src/main/java/androidx/window/layout/ExtensionWindowLayoutInfoBackend.kt
index 0dde6b2..da5995e 100644
--- a/window/window/src/main/java/androidx/window/layout/ExtensionWindowLayoutInfoBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/ExtensionWindowLayoutInfoBackend.kt
@@ -16,17 +16,16 @@
package androidx.window.layout
-import android.annotation.SuppressLint
import android.app.Activity
import androidx.annotation.GuardedBy
import androidx.core.util.Consumer
+import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.layout.WindowLayoutComponent
import androidx.window.layout.ExtensionsWindowLayoutInfoAdapter.translate
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
-import java.util.function.Consumer as JavaConsumer
/**
* A wrapper around [WindowLayoutComponent] that ensures
@@ -34,7 +33,8 @@
* there are active listeners.
*/
internal class ExtensionWindowLayoutInfoBackend(
- private val component: WindowLayoutComponent
+ private val component: WindowLayoutComponent,
+ private val consumerAdapter: ConsumerAdapter
) : WindowBackend {
private val extensionWindowBackendLock = ReentrantLock()
@@ -42,6 +42,8 @@
private val activityToListeners = mutableMapOf<Activity, MulticastConsumer>()
@GuardedBy("lock")
private val listenerToActivity = mutableMapOf<Consumer<WindowLayoutInfo>, Activity>()
+ @GuardedBy("lock")
+ private val consumerToToken = mutableMapOf<MulticastConsumer, ConsumerAdapter.Subscription>()
/**
* Registers a listener to consume new values of [WindowLayoutInfo]. If there was a listener
@@ -65,7 +67,16 @@
activityToListeners[activity] = consumer
listenerToActivity[callback] = activity
consumer.addListener(callback)
- component.addWindowLayoutInfoListener(activity, consumer)
+ val disposableToken = consumerAdapter.createSubscription(
+ component,
+ OEMWindowLayoutInfo::class,
+ "addWindowLayoutInfoListener",
+ "removeWindowLayoutInfoListener",
+ activity
+ ) { value ->
+ consumer.accept(value)
+ }
+ consumerToToken[consumer] = disposableToken
}
}
}
@@ -81,20 +92,19 @@
val multicastListener = activityToListeners[activity] ?: return
multicastListener.removeListener(callback)
if (multicastListener.isEmpty()) {
- component.removeWindowLayoutInfoListener(multicastListener)
+ consumerToToken.remove(multicastListener)?.dispose()
}
}
}
/**
- * A class that implements [JavaConsumer] by aggregating multiple instances of [JavaConsumer]
+ * A class that implements [Consumer] by aggregating multiple instances of [Consumer]
* and multicasting each value that is consumed. [MulticastConsumer] also replays the last known
* value whenever a new consumer registers.
*/
- @SuppressLint("NewApi") // TODO(b/205656281) window-extensions is only available in R+
private class MulticastConsumer(
private val activity: Activity
- ) : JavaConsumer<OEMWindowLayoutInfo> {
+ ) : Consumer<OEMWindowLayoutInfo> {
private val multicastConsumerLock = ReentrantLock()
@GuardedBy("lock")
private var lastKnownValue: WindowLayoutInfo? = null
diff --git a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
index 6785ecf..6c9ec9d 100644
--- a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
@@ -18,65 +18,62 @@
import android.app.Activity
import android.graphics.Rect
-import android.os.Build
-import androidx.annotation.RequiresApi
+import androidx.window.core.ConsumerAdapter
import androidx.window.extensions.WindowExtensionsProvider
import androidx.window.extensions.layout.WindowLayoutComponent
import java.lang.reflect.Method
import java.lang.reflect.Modifier
-import java.util.function.Consumer
import kotlin.reflect.KClass
-internal object SafeWindowLayoutComponentProvider {
+internal class SafeWindowLayoutComponentProvider(
+ private val loader: ClassLoader,
+ private val consumerAdapter: ConsumerAdapter
+) {
- val windowLayoutComponent: WindowLayoutComponent? by lazy {
- val loader = SafeWindowLayoutComponentProvider::class.java.classLoader
- if (loader != null && canUseWindowLayoutComponent(loader)) {
- try {
- WindowExtensionsProvider.getWindowExtensions().windowLayoutComponent
- } catch (e: UnsupportedOperationException) {
+ val windowLayoutComponent: WindowLayoutComponent?
+ get() {
+ return if (canUseWindowLayoutComponent()) {
+ try {
+ WindowExtensionsProvider.getWindowExtensions().windowLayoutComponent
+ } catch (e: UnsupportedOperationException) {
+ null
+ }
+ } else {
null
}
- } else {
- null
}
+
+ private fun canUseWindowLayoutComponent(): Boolean {
+ return isWindowLayoutProviderValid() &&
+ isWindowExtensionsValid() &&
+ isWindowLayoutComponentValid() &&
+ isFoldingFeatureValid()
}
- private fun canUseWindowLayoutComponent(classLoader: ClassLoader): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- isWindowLayoutProviderValid(classLoader) &&
- isWindowExtensionsValid(classLoader) &&
- isWindowLayoutComponentValid(classLoader) &&
- isFoldingFeatureValid(classLoader)
- } else {
- false
- }
- }
-
- private fun isWindowLayoutProviderValid(classLoader: ClassLoader): Boolean {
+ private fun isWindowLayoutProviderValid(): Boolean {
return validate {
- val providerClass = windowExtensionsProviderClass(classLoader)
+ val providerClass = windowExtensionsProviderClass
val getWindowExtensionsMethod = providerClass.getDeclaredMethod("getWindowExtensions")
- val windowExtensionsClass = windowExtensionsClass(classLoader)
+ val windowExtensionsClass = windowExtensionsClass
getWindowExtensionsMethod.doesReturn(windowExtensionsClass) &&
getWindowExtensionsMethod.isPublic
}
}
- private fun isWindowExtensionsValid(classLoader: ClassLoader): Boolean {
+ private fun isWindowExtensionsValid(): Boolean {
return validate {
- val extensionsClass = windowExtensionsClass(classLoader)
+ val extensionsClass = windowExtensionsClass
val getWindowLayoutComponentMethod =
extensionsClass.getMethod("getWindowLayoutComponent")
- val windowLayoutComponentClass = windowLayoutComponentClass(classLoader)
+ val windowLayoutComponentClass = windowLayoutComponentClass
getWindowLayoutComponentMethod.isPublic &&
getWindowLayoutComponentMethod.doesReturn(windowLayoutComponentClass)
}
}
- private fun isFoldingFeatureValid(classLoader: ClassLoader): Boolean {
+ private fun isFoldingFeatureValid(): Boolean {
return validate {
- val foldingFeatureClass = foldingFeatureClass(classLoader)
+ val foldingFeatureClass = foldingFeatureClass
val getBoundsMethod = foldingFeatureClass.getMethod("getBounds")
val getTypeMethod = foldingFeatureClass.getMethod("getType")
val getStateMethod = foldingFeatureClass.getMethod("getState")
@@ -89,18 +86,18 @@
}
}
- @RequiresApi(24)
- private fun isWindowLayoutComponentValid(classLoader: ClassLoader): Boolean {
+ private fun isWindowLayoutComponentValid(): Boolean {
return validate {
- val windowLayoutComponent = windowLayoutComponentClass(classLoader)
+ val consumerClass = consumerAdapter.consumerClassOrNull() ?: return@validate false
+ val windowLayoutComponent = windowLayoutComponentClass
val addListenerMethod = windowLayoutComponent
.getMethod(
"addWindowLayoutInfoListener",
Activity::class.java,
- Consumer::class.java
+ consumerClass
)
val removeListenerMethod = windowLayoutComponent
- .getMethod("removeWindowLayoutInfoListener", Consumer::class.java)
+ .getMethod("removeWindowLayoutInfoListener", consumerClass)
addListenerMethod.isPublic && removeListenerMethod.isPublic
}
}
@@ -128,15 +125,23 @@
return returnType.equals(clazz)
}
- private fun windowExtensionsProviderClass(classLoader: ClassLoader) =
- classLoader.loadClass("androidx.window.extensions.WindowExtensionsProvider")
+ private val windowExtensionsProviderClass: Class<*>
+ get() {
+ return loader.loadClass("androidx.window.extensions.WindowExtensionsProvider")
+ }
- private fun windowExtensionsClass(classLoader: ClassLoader) =
- classLoader.loadClass("androidx.window.extensions.WindowExtensions")
+ private val windowExtensionsClass: Class<*>
+ get() {
+ return loader.loadClass("androidx.window.extensions.WindowExtensions")
+ }
- private fun foldingFeatureClass(classLoader: ClassLoader) =
- classLoader.loadClass("androidx.window.extensions.layout.FoldingFeature")
+ private val foldingFeatureClass: Class<*>
+ get() {
+ return loader.loadClass("androidx.window.extensions.layout.FoldingFeature")
+ }
- private fun windowLayoutComponentClass(classLoader: ClassLoader) =
- classLoader.loadClass("androidx.window.extensions.layout.WindowLayoutComponent")
+ private val windowLayoutComponentClass: Class<*>
+ get() {
+ return loader.loadClass("androidx.window.extensions.layout.WindowLayoutComponent")
+ }
}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
index 683c043..9851649 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -21,6 +21,7 @@
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.core.ConsumerAdapter
import kotlinx.coroutines.flow.Flow
/**
@@ -52,6 +53,24 @@
private val DEBUG = false
private val TAG = WindowInfoTracker::class.simpleName
+ @Suppress("MemberVisibilityCanBePrivate") // Avoid synthetic accessor
+ internal val extensionBackend: WindowBackend? by lazy {
+ try {
+ val loader = WindowInfoTracker::class.java.classLoader
+ val provider = loader?.let {
+ SafeWindowLayoutComponentProvider(loader, ConsumerAdapter(loader))
+ }
+ provider?.windowLayoutComponent?.let { component ->
+ ExtensionWindowLayoutInfoBackend(component, ConsumerAdapter(loader))
+ }
+ } catch (t: Throwable) {
+ if (DEBUG) {
+ Log.d(TAG, "Failed to load WindowExtensions")
+ }
+ null
+ }
+ }
+
private var decorator: WindowInfoTrackerDecorator = EmptyDecorator
/**
@@ -64,27 +83,11 @@
@JvmName("getOrCreate")
@JvmStatic
public fun getOrCreate(context: Context): WindowInfoTracker {
- val repo = WindowInfoTrackerImpl(
- WindowMetricsCalculatorCompat,
- windowBackend(context)
- )
+ val backend = extensionBackend ?: SidecarWindowBackend.getInstance(context)
+ val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, backend)
return decorator.decorate(repo)
}
- @Suppress("MemberVisibilityCanBePrivate") // Avoid synthetic accessor
- internal fun windowBackend(context: Context): WindowBackend {
- val extensionBackend = try {
- SafeWindowLayoutComponentProvider.windowLayoutComponent
- ?.let(::ExtensionWindowLayoutInfoBackend)
- } catch (t: Throwable) {
- if (DEBUG) {
- Log.d(TAG, "Failed to load WindowExtensions")
- }
- null
- }
- return extensionBackend ?: SidecarWindowBackend.getInstance(context)
- }
-
@JvmStatic
@RestrictTo(LIBRARY_GROUP)
public fun overrideDecorator(overridingDecorator: WindowInfoTrackerDecorator) {
diff --git a/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt b/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt
new file mode 100644
index 0000000..6df72e2
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.core
+
+import android.app.Activity
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.nhaarman.mockitokotlin2.mock
+import java.util.function.Consumer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+class ConsumerAdapterTest {
+
+ internal class TestListenerInterface {
+
+ val consumers = mutableListOf<Consumer<String>>()
+
+ fun addConsumer(c: Consumer<String>) {
+ consumers.add(c)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun addConsumer(a: Activity, c: Consumer<String>) {
+ consumers.add(c)
+ }
+
+ fun removeConsumer(c: Consumer<String>) {
+ consumers.remove(c)
+ }
+ }
+
+ private val value = "SOME_VALUE"
+ private val loader = ConsumerAdapterTest::class.java.classLoader!!
+ private val listenerInterface = TestListenerInterface()
+ private val adapter = ConsumerAdapter(loader)
+
+ @Before
+ fun setUp() {
+ assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ }
+
+ @Test
+ @RequiresApi(24)
+ fun testAddByReflection() {
+ val values = mutableListOf<String>()
+ adapter.addConsumer(listenerInterface, String::class, "addConsumer") { s: String ->
+ values.add(s)
+ }
+
+ assertEquals(1, listenerInterface.consumers.size)
+ listenerInterface.consumers.first().accept(value)
+ assertEquals(listOf(value), values)
+ }
+
+ @Test
+ @RequiresApi(24)
+ fun testSubscribeByReflection() {
+ val values = mutableListOf<String>()
+ adapter.createSubscription(
+ listenerInterface,
+ String::class,
+ "addConsumer",
+ "removeConsumer",
+ mock()
+ ) { s: String ->
+ values.add(s)
+ }
+
+ assertEquals(1, listenerInterface.consumers.size)
+ listenerInterface.consumers.first().accept(value)
+ assertEquals(listOf(value), values)
+ }
+
+ @Test
+ @RequiresApi(24)
+ fun testDisposeSubscribe() {
+ val values = mutableListOf<String>()
+ val subscription = adapter.createSubscription(
+ listenerInterface,
+ String::class,
+ "addConsumer",
+ "removeConsumer",
+ mock()
+ ) { s: String ->
+ values.add(s)
+ }
+ subscription.dispose()
+
+ assertTrue(listenerInterface.consumers.isEmpty())
+ }
+
+ @Test
+ @RequiresApi(24)
+ fun testToStringAdd() {
+ val values = mutableListOf<String>()
+ val consumer: (String) -> Unit = { s: String -> values.add(s) }
+ adapter.addConsumer(listenerInterface, String::class, "addConsumer", consumer)
+ assertEquals(consumer.toString(), listenerInterface.consumers.first().toString())
+ }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/core/PredicateAdapterTest.kt b/window/window/src/test/java/androidx/window/core/PredicateAdapterTest.kt
new file mode 100644
index 0000000..cc8e69c
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/core/PredicateAdapterTest.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.core
+
+import android.os.Build
+import java.util.function.Predicate
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PredicateAdapterTest {
+
+ private val loader = PredicateAdapterTest::class.java.classLoader!!
+ private val predicate = { s: String -> s.isEmpty() }
+ private val pairPredicate = { s: String, t: String -> s == t }
+ private val adapter = PredicateAdapter(loader)
+
+ @Test
+ fun testEquals_sameReference() {
+ val obj = adapter.buildPredicate(String::class, predicate)
+
+ assertTrue(obj == obj)
+ }
+
+ @Test
+ fun testEquals_differentReference() {
+ val lhs = adapter.buildPredicate(String::class, predicate)
+ val rhs = adapter.buildPredicate(String::class, predicate)
+
+ assertFalse(lhs == rhs)
+ }
+
+ @Test
+ fun testPairEquals_sameReference() {
+ val obj = adapter.buildPairPredicate(String::class, String::class, pairPredicate)
+
+ assertTrue(obj == obj)
+ }
+
+ @Test
+ fun testPairEquals_differentReference() {
+ val lhs = adapter.buildPairPredicate(String::class, String::class, pairPredicate)
+ val rhs = adapter.buildPairPredicate(String::class, String::class, pairPredicate)
+
+ assertFalse(lhs == rhs)
+ }
+
+ @Test
+ fun testHashCode() {
+ val actual = adapter.buildPredicate(String::class, predicate).hashCode()
+ assertEquals(predicate.hashCode(), actual)
+ }
+
+ @Test
+ fun testPairHashCode() {
+ val actual = adapter.buildPairPredicate(String::class, String::class, pairPredicate)
+ .hashCode()
+ assertEquals(pairPredicate.hashCode(), actual)
+ }
+
+ @Test
+ fun testToString() {
+ val actual = adapter.buildPredicate(String::class, predicate).toString()
+ assertEquals(predicate.toString(), actual)
+ }
+
+ @Test
+ fun testPairToString() {
+ val actual = adapter.buildPairPredicate(String::class, String::class, pairPredicate)
+ .toString()
+ assertEquals(pairPredicate.toString(), actual)
+ }
+
+ @Test
+ @Suppress("UNCHECKED_CAST") //
+ fun testWrapPredicate() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return
+ }
+ val actual = adapter.buildPredicate(String::class, predicate) as Predicate<String>
+ val inputs = listOf("", "a", "abcd")
+ inputs.forEach { data ->
+ assertEquals("Checking predicate on $data", predicate(data), actual.test(data))
+ }
+ }
+
+ @Test
+ @Suppress("UNCHECKED_CAST") //
+ fun testWrapPairPredicate() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return
+ }
+ val actual = adapter.buildPairPredicate(
+ String::class,
+ String::class,
+ pairPredicate
+ ) as Predicate<Pair<String, String>>
+
+ val inputs = listOf("", "a").zip(listOf("", "b"))
+ inputs.forEach { data ->
+ assertEquals(
+ "Checking predicate on $data",
+ pairPredicate(data.first, data.second),
+ actual.test(data)
+ )
+ }
+ }
+
+ @Test
+ @Suppress("UNCHECKED_CAST") //
+ fun test_additionalPredicateMethods() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return
+ }
+ val actual = adapter.buildPredicate(String::class, predicate) as Predicate<String>
+ val innerAnd = actual.and { true }
+ val outerAnd = Predicate<String> { true }.and(actual)
+
+ val innerOr = actual.and { true }
+ val outerOr = Predicate<String> { true }.and(actual)
+
+ val notNot = actual.negate().negate()
+
+ val inputs = listOf("", "a", "abcd")
+ inputs.forEach { data ->
+ assertEquals(
+ "Checking innerAnd predicate on $data",
+ innerAnd.test(data),
+ actual.test(data)
+ )
+ assertEquals(
+ "Checking outerAnd predicate on $data",
+ outerAnd.test(data),
+ actual.test(data)
+ )
+ assertEquals(
+ "Checking innerOr predicate on $data",
+ innerOr.test(data),
+ actual.test(data)
+ )
+ assertEquals(
+ "Checking outerOr predicate on $data",
+ outerOr.test(data),
+ actual.test(data)
+ )
+ assertEquals(
+ "Checking notNot predicate on $data",
+ notNot.test(data),
+ actual.test(data)
+ )
+ }
+ }
+}
\ No newline at end of file