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