[go: nahoru, domu]

Use reflection to access Java 8 Predicate API.

Use reflection to use Java 8 API for window extensions.
We are running into an issue where desugar will change the Java 8 APIs
to the synthetic APIs.  We need exactly Java 8 APIs at the moment.

Bug: 203472665
Test: On a device with extensions do the following EXACTLY
 Build the samples using ./gradlew window:window-samples:assemble
 Install the samples using adb install <path to apk>
 Launch samples and expect no crash.
Test: ./gradlew window:window:test
Change-Id: I4c02b49825cdc037aba99bba824fa1f8b5b113b7
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 90bf06b..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,12 +19,13 @@
 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 androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
+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
@@ -34,14 +35,15 @@
  * 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
@@ -68,55 +70,56 @@
     }
 
     @SuppressLint("ClassVerificationFailure", "NewApi")
-    fun translateActivityPairPredicates(
-        splitPairFilters: Set<SplitPairFilter>
-    ): (Pair<Activity, Activity>) -> Boolean {
-        return { (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>
-    ): (Pair<Activity, Intent>) -> Boolean {
-        return { (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
-    ): (WindowMetrics) -> Boolean {
-        return { windowMetrics ->
+    private fun translateParentMetricsPredicate(splitRule: SplitRule): Any {
+        return predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
             splitRule.checkParentMetrics(windowMetrics)
         }
     }
 
     @SuppressLint("ClassVerificationFailure", "NewApi")
-    fun translateActivityPredicates(
-        activityFilters: Set<ActivityFilter>
-    ): (Activity) -> Boolean {
-        return { 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>
-    ): (Intent) -> Boolean {
-        return { 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
+        rule: SplitPairRule,
+        predicateClass: Class<*>
     ): OEMSplitPairRule {
-        val builder = SplitPairRuleBuilder(
+        val builder = SplitPairRuleBuilder::class.java.getConstructor(
+            predicateClass,
+            predicateClass,
+            predicateClass
+        ).newInstance(
             translateActivityPairPredicates(rule.filters),
             translateActivityIntentPredicates(rule.filters),
             translateParentMetricsPredicate(rule)
@@ -136,9 +139,15 @@
 
     @SuppressLint("WrongConstant") // Converting from Jetpack to Extensions constants
     private fun translateSplitPlaceholderRule(
-        rule: SplitPlaceholderRule
+        rule: SplitPlaceholderRule,
+        predicateClass: Class<*>
     ): OEMSplitPlaceholderRule {
-        val builder = SplitPlaceholderRuleBuilder(
+        val builder = SplitPlaceholderRuleBuilder::class.java.getConstructor(
+            Intent::class.java,
+            predicateClass,
+            predicateClass,
+            predicateClass
+        ).newInstance(
             rule.placeholderIntent,
             translateActivityPredicates(rule.filters),
             translateIntentPredicates(rule.filters),
@@ -156,8 +165,14 @@
         return builder.build()
     }
 
-    private fun translateActivityRule(rule: ActivityRule): OEMActivityRule {
-        return ActivityRuleBuilder(
+    private fun translateActivityRule(
+        rule: ActivityRule,
+        predicateClass: Class<*>
+    ): OEMActivityRule {
+        return ActivityRuleBuilder::class.java.getConstructor(
+            predicateClass,
+            predicateClass
+        ).newInstance(
             translateActivityPredicates(rule.filters),
             translateIntentPredicates(rule.filters)
         )
@@ -166,21 +181,14 @@
     }
 
     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 -> translateActivityRule(rule)
+                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
-    }
 }
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..87ccd0d 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -21,9 +21,7 @@
 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
 
 /**
  * Adapter implementation for different historical versions of activity embedding OEM interface in
@@ -34,13 +32,10 @@
     private val embeddingExtension: ActivityEmbeddingComponent,
     private val adapter: EmbeddingAdapter
 ) : 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) {
@@ -94,22 +89,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..672fb9b 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -22,6 +22,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.Consumer
 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 +75,12 @@
                 if (isExtensionVersionSupported(EmbeddingCompat.getExtensionApiLevel()) &&
                     EmbeddingCompat.isEmbeddingAvailable()
                 ) {
-                    impl = EmbeddingCompat()
+                    impl = EmbeddingBackend::class.java.classLoader?.let { loader ->
+                        EmbeddingCompat(
+                            EmbeddingCompat.embeddingComponent(),
+                            EmbeddingAdapter(PredicateAdapter(loader))
+                        )
+                    }
                     // TODO(b/190433400): Check API conformance
                 }
             } catch (t: Throwable) {
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