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