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