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     val result = callStatic(name, *args) ?: return null
103     return wrap(result)
104   }
105 
106   fun setField(name: String, value: Any) {
107     setFieldWithReceiver(instance, name, value)
108   }
109 
110   fun setStaticField(name: String, value: Any) {
111     setFieldWithReceiver(null, name, value)
112   }
113 
114   private fun callWithReceiver(receiver: Any?, name: String, vararg args: Any?): Any? {
115     try {
116       return getMethodWithArgumentClassTypes(clazz, name, *objectsToJavaClassTypes(*args)).invoke(receiver, *args)
117     } catch (e: IllegalAccessException) {
118       EXL.e(TAG, e)
119       e.printStackTrace()
120     } catch (e: InvocationTargetException) {
121       EXL.e(TAG, e)
122       e.printStackTrace()
123     } catch (e: NoSuchMethodException) {
124       EXL.e(TAG, e)
125       e.printStackTrace()
126     } catch (e: NoSuchMethodError) {
127       EXL.e(TAG, e)
128       e.printStackTrace()
129     } catch (e: Throwable) {
130       EXL.e(TAG, "Runtime exception in RNObject when calling method $name: $e")
131     }
132     return null
133   }
134 
135   private fun setFieldWithReceiver(receiver: Any?, name: String, value: Any) {
136     try {
137       getFieldWithType(clazz, name, value.javaClass)[receiver] = value
138     } catch (e: IllegalAccessException) {
139       EXL.e(TAG, e)
140       e.printStackTrace()
141     } catch (e: NoSuchFieldException) {
142       EXL.e(TAG, e)
143       e.printStackTrace()
144     } catch (e: NoSuchMethodError) {
145       EXL.e(TAG, e)
146       e.printStackTrace()
147     } catch (e: Throwable) {
148       EXL.e(TAG, "Runtime exception in RNObject when setting field $name: $e")
149     }
150   }
151 
152   fun onHostResume(one: Any?, two: Any?) {
153     call("onHostResume", one, two)
154   }
155 
156   fun onHostPause() {
157     call("onHostPause")
158   }
159 
160   fun onHostDestroy() {
161     call("onHostDestroy")
162   }
163 
164   companion object {
165     private val TAG = RNObject::class.java.simpleName
166 
167     const val UNVERSIONED = "UNVERSIONED"
168 
169     @JvmStatic fun wrap(obj: Any): RNObject {
170       return RNObject(obj)
171     }
172 
173     fun versionedEnum(sdkVersion: String, className: String, value: String): Any {
174       return try {
175         RNObject(className).loadVersion(sdkVersion).rnClass()!!.getDeclaredField(value)[null]
176       } catch (e: IllegalAccessException) {
177         EXL.e(TAG, e)
178         throw IllegalStateException("Unable to create enum: $className.value", e)
179       } catch (e: NoSuchFieldException) {
180         EXL.e(TAG, e)
181         throw IllegalStateException("Unable to create enum: $className.value", e)
182       }
183     }
184 
185     fun versionForClassname(classname: String): String {
186       return if (classname.startsWith("abi")) {
187         val abiVersion = classname.split(".").toTypedArray()[0]
188         abiVersion.substring(3)
189       } else {
190         UNVERSIONED
191       }
192     }
193 
194     private fun removeVersionFromClass(clazz: Class<*>?): String {
195       val name = clazz!!.name
196       return if (name.startsWith("abi")) {
197         name.substring(name.indexOf('.') + 1)
198       } else name
199     }
200 
201     private fun objectsToJavaClassTypes(vararg objects: Any?): Array<Class<*>?> {
202       val classes: Array<Class<*>?> = arrayOfNulls(objects.size)
203       for (i in objects.indices) {
204         if (objects[i] != null) {
205           classes[i] = objects[i]!!::class.java
206         }
207       }
208       return classes
209     }
210 
211     // Allow types that are too specific so that we don't have to specify exact classes
212     @Throws(NoSuchMethodException::class)
213     private fun getMethodWithArgumentClassTypes(clazz: Class<*>?, name: String, vararg argumentClassTypes: Class<*>?): Method {
214       val methods = clazz!!.methods
215       for (i in methods.indices) {
216         val method = methods[i]
217         if (method.name != name) {
218           continue
219         }
220         val currentMethodParameterTypes = method.parameterTypes
221         if (currentMethodParameterTypes.size != argumentClassTypes.size) {
222           continue
223         }
224         var isValid = true
225         for (j in currentMethodParameterTypes.indices) {
226           if (!isAssignableFrom(currentMethodParameterTypes[j], argumentClassTypes[j])) {
227             isValid = false
228             break
229           }
230         }
231         if (!isValid) {
232           continue
233         }
234         return method
235       }
236       throw NoSuchMethodException()
237     }
238 
239     // Allow boxed -> unboxed assignments
240     private fun isAssignableFrom(methodParameterClassType: Class<*>, argumentClassType: Class<*>?): Boolean {
241       if (argumentClassType == null) {
242         // There's not really a good way to handle this.
243         return true
244       }
245       if (methodParameterClassType.isAssignableFrom(argumentClassType)) {
246         return true
247       }
248       if (methodParameterClassType == Boolean::class.javaPrimitiveType && (argumentClassType == java.lang.Boolean::class.java || argumentClassType == Boolean::class.java)) {
249         return true
250       } else if (methodParameterClassType == Byte::class.javaPrimitiveType && (argumentClassType == java.lang.Byte::class.java || argumentClassType == Byte::class.java)) {
251         return true
252       } else if (methodParameterClassType == Char::class.javaPrimitiveType && (argumentClassType == java.lang.Character::class.java || argumentClassType == Char::class.java)) {
253         return true
254       } else if (methodParameterClassType == Float::class.javaPrimitiveType && (argumentClassType == java.lang.Float::class.java || argumentClassType == Float::class.java)) {
255         return true
256       } else if (methodParameterClassType == Int::class.javaPrimitiveType && (argumentClassType == java.lang.Integer::class.java || argumentClassType == Int::class.java)) {
257         return true
258       } else if (methodParameterClassType == Long::class.javaPrimitiveType && (argumentClassType == java.lang.Long::class.java || argumentClassType == Long::class.java)) {
259         return true
260       } else if (methodParameterClassType == Short::class.javaPrimitiveType && (argumentClassType == java.lang.Short::class.java || argumentClassType == Short::class.java)) {
261         return true
262       } else if (methodParameterClassType == Double::class.javaPrimitiveType && (argumentClassType == java.lang.Double::class.java || argumentClassType == Double::class.java)) {
263         return true
264       }
265       return false
266     }
267 
268     // Allow types that are too specific so that we don't have to specify exact classes
269     @Throws(NoSuchMethodException::class)
270     private fun getConstructorWithArgumentClassTypes(clazz: Class<*>?, vararg argumentClassTypes: Class<*>?): Constructor<*> {
271       val constructors = clazz!!.constructors
272       for (i in constructors.indices) {
273         val constructor = constructors[i]
274         val currentConstructorParameterTypes = constructor.parameterTypes
275         if (currentConstructorParameterTypes.size != argumentClassTypes.size) {
276           continue
277         }
278         var isValid = true
279         for (j in currentConstructorParameterTypes.indices) {
280           if (!isAssignableFrom(currentConstructorParameterTypes[j], argumentClassTypes[j])) {
281             isValid = false
282             break
283           }
284         }
285         if (!isValid) {
286           continue
287         }
288         return constructor
289       }
290       throw NoSuchMethodError()
291     }
292 
293     @Throws(NoSuchFieldException::class)
294     private fun getFieldWithType(clazz: Class<*>?, name: String, type: Class<*>): Field {
295       val fields = clazz!!.fields
296       for (i in fields.indices) {
297         val field = fields[i]
298         if (field.name != name) {
299           continue
300         }
301         val currentFieldType = field.type
302         if (isAssignableFrom(currentFieldType, type)) {
303           return field
304         }
305       }
306       throw NoSuchFieldException()
307     }
308   }
309 }
310