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