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