1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent 3 4 import host.exp.exponent.analytics.EXL 5 import host.exp.expoview.BuildConfig 6 import java.lang.reflect.Constructor 7 import java.lang.reflect.Field 8 import java.lang.reflect.InvocationTargetException 9 import java.lang.reflect.Method 10 11 // TODO: add type checking in DEBUG 12 class RNObject { 13 private val className: String // Unversioned 14 private var clazz: Class<*>? = null // Versioned 15 private var instance: Any? = null // Versioned 16 17 // We ignore the version of clazz 18 constructor(clazz: Class<*>?) { 19 className = removeVersionFromClass(clazz) 20 } 21 22 constructor(className: String) { 23 this.className = className 24 } 25 26 private constructor(obj: Any?) { 27 assign(obj) 28 className = removeVersionFromClass(clazz) 29 } 30 31 val isNull: Boolean 32 get() = instance == null 33 val isNotNull: Boolean 34 get() = instance != null 35 36 // required for "unversioned" flavor check 37 fun loadVersion(version: String): RNObject { 38 try { 39 clazz = if (version == UNVERSIONED || BuildConfig.FLAVOR == "unversioned") { 40 if (className.startsWith("host.exp.exponent")) { 41 Class.forName("versioned.$className") 42 } else { 43 Class.forName(className) 44 } 45 } else { 46 Class.forName("abi${version.replace('.', '_')}.$className") 47 } 48 } catch (e: ClassNotFoundException) { 49 EXL.e(TAG, e) 50 } 51 return this 52 } 53 54 fun assign(obj: Any?) { 55 if (obj != null) { 56 clazz = obj.javaClass 57 } 58 instance = obj 59 } 60 61 fun get(): Any? { 62 return instance 63 } 64 65 fun rnClass(): Class<*>? { 66 return clazz 67 } 68 69 fun version(): String { 70 return versionForClassname(clazz!!.name) 71 } 72 73 fun construct(vararg args: Any?): RNObject { 74 try { 75 instance = getConstructorWithArgumentClassTypes(clazz, *objectsToJavaClassTypes(*args)).newInstance(*args) 76 } catch (e: NoSuchMethodException) { 77 EXL.e(TAG, e) 78 } catch (e: InvocationTargetException) { 79 EXL.e(TAG, e) 80 } catch (e: InstantiationException) { 81 EXL.e(TAG, e) 82 } catch (e: IllegalAccessException) { 83 EXL.e(TAG, e) 84 } 85 return this 86 } 87 88 fun call(name: String, vararg args: Any?): Any? { 89 return callWithReceiver(instance, name, *args) 90 } 91 92 fun callRecursive(name: String, vararg args: Any?): RNObject? { 93 val result = call(name, *args) ?: return null 94 return wrap(result) 95 } 96 97 fun callStatic(name: String, vararg args: Any?): Any? { 98 return callWithReceiver(null, name, *args) 99 } 100 101 fun callStaticRecursive(name: String, vararg args: Any?): RNObject { 102 return wrap(callStatic(name, *args)) 103 } 104 105 fun setField(name: String, value: Any) { 106 setFieldWithReceiver(instance, name, value) 107 } 108 109 fun setStaticField(name: String, value: Any) { 110 setFieldWithReceiver(null, name, value) 111 } 112 113 private fun callWithReceiver(receiver: Any?, name: String, vararg args: Any?): Any? { 114 try { 115 return getMethodWithArgumentClassTypes(clazz, name, *objectsToJavaClassTypes(*args)).invoke(receiver, *args) 116 } catch (e: IllegalAccessException) { 117 EXL.e(TAG, e) 118 e.printStackTrace() 119 } catch (e: InvocationTargetException) { 120 EXL.e(TAG, e) 121 e.printStackTrace() 122 } catch (e: NoSuchMethodException) { 123 EXL.e(TAG, e) 124 e.printStackTrace() 125 } catch (e: NoSuchMethodError) { 126 EXL.e(TAG, e) 127 e.printStackTrace() 128 } catch (e: Throwable) { 129 EXL.e(TAG, "Runtime exception in RNObject when calling method $name: $e") 130 } 131 return null 132 } 133 134 private fun setFieldWithReceiver(receiver: Any?, name: String, value: Any) { 135 try { 136 getFieldWithType(clazz, name, value.javaClass)[receiver] = value 137 } catch (e: IllegalAccessException) { 138 EXL.e(TAG, e) 139 e.printStackTrace() 140 } catch (e: NoSuchFieldException) { 141 EXL.e(TAG, e) 142 e.printStackTrace() 143 } catch (e: NoSuchMethodError) { 144 EXL.e(TAG, e) 145 e.printStackTrace() 146 } catch (e: Throwable) { 147 EXL.e(TAG, "Runtime exception in RNObject when setting field $name: $e") 148 } 149 } 150 151 fun onHostResume(one: Any?, two: Any?) { 152 call("onHostResume", one, two) 153 } 154 155 fun onHostPause() { 156 call("onHostPause") 157 } 158 159 fun onHostDestroy() { 160 call("onHostDestroy") 161 } 162 163 companion object { 164 private val TAG = RNObject::class.java.simpleName 165 166 const val UNVERSIONED = "UNVERSIONED" 167 168 @JvmStatic fun wrap(obj: Any?): RNObject { 169 return RNObject(obj) 170 } 171 172 fun versionedEnum(sdkVersion: String, className: String, value: String): Any { 173 return try { 174 RNObject(className).loadVersion(sdkVersion).rnClass()!!.getDeclaredField(value)[null] 175 } catch (e: IllegalAccessException) { 176 EXL.e(TAG, e) 177 throw IllegalStateException("Unable to create enum: $className.value", e) 178 } catch (e: NoSuchFieldException) { 179 EXL.e(TAG, e) 180 throw IllegalStateException("Unable to create enum: $className.value", e) 181 } 182 } 183 184 fun versionForClassname(classname: String): String { 185 return if (classname.startsWith("abi")) { 186 val abiVersion = classname.split(".").toTypedArray()[0] 187 abiVersion.substring(3) 188 } else { 189 UNVERSIONED 190 } 191 } 192 193 private fun removeVersionFromClass(clazz: Class<*>?): String { 194 val name = clazz!!.name 195 return if (name.startsWith("abi")) { 196 name.substring(name.indexOf('.') + 1) 197 } else name 198 } 199 200 private fun objectsToJavaClassTypes(vararg objects: Any?): Array<Class<*>?> { 201 val classes: Array<Class<*>?> = arrayOfNulls(objects.size) 202 for (i in objects.indices) { 203 if (objects[i] != null) { 204 classes[i] = objects[i]!!::class.java 205 } 206 } 207 return classes 208 } 209 210 // Allow types that are too specific so that we don't have to specify exact classes 211 @Throws(NoSuchMethodException::class) 212 private fun getMethodWithArgumentClassTypes(clazz: Class<*>?, name: String, vararg argumentClassTypes: Class<*>?): Method { 213 val methods = clazz!!.methods 214 for (i in methods.indices) { 215 val method = methods[i] 216 if (method.name != name) { 217 continue 218 } 219 val currentMethodParameterTypes = method.parameterTypes 220 if (currentMethodParameterTypes.size != argumentClassTypes.size) { 221 continue 222 } 223 var isValid = true 224 for (j in currentMethodParameterTypes.indices) { 225 if (!isAssignableFrom(currentMethodParameterTypes[j], argumentClassTypes[j])) { 226 isValid = false 227 break 228 } 229 } 230 if (!isValid) { 231 continue 232 } 233 return method 234 } 235 throw NoSuchMethodException() 236 } 237 238 // Allow boxed -> unboxed assignments 239 private fun isAssignableFrom(methodParameterClassType: Class<*>, argumentClassType: Class<*>?): Boolean { 240 if (argumentClassType == null) { 241 // There's not really a good way to handle this. 242 return true 243 } 244 if (methodParameterClassType.isAssignableFrom(argumentClassType)) { 245 return true 246 } 247 if (methodParameterClassType == Boolean::class.javaPrimitiveType && (argumentClassType == java.lang.Boolean::class.java || argumentClassType == Boolean::class.java)) { 248 return true 249 } else if (methodParameterClassType == Byte::class.javaPrimitiveType && (argumentClassType == java.lang.Byte::class.java || argumentClassType == Byte::class.java)) { 250 return true 251 } else if (methodParameterClassType == Char::class.javaPrimitiveType && (argumentClassType == java.lang.Character::class.java || argumentClassType == Char::class.java)) { 252 return true 253 } else if (methodParameterClassType == Float::class.javaPrimitiveType && (argumentClassType == java.lang.Float::class.java || argumentClassType == Float::class.java)) { 254 return true 255 } else if (methodParameterClassType == Int::class.javaPrimitiveType && (argumentClassType == java.lang.Integer::class.java || argumentClassType == Int::class.java)) { 256 return true 257 } else if (methodParameterClassType == Long::class.javaPrimitiveType && (argumentClassType == java.lang.Long::class.java || argumentClassType == Long::class.java)) { 258 return true 259 } else if (methodParameterClassType == Short::class.javaPrimitiveType && (argumentClassType == java.lang.Short::class.java || argumentClassType == Short::class.java)) { 260 return true 261 } else if (methodParameterClassType == Double::class.javaPrimitiveType && (argumentClassType == java.lang.Double::class.java || argumentClassType == Double::class.java)) { 262 return true 263 } 264 return false 265 } 266 267 // Allow types that are too specific so that we don't have to specify exact classes 268 @Throws(NoSuchMethodException::class) 269 private fun getConstructorWithArgumentClassTypes(clazz: Class<*>?, vararg argumentClassTypes: Class<*>?): Constructor<*> { 270 val constructors = clazz!!.constructors 271 for (i in constructors.indices) { 272 val constructor = constructors[i] 273 val currentConstructorParameterTypes = constructor.parameterTypes 274 if (currentConstructorParameterTypes.size != argumentClassTypes.size) { 275 continue 276 } 277 var isValid = true 278 for (j in currentConstructorParameterTypes.indices) { 279 if (!isAssignableFrom(currentConstructorParameterTypes[j], argumentClassTypes[j])) { 280 isValid = false 281 break 282 } 283 } 284 if (!isValid) { 285 continue 286 } 287 return constructor 288 } 289 throw NoSuchMethodError() 290 } 291 292 @Throws(NoSuchFieldException::class) 293 private fun getFieldWithType(clazz: Class<*>?, name: String, type: Class<*>): Field { 294 val fields = clazz!!.fields 295 for (i in fields.indices) { 296 val field = fields[i] 297 if (field.name != name) { 298 continue 299 } 300 val currentFieldType = field.type 301 if (isAssignableFrom(currentFieldType, type)) { 302 return field 303 } 304 } 305 throw NoSuchFieldException() 306 } 307 } 308 } 309