1 /*
<lambda>null2  * Copyright 2021 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.compose.ui.tooling
18 
19 import androidx.compose.runtime.Composer
20 import androidx.compose.ui.ExperimentalComposeUiApi
21 import java.lang.reflect.Method
22 import java.lang.reflect.Modifier
23 import kotlin.math.ceil
24 
25 /** A utility object to invoke composable function by its name and containing class. */
26 @Deprecated("Use androidx.compose.runtime.reflect.ComposableMethodInvoker instead")
27 object ComposableInvoker {
28 
29     /**
30      * Compares the parameter types taken from the composable method and checks if they are all
31      * compatible with the types taken from the PreviewParameterProvider.
32      *
33      * @param composableMethodTypes types of the Composable Method
34      * @param previewParameterTypes types defined in the PreviewParameterProvider
35      * @return true if every `composableMethodTypes[n]` are equal or assignable to
36      *   `previewParameterTypes[n]`.
37      */
38     private fun areParameterTypesCompatible(
39         composableMethodTypes: Array<Class<*>>,
40         previewParameterTypes: Array<Class<*>>
41     ): Boolean =
42         composableMethodTypes.size == previewParameterTypes.size &&
43             composableMethodTypes
44                 .mapIndexed { index, clazz ->
45                     val composableParameterType = previewParameterTypes[index]
46                     // We can't use [isAssignableFrom] if we have java primitives.
47                     // Java primitives aren't equal to Java classes:
48                     // comparing int with kotlin.Int or java.lang.Integer will return false.
49                     // However, if we convert them both to a KClass they can be compared:
50                     // int and java.lang.Integer will be both converted to Int
51                     // see more:
52                     // https://docs.oracle.com/javase/6/docs/api/java/lang/Class.html#isAssignableFrom(java.lang.Class)
53                     clazz.kotlin == composableParameterType.kotlin ||
54                         clazz.isAssignableFrom(composableParameterType)
55                 }
56                 .all { it }
57 
58     /**
59      * Takes the declared methods and accounts for compatible types so the signature does not need
60      * to exactly match. This allows finding method calls that use subclasses as parameters instead
61      * of the exact types.
62      *
63      * @return the compatible [Method] with the name [methodName]
64      * @throws NoSuchMethodException if the method is not found
65      */
66     private fun Array<Method>.findCompatibleComposeMethod(
67         methodName: String,
68         vararg args: Class<*>
69     ): Method =
70         firstOrNull {
71             (methodName == it.name || it.name.startsWith("$methodName-")) &&
72                 // Methods with inlined classes as parameter will have the name mangled
73                 // so we need to check for methodName-xxxx as well
74                 areParameterTypesCompatible(it.parameterTypes, arrayOf(*args))
75         } ?: throw NoSuchMethodException("$methodName not found")
76 
77     private inline fun <reified T> T.dup(count: Int): Array<T> {
78         return (0 until count).map { this }.toTypedArray()
79     }
80 
81     /**
82      * Find the given method by name. If the method has parameters, this function will try to find
83      * the version that accepts default parameters.
84      *
85      * @return null if the composable method is not found. Returns the [Method] otherwise.
86      */
87     private fun Class<*>.findComposableMethod(
88         methodName: String,
89         vararg previewParamArgs: Any?
90     ): Method? {
91         val argsArray: Array<Class<out Any>> =
92             previewParamArgs.mapNotNull { it?.javaClass }.toTypedArray()
93         return try {
94             // without defaults
95             val changedParamsCount = changedParamCount(argsArray.size, 0)
96             val changedParams = Int::class.java.dup(changedParamsCount)
97             declaredMethods.findCompatibleComposeMethod(
98                 methodName,
99                 *argsArray,
100                 Composer::class.java, // composer param
101                 *changedParams // changed param
102             )
103         } catch (e: ReflectiveOperationException) {
104             try {
105                 declaredMethods.find {
106                     it.name == methodName ||
107                         // Methods with inlined classes as parameter will have the name mangled
108                         // so we need to check for methodName-xxxx as well
109                         it.name.startsWith("$methodName-")
110                 }
111             } catch (e: ReflectiveOperationException) {
112                 null
113             }
114         }
115     }
116 
117     /**
118      * Returns the default value for the [Class] type. This will be 0 for numeric types, false for
119      * boolean, '0' for char and null for object references.
120      */
121     private fun Class<*>.getDefaultValue(): Any? =
122         when (name) {
123             "int" -> 0.toInt()
124             "short" -> 0.toShort()
125             "byte" -> 0.toByte()
126             "long" -> 0.toLong()
127             "double" -> 0.toDouble()
128             "float" -> 0.toFloat()
129             "boolean" -> false
130             "char" -> 0.toChar()
131             else -> null
132         }
133 
134     /**
135      * Calls the method on the given [instance]. If the method accepts default values, this function
136      * will call it with the correct options set.
137      */
138     @Suppress("BanUncheckedReflection")
139     private fun Method.invokeComposableMethod(
140         instance: Any?,
141         composer: Composer,
142         vararg args: Any?
143     ): Any? {
144         val composerIndex = parameterTypes.indexOfLast { it == Composer::class.java }
145         val realParams = composerIndex
146         val thisParams = if (instance != null) 1 else 0
147         val changedParams = changedParamCount(realParams, thisParams)
148         val totalParamsWithoutDefaults =
149             realParams +
150                 1 + // composer
151                 changedParams
152         val totalParams = parameterTypes.size
153         val isDefault = totalParams != totalParamsWithoutDefaults
154         val defaultParams = if (isDefault) defaultParamCount(realParams) else 0
155 
156         check(
157             realParams +
158                 1 + // composer
159                 changedParams +
160                 defaultParams == totalParams
161         ) {
162             "params don't add up to total params"
163         }
164 
165         val changedStartIndex = composerIndex + 1
166         val defaultStartIndex = changedStartIndex + changedParams
167 
168         val arguments =
169             Array(totalParams) { idx ->
170                 when (idx) {
171                     // pass in "empty" value for all real parameters since we will be using
172                     // defaults.
173                     in 0 until realParams ->
174                         args.getOrElse(idx) { parameterTypes[idx].getDefaultValue() }
175                     // the composer is the first synthetic parameter
176                     composerIndex -> composer
177                     // since this is the root we don't need to be anything unique. 0 should suffice.
178                     // changed parameters should be 0 to indicate "uncertain"
179                     in changedStartIndex until defaultStartIndex -> 0
180                     // Default values mask, all parameters set to use defaults
181                     in defaultStartIndex until totalParams -> 0b111111111111111111111.toInt()
182                     else -> error("Unexpected index")
183                 }
184             }
185         return invoke(instance, *arguments)
186     }
187 
188     private const val SLOTS_PER_INT = 10
189     private const val BITS_PER_INT = 31
190 
191     private fun changedParamCount(realValueParams: Int, thisParams: Int): Int {
192         if (realValueParams == 0) return 1
193         val totalParams = realValueParams + thisParams
194         return ceil(totalParams.toDouble() / SLOTS_PER_INT.toDouble()).toInt()
195     }
196 
197     private fun defaultParamCount(realValueParams: Int): Int {
198         return ceil(realValueParams.toDouble() / BITS_PER_INT.toDouble()).toInt()
199     }
200 
201     /**
202      * Invokes the given [methodName] belonging to the given [className]. The [methodName] is
203      * expected to be a Composable function. This method [args] will be forwarded to the Composable
204      * function.
205      */
206     @ExperimentalComposeUiApi
207     fun invokeComposable(
208         className: String,
209         methodName: String,
210         composer: Composer,
211         vararg args: Any?
212     ) {
213         try {
214             val composableClass = Class.forName(className)
215             val method =
216                 composableClass.findComposableMethod(methodName, *args)
217                     ?: throw NoSuchMethodException("Composable $className.$methodName not found")
218             method.isAccessible = true
219 
220             if (Modifier.isStatic(method.modifiers)) {
221                 // This is a top level or static method
222                 method.invokeComposableMethod(null, composer, *args)
223             } else {
224                 // The method is part of a class. We try to instantiate the class with an empty
225                 // constructor.
226                 val instance = composableClass.getConstructor().newInstance()
227                 method.invokeComposableMethod(instance, composer, *args)
228             }
229         } catch (e: Exception) {
230             PreviewLogger.logWarning("Failed to invoke Composable Method '$className.$methodName'")
231             throw e
232         }
233     }
234 }
235