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
loadVersionnull37   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 
assignnull54   fun assign(obj: Any?) {
55     if (obj != null) {
56       clazz = obj.javaClass
57     }
58     instance = obj
59   }
60 
getnull61   fun get(): Any? {
62     return instance
63   }
64 
rnClassnull65   fun rnClass(): Class<*>? {
66     return clazz
67   }
68 
versionnull69   fun version(): String {
70     return versionForClassname(clazz!!.name)
71   }
72 
constructnull73   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 
callnull88   fun call(name: String, vararg args: Any?): Any? {
89     return callWithReceiver(instance, false, name, *args)
90   }
91 
92   /** Similar to [call] but without capturing reflection [InvocationTargetException] */
callWithThrowablenull93   fun callWithThrowable(name: String, vararg args: Any?): Any? {
94     return callWithReceiver(instance, true, name, *args)
95   }
96 
callRecursivenull97   fun callRecursive(name: String, vararg args: Any?): RNObject? {
98     val result = call(name, *args) ?: return null
99     return wrap(result)
100   }
101 
callStaticnull102   fun callStatic(name: String, vararg args: Any?): Any? {
103     return callWithReceiver(null, false, name, *args)
104   }
105 
callStaticRecursivenull106   fun callStaticRecursive(name: String, vararg args: Any?): RNObject? {
107     val result = callStatic(name, *args) ?: return null
108     return wrap(result)
109   }
110 
setFieldnull111   fun setField(name: String, value: Any) {
112     setFieldWithReceiver(instance, name, value)
113   }
114 
setStaticFieldnull115   fun setStaticField(name: String, value: Any) {
116     setFieldWithReceiver(null, name, value)
117   }
118 
callWithReceivernull119   private fun callWithReceiver(receiver: Any?, rethrow: Boolean, name: String, vararg args: Any?): Any? {
120     try {
121       return getMethodWithArgumentClassTypes(clazz, name, *objectsToJavaClassTypes(*args)).invoke(receiver, *args)
122     } catch (e: IllegalAccessException) {
123       EXL.e(TAG, e)
124       e.printStackTrace()
125     } catch (e: InvocationTargetException) {
126       EXL.e(TAG, e)
127       e.printStackTrace()
128       if (rethrow) {
129         e.cause?.let {
130           throw it
131         }
132       }
133     } catch (e: NoSuchMethodException) {
134       EXL.e(TAG, e)
135       e.printStackTrace()
136     } catch (e: NoSuchMethodError) {
137       EXL.e(TAG, e)
138       e.printStackTrace()
139     } catch (e: Throwable) {
140       EXL.e(TAG, "Runtime exception in RNObject when calling method $name: $e")
141     }
142     return null
143   }
144 
setFieldWithReceivernull145   private fun setFieldWithReceiver(receiver: Any?, name: String, value: Any) {
146     try {
147       getFieldWithType(clazz, name, value.javaClass)[receiver] = value
148     } catch (e: IllegalAccessException) {
149       EXL.e(TAG, e)
150       e.printStackTrace()
151     } catch (e: NoSuchFieldException) {
152       EXL.e(TAG, e)
153       e.printStackTrace()
154     } catch (e: NoSuchMethodError) {
155       EXL.e(TAG, e)
156       e.printStackTrace()
157     } catch (e: Throwable) {
158       EXL.e(TAG, "Runtime exception in RNObject when setting field $name: $e")
159     }
160   }
161 
onHostResumenull162   fun onHostResume(one: Any?, two: Any?) {
163     call("onHostResume", one, two)
164   }
165 
onHostPausenull166   fun onHostPause() {
167     call("onHostPause")
168   }
169 
onHostDestroynull170   fun onHostDestroy() {
171     call("onHostDestroy")
172   }
173 
174   companion object {
175     private val TAG = RNObject::class.java.simpleName
176 
177     const val UNVERSIONED = "UNVERSIONED"
178 
wrapnull179     @JvmStatic fun wrap(obj: Any): RNObject {
180       return RNObject(obj)
181     }
182 
versionedEnumnull183     fun versionedEnum(sdkVersion: String, className: String, value: String): Any {
184       return try {
185         RNObject(className).loadVersion(sdkVersion).rnClass()!!.getDeclaredField(value)[null]
186       } catch (e: IllegalAccessException) {
187         EXL.e(TAG, e)
188         throw IllegalStateException("Unable to create enum: $className.value", e)
189       } catch (e: NoSuchFieldException) {
190         EXL.e(TAG, e)
191         throw IllegalStateException("Unable to create enum: $className.value", e)
192       }
193     }
194 
versionForClassnamenull195     fun versionForClassname(classname: String): String {
196       return if (classname.startsWith("abi")) {
197         val abiVersion = classname.split(".").toTypedArray()[0]
198         abiVersion.substring(3)
199       } else {
200         UNVERSIONED
201       }
202     }
203 
removeVersionFromClassnull204     private fun removeVersionFromClass(clazz: Class<*>?): String {
205       val name = clazz!!.name
206       return if (name.startsWith("abi")) {
207         name.substring(name.indexOf('.') + 1)
208       } else name
209     }
210 
objectsToJavaClassTypesnull211     private fun objectsToJavaClassTypes(vararg objects: Any?): Array<Class<*>?> {
212       val classes: Array<Class<*>?> = arrayOfNulls(objects.size)
213       for (i in objects.indices) {
214         if (objects[i] != null) {
215           classes[i] = objects[i]!!::class.java
216         }
217       }
218       return classes
219     }
220 
221     // Allow types that are too specific so that we don't have to specify exact classes
222     @Throws(NoSuchMethodException::class)
getMethodWithArgumentClassTypesnull223     private fun getMethodWithArgumentClassTypes(clazz: Class<*>?, name: String, vararg argumentClassTypes: Class<*>?): Method {
224       val methods = clazz!!.methods
225       for (i in methods.indices) {
226         val method = methods[i]
227         if (method.name != name) {
228           continue
229         }
230         val currentMethodParameterTypes = method.parameterTypes
231         if (currentMethodParameterTypes.size != argumentClassTypes.size) {
232           continue
233         }
234         var isValid = true
235         for (j in currentMethodParameterTypes.indices) {
236           if (!isAssignableFrom(currentMethodParameterTypes[j], argumentClassTypes[j])) {
237             isValid = false
238             break
239           }
240         }
241         if (!isValid) {
242           continue
243         }
244         return method
245       }
246       throw NoSuchMethodException()
247     }
248 
249     // Allow boxed -> unboxed assignments
isAssignableFromnull250     private fun isAssignableFrom(methodParameterClassType: Class<*>, argumentClassType: Class<*>?): Boolean {
251       if (argumentClassType == null) {
252         // There's not really a good way to handle this.
253         return true
254       }
255       if (methodParameterClassType.isAssignableFrom(argumentClassType)) {
256         return true
257       }
258       if (methodParameterClassType == Boolean::class.javaPrimitiveType && (argumentClassType == java.lang.Boolean::class.java || argumentClassType == Boolean::class.java)) {
259         return true
260       } else if (methodParameterClassType == Byte::class.javaPrimitiveType && (argumentClassType == java.lang.Byte::class.java || argumentClassType == Byte::class.java)) {
261         return true
262       } else if (methodParameterClassType == Char::class.javaPrimitiveType && (argumentClassType == java.lang.Character::class.java || argumentClassType == Char::class.java)) {
263         return true
264       } else if (methodParameterClassType == Float::class.javaPrimitiveType && (argumentClassType == java.lang.Float::class.java || argumentClassType == Float::class.java)) {
265         return true
266       } else if (methodParameterClassType == Int::class.javaPrimitiveType && (argumentClassType == java.lang.Integer::class.java || argumentClassType == Int::class.java)) {
267         return true
268       } else if (methodParameterClassType == Long::class.javaPrimitiveType && (argumentClassType == java.lang.Long::class.java || argumentClassType == Long::class.java)) {
269         return true
270       } else if (methodParameterClassType == Short::class.javaPrimitiveType && (argumentClassType == java.lang.Short::class.java || argumentClassType == Short::class.java)) {
271         return true
272       } else if (methodParameterClassType == Double::class.javaPrimitiveType && (argumentClassType == java.lang.Double::class.java || argumentClassType == Double::class.java)) {
273         return true
274       }
275       return false
276     }
277 
278     // Allow types that are too specific so that we don't have to specify exact classes
279     @Throws(NoSuchMethodException::class)
getConstructorWithArgumentClassTypesnull280     private fun getConstructorWithArgumentClassTypes(clazz: Class<*>?, vararg argumentClassTypes: Class<*>?): Constructor<*> {
281       val constructors = clazz!!.constructors
282       for (i in constructors.indices) {
283         val constructor = constructors[i]
284         val currentConstructorParameterTypes = constructor.parameterTypes
285         if (currentConstructorParameterTypes.size != argumentClassTypes.size) {
286           continue
287         }
288         var isValid = true
289         for (j in currentConstructorParameterTypes.indices) {
290           if (!isAssignableFrom(currentConstructorParameterTypes[j], argumentClassTypes[j])) {
291             isValid = false
292             break
293           }
294         }
295         if (!isValid) {
296           continue
297         }
298         return constructor
299       }
300       throw NoSuchMethodError()
301     }
302 
303     @Throws(NoSuchFieldException::class)
getFieldWithTypenull304     private fun getFieldWithType(clazz: Class<*>?, name: String, type: Class<*>): Field {
305       val fields = clazz!!.fields
306       for (i in fields.indices) {
307         val field = fields[i]
308         if (field.name != name) {
309           continue
310         }
311         val currentFieldType = field.type
312         if (isAssignableFrom(currentFieldType, type)) {
313           return field
314         }
315       }
316       throw NoSuchFieldException()
317     }
318   }
319 }
320