1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.window.core 18 19 import android.annotation.SuppressLint 20 import android.app.Activity 21 import android.content.Context 22 import androidx.annotation.CheckResult 23 import java.lang.reflect.InvocationHandler 24 import java.lang.reflect.Method 25 import java.lang.reflect.Proxy 26 import kotlin.reflect.KClass 27 import kotlin.reflect.cast 28 29 /** 30 * An adapter over {@link java.util.function.Consumer} to workaround mismatch in expected extension 31 * API signatures after library desugaring. See b/203472665 32 */ 33 @SuppressLint("BanUncheckedReflection") 34 internal class ConsumerAdapter(private val loader: ClassLoader) { consumerClassOrNullnull35 internal fun consumerClassOrNull(): Class<*>? { 36 return try { 37 unsafeConsumerClass() 38 } catch (e: ClassNotFoundException) { 39 null 40 } 41 } 42 unsafeConsumerClassnull43 private fun unsafeConsumerClass(): Class<*> { 44 return loader.loadClass("java.util.function.Consumer") 45 } 46 47 internal interface Subscription { disposenull48 fun dispose() 49 } 50 51 private fun <T : Any> buildConsumer(clazz: KClass<T>, consumer: (T) -> Unit): Any { 52 val handler = ConsumerHandler(clazz, consumer) 53 return Proxy.newProxyInstance(loader, arrayOf(unsafeConsumerClass()), handler) 54 } 55 addConsumernull56 fun <T : Any> addConsumer( 57 obj: Any, 58 clazz: KClass<T>, 59 methodName: String, 60 consumer: (T) -> Unit 61 ) { 62 obj.javaClass 63 .getMethod(methodName, unsafeConsumerClass()) 64 .invoke(obj, buildConsumer(clazz, consumer)) 65 } 66 67 @CheckResult createSubscriptionnull68 fun <T : Any> createSubscription( 69 obj: Any, 70 clazz: KClass<T>, 71 addMethodName: String, 72 removeMethodName: String, 73 activity: Activity, 74 consumer: (T) -> Unit 75 ): Subscription { 76 val javaConsumer = buildConsumer(clazz, consumer) 77 obj.javaClass 78 .getMethod(addMethodName, Activity::class.java, unsafeConsumerClass()) 79 .invoke(obj, activity, javaConsumer) 80 val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass()) 81 return object : Subscription { 82 override fun dispose() { 83 removeMethod.invoke(obj, javaConsumer) 84 } 85 } 86 } 87 88 @CheckResult createSubscriptionNoActivitynull89 fun <T : Any> createSubscriptionNoActivity( 90 obj: Any, 91 clazz: KClass<T>, 92 addMethodName: String, 93 removeMethodName: String, 94 consumer: (T) -> Unit 95 ): Subscription { 96 val javaConsumer = buildConsumer(clazz, consumer) 97 obj.javaClass.getMethod(addMethodName, unsafeConsumerClass()).invoke(obj, javaConsumer) 98 val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass()) 99 return object : Subscription { 100 override fun dispose() { 101 removeMethod.invoke(obj, javaConsumer) 102 } 103 } 104 } 105 106 @CheckResult createSubscriptionnull107 fun <T : Any> createSubscription( 108 obj: Any, 109 clazz: KClass<T>, 110 addMethodName: String, 111 removeMethodName: String, 112 context: Context, 113 consumer: (T) -> Unit 114 ): Subscription { 115 val javaConsumer = buildConsumer(clazz, consumer) 116 obj.javaClass 117 .getMethod(addMethodName, Context::class.java, unsafeConsumerClass()) 118 .invoke(obj, context, javaConsumer) 119 val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass()) 120 return object : Subscription { 121 override fun dispose() { 122 removeMethod.invoke(obj, javaConsumer) 123 } 124 } 125 } 126 127 /** 128 * Similar to {@link #createSubscription} but without needing to provide a {@code 129 * removeMethodName} due to it being handled on the extensions side 130 */ createConsumernull131 fun <T : Any> createConsumer( 132 obj: Any, 133 clazz: KClass<T>, 134 addMethodName: String, 135 activity: Activity, 136 consumer: (T) -> Unit 137 ) { 138 val javaConsumer = buildConsumer(clazz, consumer) 139 obj.javaClass 140 .getMethod(addMethodName, Activity::class.java, unsafeConsumerClass()) 141 .invoke(obj, activity, javaConsumer) 142 } 143 144 private class ConsumerHandler<T : Any>( 145 private val clazz: KClass<T>, 146 private val consumer: (T) -> Unit 147 ) : InvocationHandler { invokenull148 override fun invoke(obj: Any, method: Method, parameters: Array<out Any>?): Any { 149 return when { 150 method.isAccept(parameters) -> { 151 val argument = clazz.cast(parameters?.get(0)) 152 invokeAccept(argument) 153 } 154 method.isEquals(parameters) -> { 155 obj === parameters?.get(0) 156 } 157 method.isHashCode(parameters) -> { 158 consumer.hashCode() 159 } 160 method.isToString(parameters) -> { 161 // MulticastConsumer#accept must not be obfuscated by proguard if kotlin-reflect 162 // is included. Otherwise, invocation of consumer#toString (e.g. by the library 163 // or by the on-device implementation) will crash due to kotlin-reflect not 164 // finding MulticastConsumer#accept. 165 consumer.toString() 166 } 167 else -> { 168 throw UnsupportedOperationException( 169 "Unexpected method call object:$obj, method: $method, args: $parameters" 170 ) 171 } 172 } 173 } 174 invokeAcceptnull175 fun invokeAccept(parameter: T) { 176 consumer(parameter) 177 } 178 Methodnull179 private fun Method.isEquals(args: Array<out Any>?): Boolean { 180 return name == "equals" && returnType.equals(Boolean::class.java) && args?.size == 1 181 } 182 isHashCodenull183 private fun Method.isHashCode(args: Array<out Any>?): Boolean { 184 return name == "hashCode" && returnType.equals(Int::class.java) && args == null 185 } 186 isAcceptnull187 private fun Method.isAccept(args: Array<out Any>?): Boolean { 188 return name == "accept" && args?.size == 1 189 } 190 Methodnull191 private fun Method.isToString(args: Array<out Any>?): Boolean { 192 return name == "toString" && returnType.equals(String::class.java) && args == null 193 } 194 } 195 } 196